/* code.js — bundle concaténé Généré: 2025-10-08T10:03:49.358Z Source: lib Fichiers: 50 Ordre: topo */ /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/ErrorReporting.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: lib/error-reporting.js - CONVERTI POUR NODE.JS // Description: Système de validation et rapport d'erreur // ======================================== // Lazy loading des modules externes (évite blocage googleapis) let google, nodemailer; const fs = require('fs').promises; const path = require('path'); const pino = require('pino'); const pretty = require('pino-pretty'); const { PassThrough } = require('stream'); const WebSocket = require('ws'); // Configuration const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c'; // WebSocket server for real-time logs let wsServer; const wsClients = new Set(); // Enhanced Pino logger configuration with real-time streaming and dated files const now = new Date(); const timestamp = now.toISOString().slice(0, 10) + '_' + now.toLocaleTimeString('fr-FR').replace(/:/g, '-'); const logFile = path.join(__dirname, '..', 'logs', `seo-generator-${timestamp}.log`); const prettyStream = pretty({ colorize: true, translateTime: 'HH:MM:ss.l', ignore: 'pid,hostname', }); const tee = new PassThrough(); // Lazy loading des pipes console (évite blocage à l'import) let consolePipeInitialized = false; // File destination with dated filename - FORCE DEBUG LEVEL const fileDest = pino.destination({ dest: logFile, mkdir: true, sync: false, minLength: 0 // Force immediate write even for small logs }); tee.pipe(fileDest); // Custom levels for Pino to include TRACE, PROMPT, and LLM const customLevels = { trace: 5, // Below debug (10) debug: 10, info: 20, prompt: 25, // New level for prompts (between info and warn) llm: 26, // New level for LLM interactions (between prompt and warn) warn: 30, error: 40, fatal: 50 }; // Pino logger instance with enhanced configuration and custom levels const logger = pino( { level: 'debug', // FORCE DEBUG LEVEL for file logging base: undefined, timestamp: pino.stdTimeFunctions.isoTime, customLevels: customLevels, useOnlyCustomLevels: true }, tee ); // Initialize WebSocket server (only when explicitly requested) function initWebSocketServer() { if (!wsServer && process.env.ENABLE_LOG_WS === 'true') { try { const logPort = process.env.LOG_WS_PORT || 8082; wsServer = new WebSocket.Server({ port: logPort }); wsServer.on('connection', (ws) => { wsClients.add(ws); logger.info('Client connected to log WebSocket'); ws.on('close', () => { wsClients.delete(ws); logger.info('Client disconnected from log WebSocket'); }); ws.on('error', (error) => { logger.error('WebSocket error:', error.message); wsClients.delete(ws); }); }); wsServer.on('error', (error) => { if (error.code === 'EADDRINUSE') { logger.warn(`WebSocket port ${logPort} already in use`); wsServer = null; } else { logger.error('WebSocket server error:', error.message); } }); logger.info(`Log WebSocket server started on port ${logPort}`); } catch (error) { logger.warn(`Failed to start WebSocket server: ${error.message}`); wsServer = null; } } } // Broadcast log to WebSocket clients function broadcastLog(message, level) { const logData = { timestamp: new Date().toISOString(), level: level.toUpperCase(), message: message }; wsClients.forEach(ws => { if (ws.readyState === WebSocket.OPEN) { try { ws.send(JSON.stringify(logData)); } catch (error) { logger.error('Failed to send log to WebSocket client:', error.message); wsClients.delete(ws); } } }); } // 🔄 NODE.JS : Google Sheets API setup (remplace SpreadsheetApp) let sheets; let auth; async function initGoogleSheets() { if (!sheets) { // Lazy load googleapis seulement quand nécessaire if (!google) { google = require('googleapis').google; } // Configuration auth Google Sheets API // Pour la démo, on utilise une clé de service (à configurer) auth = new google.auth.GoogleAuth({ keyFile: process.env.GOOGLE_CREDENTIALS_PATH, // Chemin vers fichier JSON credentials scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); sheets = google.sheets({ version: 'v4', auth }); } return sheets; } async function logSh(message, level = 'INFO') { // Initialize WebSocket server if not already done if (!wsServer) { initWebSocketServer(); } // Initialize console pipe if needed (lazy loading) if (!consolePipeInitialized && process.env.ENABLE_CONSOLE_LOG === 'true') { tee.pipe(prettyStream).pipe(process.stdout); consolePipeInitialized = true; } // Convert level to lowercase for Pino const pinoLevel = level.toLowerCase(); // Enhanced trace metadata for hierarchical logging const traceData = {}; if (message.includes('▶') || message.includes('✔') || message.includes('✖') || message.includes('•')) { traceData.trace = true; traceData.evt = message.includes('▶') ? 'span.start' : message.includes('✔') ? 'span.end' : message.includes('✖') ? 'span.error' : 'span.event'; } // Log with Pino (handles console output with pretty formatting and file logging) switch (pinoLevel) { case 'error': logger.error(traceData, message); break; case 'warning': case 'warn': logger.warn(traceData, message); break; case 'debug': logger.debug(traceData, message); break; case 'trace': logger.trace(traceData, message); break; case 'prompt': logger.prompt(traceData, message); break; case 'llm': logger.llm(traceData, message); break; default: logger.info(traceData, message); } // Broadcast to WebSocket clients for real-time viewing broadcastLog(message, level); // Force immediate flush to ensure real-time display and prevent log loss logger.flush(); // Log to Google Sheets if enabled (async, non-blocking) if (process.env.ENABLE_SHEETS_LOGGING === 'true') { setImmediate(() => { logToGoogleSheets(message, level).catch(err => { // Silent fail for Google Sheets logging to avoid recursion }); }); } } // Fonction pour déterminer si on doit logger en console function shouldLogToConsole(messageLevel, configLevel) { const levels = { DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3 }; return levels[messageLevel] >= levels[configLevel]; } // Log to file is now handled by Pino transport // This function is kept for compatibility but does nothing async function logToFile(message, level) { // Pino handles file logging via transport configuration // This function is deprecated and kept for compatibility only } // 🔄 NODE.JS : Log vers Google Sheets (version async) async function logToGoogleSheets(message, level) { try { const sheetsApi = await initGoogleSheets(); const values = [[ new Date().toISOString(), level, message, 'Node.js workflow' ]]; await sheetsApi.spreadsheets.values.append({ spreadsheetId: SHEET_ID, range: 'Logs!A:D', valueInputOption: 'RAW', insertDataOption: 'INSERT_ROWS', resource: { values } }); } catch (error) { logSh('Échec log Google Sheets: ' + error.message, 'WARNING'); // Using logSh instead of console.warn } } // 🔄 NODE.JS : Version simplifiée cleanLogSheet async function cleanLogSheet() { try { logSh('🧹 Nettoyage logs...', 'INFO'); // Using logSh instead of console.log // 1. Nettoyer fichiers logs locaux (garder 7 derniers jours) await cleanLocalLogs(); // 2. Nettoyer Google Sheets si activé if (process.env.ENABLE_SHEETS_LOGGING === 'true') { await cleanGoogleSheetsLogs(); } logSh('✅ Logs nettoyés', 'INFO'); // Using logSh instead of console.log } catch (error) { logSh('Erreur nettoyage logs: ' + error.message, 'ERROR'); // Using logSh instead of console.error } } async function cleanLocalLogs() { try { // Note: With Pino, log files are managed differently // This function is kept for compatibility with Google Sheets logs cleanup // Pino log rotation should be handled by external tools like logrotate // For now, we keep the basic cleanup for any remaining old log files const logsDir = path.join(__dirname, '../logs'); try { const files = await fs.readdir(logsDir); const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - 7); // Garder 7 jours for (const file of files) { if (file.endsWith('.log')) { const filePath = path.join(logsDir, file); const stats = await fs.stat(filePath); if (stats.mtime < cutoffDate) { await fs.unlink(filePath); logSh(`🗑️ Supprimé log ancien: ${file}`, 'INFO'); } } } } catch (error) { // Directory might not exist, that's fine } } catch (error) { // Silent fail } } async function cleanGoogleSheetsLogs() { try { const sheetsApi = await initGoogleSheets(); // Clear + remettre headers await sheetsApi.spreadsheets.values.clear({ spreadsheetId: SHEET_ID, range: 'Logs!A:D' }); await sheetsApi.spreadsheets.values.update({ spreadsheetId: SHEET_ID, range: 'Logs!A1:D1', valueInputOption: 'RAW', resource: { values: [['Timestamp', 'Level', 'Message', 'Source']] } }); } catch (error) { logSh('Échec nettoyage Google Sheets: ' + error.message, 'WARNING'); // Using logSh instead of console.warn } } // ============= VALIDATION PRINCIPALE - IDENTIQUE ============= function validateWorkflowIntegrity(elements, generatedContent, finalXML, csvData) { logSh('🔍 >>> VALIDATION INTÉGRITÉ WORKFLOW <<<', 'INFO'); // Using logSh instead of console.log const errors = []; const warnings = []; const stats = { elementsExtracted: elements.length, contentGenerated: Object.keys(generatedContent).length, tagsReplaced: 0, tagsRemaining: 0 }; // TEST 1: Détection tags dupliqués const duplicateCheck = detectDuplicateTags(elements); if (duplicateCheck.hasDuplicates) { errors.push({ type: 'DUPLICATE_TAGS', severity: 'HIGH', message: `Tags dupliqués détectés: ${duplicateCheck.duplicates.join(', ')}`, impact: 'Certains contenus ne seront pas remplacés dans le XML final', suggestion: 'Vérifier le template XML pour corriger la structure' }); } // TEST 2: Cohérence éléments extraits vs générés const missingGeneration = elements.filter(el => !generatedContent[el.originalTag]); if (missingGeneration.length > 0) { errors.push({ type: 'MISSING_GENERATION', severity: 'HIGH', message: `${missingGeneration.length} éléments extraits mais non générés`, details: missingGeneration.map(el => el.originalTag), impact: 'Contenu incomplet dans le XML final' }); } // TEST 3: Tags non remplacés dans XML final const remainingTags = (finalXML.match(/\|[^|]*\|/g) || []); stats.tagsRemaining = remainingTags.length; if (remainingTags.length > 0) { errors.push({ type: 'UNREPLACED_TAGS', severity: 'HIGH', message: `${remainingTags.length} tags non remplacés dans le XML final`, details: remainingTags.slice(0, 5), impact: 'XML final contient des placeholders non remplacés' }); } // TEST 4: Variables CSV manquantes const missingVars = detectMissingCSVVariables(csvData); if (missingVars.length > 0) { warnings.push({ type: 'MISSING_CSV_VARIABLES', severity: 'MEDIUM', message: `Variables CSV manquantes: ${missingVars.join(', ')}`, impact: 'Système de génération de mots-clés automatique activé' }); } // TEST 5: Qualité génération IA const generationQuality = assessGenerationQuality(generatedContent); if (generationQuality.errorRate > 0.1) { warnings.push({ type: 'GENERATION_QUALITY', severity: 'MEDIUM', message: `${(generationQuality.errorRate * 100).toFixed(1)}% d'erreurs de génération IA`, impact: 'Qualité du contenu potentiellement dégradée' }); } // CALCUL STATS FINALES stats.tagsReplaced = elements.length - remainingTags.length; stats.successRate = stats.elementsExtracted > 0 ? ((stats.tagsReplaced / elements.length) * 100).toFixed(1) : '100'; const report = { timestamp: new Date().toISOString(), csvData: { mc0: csvData.mc0, t0: csvData.t0 }, stats: stats, errors: errors, warnings: warnings, status: errors.length === 0 ? 'SUCCESS' : 'ERROR' }; const logLevel = report.status === 'SUCCESS' ? 'INFO' : 'ERROR'; logSh(`✅ Validation terminée: ${report.status} (${errors.length} erreurs, ${warnings.length} warnings)`, 'INFO'); // Using logSh instead of console.log // ENVOYER RAPPORT SI ERREURS (async en arrière-plan) if (errors.length > 0 || warnings.length > 2) { sendErrorReport(report).catch(err => { logSh('Erreur envoi rapport: ' + err.message, 'ERROR'); // Using logSh instead of console.error }); } return report; } // ============= HELPERS - IDENTIQUES ============= function detectDuplicateTags(elements) { const tagCounts = {}; const duplicates = []; elements.forEach(element => { const tag = element.originalTag; tagCounts[tag] = (tagCounts[tag] || 0) + 1; if (tagCounts[tag] === 2) { duplicates.push(tag); logSh(`❌ DUPLICATE détecté: ${tag}`, 'ERROR'); // Using logSh instead of console.error } }); return { hasDuplicates: duplicates.length > 0, duplicates: duplicates, counts: tagCounts }; } function detectMissingCSVVariables(csvData) { const missing = []; if (!csvData.mcPlus1 || csvData.mcPlus1.split(',').length < 4) { missing.push('MC+1 (insuffisant)'); } if (!csvData.tPlus1 || csvData.tPlus1.split(',').length < 4) { missing.push('T+1 (insuffisant)'); } if (!csvData.lPlus1 || csvData.lPlus1.split(',').length < 4) { missing.push('L+1 (insuffisant)'); } return missing; } function assessGenerationQuality(generatedContent) { let errorCount = 0; let totalCount = Object.keys(generatedContent).length; Object.values(generatedContent).forEach(content => { if (content && ( content.includes('[ERREUR') || content.includes('ERROR') || content.length < 10 )) { errorCount++; } }); return { errorRate: totalCount > 0 ? errorCount / totalCount : 0, totalGenerated: totalCount, errorsFound: errorCount }; } // 🔄 NODE.JS : Email avec nodemailer (remplace MailApp) async function sendErrorReport(report) { try { logSh('📧 Envoi rapport d\'erreur par email...', 'INFO'); // Using logSh instead of console.log // Lazy load nodemailer seulement quand nécessaire if (!nodemailer) { nodemailer = require('nodemailer'); } // Configuration nodemailer (Gmail par exemple) const transporter = nodemailer.createTransport({ service: 'gmail', auth: { user: process.env.EMAIL_USER, // 'your-email@gmail.com' pass: process.env.EMAIL_APP_PASSWORD // App password Google } }); const subject = `Erreur Workflow SEO Node.js - ${report.status} - ${report.csvData.mc0}`; const htmlBody = createHTMLReport(report); const mailOptions = { from: process.env.EMAIL_USER, to: 'alexistrouve.pro@gmail.com', subject: subject, html: htmlBody, attachments: [{ filename: `error-report-${Date.now()}.json`, content: JSON.stringify(report, null, 2), contentType: 'application/json' }] }; await transporter.sendMail(mailOptions); logSh('✅ Rapport d\'erreur envoyé par email', 'INFO'); // Using logSh instead of console.log } catch (error) { logSh(`❌ Échec envoi email: ${error.message}`, 'ERROR'); // Using logSh instead of console.error } } // ============= HTML REPORT - IDENTIQUE ============= function createHTMLReport(report) { const statusColor = report.status === 'SUCCESS' ? '#28a745' : '#dc3545'; let html = `

Rapport Workflow SEO Automatisé (Node.js)

Résumé Exécutif

Statut: ${report.status}

Article: ${report.csvData.t0}

Mot-clé: ${report.csvData.mc0}

Taux de réussite: ${report.stats.successRate}%

Timestamp: ${report.timestamp}

Plateforme: Node.js Server

`; if (report.errors.length > 0) { html += `

Erreurs Critiques (${report.errors.length})

`; report.errors.forEach((error, i) => { html += `

${i + 1}. ${error.type}

Message: ${error.message}

Impact: ${error.impact}

${error.suggestion ? `

Solution: ${error.suggestion}

` : ''}
`; }); html += `
`; } if (report.warnings.length > 0) { html += `

Avertissements (${report.warnings.length})

`; report.warnings.forEach((warning, i) => { html += `

${i + 1}. ${warning.type}

${warning.message}

`; }); html += `
`; } html += `

Statistiques Détaillées

Informations Système

`; return html; } // 🔄 NODE.JS EXPORTS module.exports = { logSh, cleanLogSheet, validateWorkflowIntegrity, detectDuplicateTags, detectMissingCSVVariables, assessGenerationQuality, sendErrorReport, createHTMLReport, initWebSocketServer }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/trace.js │ └────────────────────────────────────────────────────────────────────┘ */ // lib/trace.js const { AsyncLocalStorage } = require('node:async_hooks'); const { randomUUID } = require('node:crypto'); const { logSh } = require('./ErrorReporting'); const als = new AsyncLocalStorage(); function now() { return performance.now(); } function dur(ms) { if (ms < 1e3) return `${ms.toFixed(1)}ms`; const s = ms / 1e3; return s < 60 ? `${s.toFixed(2)}s` : `${(s/60).toFixed(2)}m`; } class Span { constructor({ name, parent = null, attrs = {} }) { this.id = randomUUID(); this.name = name; this.parent = parent; this.children = []; this.attrs = attrs; this.start = now(); this.end = null; this.status = 'ok'; this.error = null; } pathNames() { const names = []; let cur = this; while (cur) { names.unshift(cur.name); cur = cur.parent; } return names.join(' > '); } finish() { this.end = now(); } duration() { return (this.end ?? now()) - this.start; } } class Tracer { constructor() { this.rootSpans = []; } current() { return als.getStore(); } async startSpan(name, attrs = {}) { const parent = this.current(); const span = new Span({ name, parent, attrs }); if (parent) parent.children.push(span); else this.rootSpans.push(span); // Formater les paramètres pour affichage const paramsStr = this.formatParams(attrs); await logSh(`▶ ${name}${paramsStr}`, 'TRACE'); return span; } async run(name, fn, attrs = {}) { const parent = this.current(); const span = await this.startSpan(name, attrs); return await als.run(span, async () => { try { const res = await fn(); span.finish(); const paramsStr = this.formatParams(span.attrs); await logSh(`✔ ${name}${paramsStr} (${dur(span.duration())})`, 'TRACE'); return res; } catch (err) { span.status = 'error'; span.error = { message: err?.message, stack: err?.stack }; span.finish(); const paramsStr = this.formatParams(span.attrs); await logSh(`✖ ${name}${paramsStr} FAILED (${dur(span.duration())})`, 'ERROR'); await logSh(`Stack trace: ${span.error.message}`, 'ERROR'); if (span.error.stack) { const stackLines = span.error.stack.split('\n').slice(1, 6); // Première 5 lignes du stack for (const line of stackLines) { await logSh(` ${line.trim()}`, 'ERROR'); } } throw err; } }); } async event(msg, extra = {}) { const span = this.current(); const data = { trace: true, evt: 'span.event', ...extra }; if (span) { data.span = span.id; data.path = span.pathNames(); data.since_ms = +( (now() - span.start).toFixed(1) ); } await logSh(`• ${msg}`, 'TRACE'); } async annotate(fields = {}) { const span = this.current(); if (span) Object.assign(span.attrs, fields); await logSh('… annotate', 'TRACE'); } formatParams(attrs = {}) { const params = Object.entries(attrs) .filter(([key, value]) => value !== undefined && value !== null) .map(([key, value]) => { // Tronquer les valeurs trop longues const strValue = String(value); const truncated = strValue.length > 50 ? strValue.substring(0, 47) + '...' : strValue; return `${key}=${truncated}`; }); return params.length > 0 ? `(${params.join(', ')})` : ''; } printSummary() { const lines = []; const draw = (node, depth = 0) => { const pad = ' '.repeat(depth); const icon = node.status === 'error' ? '✖' : '✔'; lines.push(`${pad}${icon} ${node.name} (${dur(node.duration())})`); if (Object.keys(node.attrs ?? {}).length) { lines.push(`${pad} attrs: ${JSON.stringify(node.attrs)}`); } for (const ch of node.children) draw(ch, depth + 1); if (node.status === 'error' && node.error?.message) { lines.push(`${pad} error: ${node.error.message}`); if (node.error.stack) { const stackLines = String(node.error.stack || '').split('\n').slice(1, 4).map(s => s.trim()); if (stackLines.length) { lines.push(`${pad} stack:`); stackLines.forEach(line => { if (line) lines.push(`${pad} ${line}`); }); } } } }; for (const r of this.rootSpans) draw(r, 0); const summary = lines.join('\n'); logSh(`\n—— TRACE SUMMARY ——\n${summary}\n—— END TRACE ——`, 'INFO'); return summary; } } const tracer = new Tracer(); module.exports = { Span, Tracer, tracer }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/trend-prompts/TrendManager.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // TREND MANAGER - GESTION TENDANCES PROMPTS // Responsabilité: Configuration tendances pour moduler les prompts selon contexte // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); /** * TREND MANAGER * Gère les tendances configurables pour adapter les prompts selon le contexte */ class TrendManager { constructor() { this.name = 'TrendManager'; this.currentTrend = null; this.customTrends = new Map(); // Initialiser les tendances prédéfinies this.initializePredefinedTrends(); } /** * TENDANCES PRÉDÉFINIES */ initializePredefinedTrends() { this.predefinedTrends = { // ========== TENDANCES SECTORIELLES ========== 'eco-responsable': { name: 'Eco-Responsable', description: 'Accent sur durabilité, écologie, responsabilité environnementale', config: { technical: { targetTerms: ['durable', 'écologique', 'responsable', 'recyclé', 'bio', 'naturel'], focusAreas: ['impact environnemental', 'cycle de vie', 'matériaux durables'] }, style: { targetStyle: 'conscient et responsable', tone: 'engagé mais pédagogique', values: ['durabilité', 'respect environnement', 'qualité long terme'] }, adversarial: { avoidTerms: ['jetable', 'synthétique', 'intensive'], emphasize: ['naturel', 'durable', 'responsable'] } } }, 'tech-innovation': { name: 'Tech Innovation', description: 'Focus technologie avancée, innovation, digitalisation', config: { technical: { targetTerms: ['intelligent', 'connecté', 'automatisé', 'numérique', 'innovation'], focusAreas: ['technologie avancée', 'connectivité', 'automatisation'] }, style: { targetStyle: 'moderne et dynamique', tone: 'enthousiaste et précis', values: ['innovation', 'performance', 'efficacité'] }, adversarial: { avoidTerms: ['traditionnel', 'manuel', 'basique'], emphasize: ['intelligent', 'avancé', 'innovant'] } } }, 'artisanal-premium': { name: 'Artisanal Premium', description: 'Savoir-faire artisanal, qualité premium, tradition', config: { technical: { targetTerms: ['artisanal', 'fait main', 'traditionnel', 'savoir-faire', 'premium'], focusAreas: ['qualité artisanale', 'techniques traditionnelles', 'finitions soignées'] }, style: { targetStyle: 'authentique et raffiné', tone: 'respectueux et valorisant', values: ['authenticité', 'qualité', 'tradition'] }, adversarial: { avoidTerms: ['industriel', 'masse', 'standard'], emphasize: ['unique', 'authentique', 'raffiné'] } } }, // ========== TENDANCES GÉNÉRATIONNELLES ========== 'generation-z': { name: 'Génération Z', description: 'Style moderne, inclusif, digital native', config: { technical: { targetTerms: ['tendance', 'viral', 'personnalisable', 'inclusif', 'durable'], focusAreas: ['personnalisation', 'impact social', 'durabilité'] }, style: { targetStyle: 'moderne et inclusif', tone: 'décontracté mais informatif', values: ['authenticité', 'inclusivité', 'durabilité'] }, adversarial: { avoidTerms: ['traditionnel', 'conventionnel'], emphasize: ['moderne', 'inclusif', 'authentique'] } } }, 'millenial-pro': { name: 'Millennial Pro', description: 'Efficacité, équilibre vie-travail, qualité', config: { technical: { targetTerms: ['efficace', 'pratique', 'gain de temps', 'qualité de vie'], focusAreas: ['efficacité', 'praticité', 'équilibre'] }, style: { targetStyle: 'pratique et équilibré', tone: 'professionnel mais humain', values: ['efficacité', 'équilibre', 'qualité'] }, adversarial: { avoidTerms: ['compliqué', 'chronophage'], emphasize: ['pratique', 'efficace', 'équilibré'] } } }, // ========== TENDANCES SAISONNIÈRES ========== 'automne-cocooning': { name: 'Automne Cocooning', description: 'Chaleur, confort, intérieur douillet', config: { technical: { targetTerms: ['chaleureux', 'confortable', 'douillet', 'cosy', 'réconfortant'], focusAreas: ['ambiance chaleureuse', 'confort', 'bien-être'] }, style: { targetStyle: 'chaleureux et enveloppant', tone: 'bienveillant et réconfortant', values: ['confort', 'chaleur', 'sérénité'] }, adversarial: { avoidTerms: ['froid', 'strict', 'minimaliste'], emphasize: ['chaleureux', 'confortable', 'accueillant'] } } }, 'printemps-renouveau': { name: 'Printemps Renouveau', description: 'Fraîcheur, renouveau, énergie positive', config: { technical: { targetTerms: ['frais', 'nouveau', 'énergisant', 'revitalisant', 'lumineux'], focusAreas: ['renouveau', 'fraîcheur', 'dynamisme'] }, style: { targetStyle: 'frais et dynamique', tone: 'optimiste et énergique', values: ['renouveau', 'fraîcheur', 'vitalité'] }, adversarial: { avoidTerms: ['terne', 'monotone', 'statique'], emphasize: ['frais', 'nouveau', 'dynamique'] } } } }; logSh(`✅ TrendManager: ${Object.keys(this.predefinedTrends).length} tendances prédéfinies chargées`, 'DEBUG'); } /** * SÉLECTIONNER UNE TENDANCE */ setTrend(trendId, customConfig = null) { return tracer.run('TrendManager.setTrend()', async () => { try { if (customConfig) { // Tendance personnalisée this.currentTrend = { id: trendId, name: customConfig.name || trendId, description: customConfig.description || 'Tendance personnalisée', config: customConfig.config, isCustom: true }; logSh(`🎯 Tendance personnalisée appliquée: ${trendId}`, 'INFO'); } else if (this.predefinedTrends[trendId]) { // Tendance prédéfinie this.currentTrend = { id: trendId, ...this.predefinedTrends[trendId], isCustom: false }; logSh(`🎯 Tendance appliquée: ${this.currentTrend.name}`, 'INFO'); } else if (this.customTrends.has(trendId)) { // Tendance personnalisée existante const customTrend = this.customTrends.get(trendId); this.currentTrend = { id: trendId, ...customTrend, isCustom: true }; logSh(`🎯 Tendance personnalisée appliquée: ${this.currentTrend.name}`, 'INFO'); } else { throw new Error(`Tendance inconnue: ${trendId}`); } await tracer.annotate({ trendId, trendName: this.currentTrend.name, isCustom: this.currentTrend.isCustom }); return this.currentTrend; } catch (error) { logSh(`❌ Erreur sélection tendance: ${error.message}`, 'ERROR'); throw error; } }); } /** * APPLIQUER TENDANCE À UNE CONFIGURATION DE COUCHE */ applyTrendToLayerConfig(layerType, baseConfig = {}) { if (!this.currentTrend) { return baseConfig; } const trendConfig = this.currentTrend.config[layerType]; if (!trendConfig) { return baseConfig; } // Fusionner configuration tendance avec configuration de base const enhancedConfig = { ...baseConfig, ...trendConfig, // Préserver les paramètres existants tout en ajoutant la tendance trendApplied: this.currentTrend.id, trendName: this.currentTrend.name }; logSh(`🎨 Tendance "${this.currentTrend.name}" appliquée à ${layerType}`, 'DEBUG'); return enhancedConfig; } /** * OBTENIR CONFIGURATION POUR UNE COUCHE SPÉCIFIQUE */ getLayerConfig(layerType, baseConfig = {}) { const config = this.applyTrendToLayerConfig(layerType, baseConfig); return { ...config, _trend: this.currentTrend ? { id: this.currentTrend.id, name: this.currentTrend.name, appliedTo: layerType } : null }; } /** * LISTER TOUTES LES TENDANCES DISPONIBLES */ getAvailableTrends() { const trends = Object.keys(this.predefinedTrends).map(id => ({ id, name: this.predefinedTrends[id].name, description: this.predefinedTrends[id].description, category: this.getTrendCategory(id), isCustom: false })); // Ajouter tendances personnalisées for (const [id, trend] of this.customTrends) { trends.push({ id, name: trend.name, description: trend.description, category: 'custom', isCustom: true }); } return trends; } /** * OBTENIR CATÉGORIE D'UNE TENDANCE */ getTrendCategory(trendId) { if (trendId.includes('generation')) return 'générationnelle'; if (trendId.includes('eco') || trendId.includes('tech') || trendId.includes('artisanal')) return 'sectorielle'; if (trendId.includes('automne') || trendId.includes('printemps')) return 'saisonnière'; return 'autre'; } /** * CRÉER UNE TENDANCE PERSONNALISÉE */ createCustomTrend(id, config) { this.customTrends.set(id, config); logSh(`✨ Tendance personnalisée créée: ${id}`, 'INFO'); return config; } /** * RÉINITIALISER (AUCUNE TENDANCE) */ clearTrend() { this.currentTrend = null; logSh('🔄 Aucune tendance appliquée', 'DEBUG'); } /** * OBTENIR TENDANCE ACTUELLE */ getCurrentTrend() { return this.currentTrend; } /** * OBTENIR STATUT */ getStatus() { return { activeTrend: this.currentTrend ? { id: this.currentTrend.id, name: this.currentTrend.name, description: this.currentTrend.description, isCustom: this.currentTrend.isCustom } : null, availableTrends: this.getAvailableTrends().length, customTrends: this.customTrends.size }; } } // ============= EXPORTS ============= module.exports = { TrendManager }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/pipeline/PipelineDefinition.js │ └────────────────────────────────────────────────────────────────────┘ */ /** * PipelineDefinition.js * * Schemas et validation pour les pipelines modulaires flexibles. * Permet de définir des workflows custom avec n'importe quelle combinaison de modules. */ const { logSh } = require('../ErrorReporting'); /** * Modules disponibles dans le pipeline */ const AVAILABLE_MODULES = { generation: { name: 'Generation', description: 'Génération initiale du contenu', modes: ['simple'], defaultIntensity: 1.0, parameters: {} }, selective: { name: 'Selective Enhancement', description: 'Amélioration sélective par couches', modes: [ 'lightEnhancement', 'standardEnhancement', 'fullEnhancement', 'personalityFocus', 'fluidityFocus', 'adaptive' ], defaultIntensity: 1.0, parameters: { layers: { type: 'array', description: 'Couches spécifiques à appliquer' } } }, adversarial: { name: 'Adversarial Generation', description: 'Techniques anti-détection', modes: ['none', 'light', 'standard', 'heavy', 'adaptive'], defaultIntensity: 1.0, parameters: { detector: { type: 'string', enum: ['general', 'gptZero', 'originality'], default: 'general' }, method: { type: 'string', enum: ['enhancement', 'regeneration', 'hybrid'], default: 'regeneration' } } }, human: { name: 'Human Simulation', description: 'Simulation comportement humain', modes: [ 'none', 'lightSimulation', 'standardSimulation', 'heavySimulation', 'adaptiveSimulation', 'personalityFocus', 'temporalFocus' ], defaultIntensity: 1.0, parameters: { fatigueLevel: { type: 'number', min: 0, max: 1, default: 0.5 }, errorRate: { type: 'number', min: 0, max: 1, default: 0.3 } } }, pattern: { name: 'Pattern Breaking', description: 'Cassage patterns LLM', modes: [ 'none', 'lightPatternBreaking', 'standardPatternBreaking', 'heavyPatternBreaking', 'adaptivePatternBreaking', 'syntaxFocus', 'connectorsFocus' ], defaultIntensity: 1.0, parameters: { focus: { type: 'string', enum: ['syntax', 'connectors', 'both'], default: 'both' } } } }; /** * Schema d'une étape de pipeline */ const STEP_SCHEMA = { step: { type: 'number', required: true, description: 'Numéro séquentiel de l\'étape' }, module: { type: 'string', required: true, enum: Object.keys(AVAILABLE_MODULES), description: 'Module à exécuter' }, mode: { type: 'string', required: true, description: 'Mode du module' }, intensity: { type: 'number', required: false, min: 0.1, max: 2.0, default: 1.0, description: 'Intensité d\'application' }, parameters: { type: 'object', required: false, default: {}, description: 'Paramètres spécifiques au module' }, saveCheckpoint: { type: 'boolean', required: false, default: false, description: 'Sauvegarder checkpoint après cette étape' }, enabled: { type: 'boolean', required: false, default: true, description: 'Activer/désactiver l\'étape' } }; /** * Schema complet d'un pipeline */ const PIPELINE_SCHEMA = { name: { type: 'string', required: true, minLength: 3, maxLength: 100 }, description: { type: 'string', required: false, maxLength: 500 }, pipeline: { type: 'array', required: true, minLength: 1, maxLength: 20 }, metadata: { type: 'object', required: false, properties: { author: { type: 'string' }, created: { type: 'string' }, version: { type: 'string' }, tags: { type: 'array' } } } }; /** * Classe PipelineDefinition */ class PipelineDefinition { constructor(definition = null) { this.definition = definition; } /** * Valide un pipeline complet */ static validate(pipeline) { const errors = []; // Validation schema principal if (!pipeline.name || typeof pipeline.name !== 'string' || pipeline.name.length < 3) { errors.push('Le nom du pipeline doit contenir au moins 3 caractères'); } if (!Array.isArray(pipeline.pipeline) || pipeline.pipeline.length === 0) { errors.push('Le pipeline doit contenir au moins une étape'); } if (pipeline.pipeline && pipeline.pipeline.length > 20) { errors.push('Le pipeline ne peut pas contenir plus de 20 étapes'); } // Validation des étapes if (Array.isArray(pipeline.pipeline)) { pipeline.pipeline.forEach((step, index) => { const stepErrors = PipelineDefinition.validateStep(step, index); errors.push(...stepErrors); }); // Vérifier séquence des steps const steps = pipeline.pipeline.map(s => s.step).sort((a, b) => a - b); for (let i = 0; i < steps.length; i++) { if (steps[i] !== i + 1) { errors.push(`Numérotation des étapes incorrecte: attendu ${i + 1}, trouvé ${steps[i]}`); break; } } } if (errors.length > 0) { logSh(`❌ Pipeline validation failed: ${errors.join(', ')}`, 'ERROR'); return { valid: false, errors }; } logSh(`✅ Pipeline "${pipeline.name}" validé: ${pipeline.pipeline.length} étapes`, 'DEBUG'); return { valid: true, errors: [] }; } /** * Valide une étape individuelle */ static validateStep(step, index) { const errors = []; // Step number if (typeof step.step !== 'number' || step.step < 1) { errors.push(`Étape ${index}: 'step' doit être un nombre >= 1`); } // Module if (!step.module || !AVAILABLE_MODULES[step.module]) { errors.push(`Étape ${index}: module '${step.module}' inconnu. Disponibles: ${Object.keys(AVAILABLE_MODULES).join(', ')}`); return errors; // Stop si module invalide } const moduleConfig = AVAILABLE_MODULES[step.module]; // Mode if (!step.mode) { errors.push(`Étape ${index}: 'mode' requis pour module ${step.module}`); } else if (!moduleConfig.modes.includes(step.mode)) { errors.push(`Étape ${index}: mode '${step.mode}' invalide pour ${step.module}. Disponibles: ${moduleConfig.modes.join(', ')}`); } // Intensity if (step.intensity !== undefined) { if (typeof step.intensity !== 'number' || step.intensity < 0.1 || step.intensity > 2.0) { errors.push(`Étape ${index}: intensity doit être entre 0.1 et 2.0`); } } // Parameters (validation basique) if (step.parameters && typeof step.parameters !== 'object') { errors.push(`Étape ${index}: parameters doit être un objet`); } return errors; } /** * Crée une étape de pipeline valide */ static createStep(stepNumber, module, mode, options = {}) { const moduleConfig = AVAILABLE_MODULES[module]; if (!moduleConfig) { throw new Error(`Module inconnu: ${module}`); } if (!moduleConfig.modes.includes(mode)) { throw new Error(`Mode ${mode} invalide pour module ${module}`); } return { step: stepNumber, module, mode, intensity: options.intensity ?? moduleConfig.defaultIntensity, parameters: options.parameters ?? {}, saveCheckpoint: options.saveCheckpoint ?? false, enabled: options.enabled ?? true }; } /** * Crée un pipeline vide */ static createEmpty(name, description = '') { return { name, description, pipeline: [], metadata: { author: 'system', created: new Date().toISOString(), version: '1.0', tags: [] } }; } /** * Clone un pipeline */ static clone(pipeline, newName = null) { const cloned = JSON.parse(JSON.stringify(pipeline)); if (newName) { cloned.name = newName; } cloned.metadata = { ...cloned.metadata, created: new Date().toISOString(), clonedFrom: pipeline.name }; return cloned; } /** * Estime la durée d'un pipeline */ static estimateDuration(pipeline) { // Durées moyennes par module (en secondes) const DURATIONS = { generation: 15, selective: 20, adversarial: 25, human: 15, pattern: 18 }; let totalSeconds = 0; pipeline.pipeline.forEach(step => { if (!step.enabled) return; const baseDuration = DURATIONS[step.module] || 20; const intensityFactor = step.intensity || 1.0; totalSeconds += baseDuration * intensityFactor; }); return { seconds: Math.round(totalSeconds), formatted: PipelineDefinition.formatDuration(totalSeconds) }; } /** * Formate une durée en secondes */ static formatDuration(seconds) { if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const secs = seconds % 60; return `${minutes}m ${secs}s`; } /** * Obtient les infos d'un module */ static getModuleInfo(moduleName) { return AVAILABLE_MODULES[moduleName] || null; } /** * Liste tous les modules disponibles */ static listModules() { return Object.entries(AVAILABLE_MODULES).map(([key, config]) => ({ id: key, ...config })); } /** * Génère un résumé lisible du pipeline */ static getSummary(pipeline) { const enabledSteps = pipeline.pipeline.filter(s => s.enabled !== false); const moduleCount = {}; enabledSteps.forEach(step => { moduleCount[step.module] = (moduleCount[step.module] || 0) + 1; }); const summary = Object.entries(moduleCount) .map(([module, count]) => `${module}×${count}`) .join(' → '); return { totalSteps: enabledSteps.length, summary, duration: PipelineDefinition.estimateDuration(pipeline) }; } } module.exports = { PipelineDefinition, AVAILABLE_MODULES, PIPELINE_SCHEMA, STEP_SCHEMA }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/batch/DigitalOceanTemplates.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // DIGITAL OCEAN TEMPLATES - RÉCUPÉRATION XML // Responsabilité: Récupération et cache des templates XML depuis DigitalOcean Spaces // ======================================== require('dotenv').config(); const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const fs = require('fs').promises; const path = require('path'); const axios = require('axios'); const AWS = require('aws-sdk'); /** * DIGITAL OCEAN TEMPLATES MANAGER * Gestion récupération, cache et fallback des templates XML */ class DigitalOceanTemplates { constructor() { this.cacheDir = path.join(__dirname, '../../cache/templates'); // Extraire bucket du endpoint si présent (ex: https://autocollant.fra1.digitaloceanspaces.com) let endpoint = process.env.DO_ENDPOINT || process.env.DO_SPACES_ENDPOINT || 'https://fra1.digitaloceanspaces.com'; let bucket = process.env.DO_BUCKET_NAME || process.env.DO_SPACES_BUCKET || 'autocollant'; // Si endpoint contient le bucket, le retirer if (endpoint.includes(`${bucket}.`)) { endpoint = endpoint.replace(`${bucket}.`, ''); } this.config = { endpoint: endpoint, bucket: bucket, region: process.env.DO_REGION || process.env.DO_SPACES_REGION || 'fra1', accessKey: process.env.DO_ACCESS_KEY_ID || process.env.DO_SPACES_KEY, secretKey: process.env.DO_SECRET_ACCESS_KEY || process.env.DO_SPACES_SECRET, timeout: 10000 // 10 secondes }; // Cache en mémoire this.memoryCache = new Map(); this.cacheExpiry = 5 * 60 * 1000; // 5 minutes // Templates par défaut this.defaultTemplates = { 'default.xml': this.getDefaultTemplate(), 'simple.xml': this.getSimpleTemplate(), 'advanced.xml': this.getAdvancedTemplate() }; this.initializeTemplateManager(); } /** * Initialise le gestionnaire de templates */ async initializeTemplateManager() { try { // Créer le dossier cache await fs.mkdir(this.cacheDir, { recursive: true }); // Vérifier la configuration DO this.checkConfiguration(); logSh('🌊 DigitalOceanTemplates initialisé', 'DEBUG'); } catch (error) { logSh(`❌ Erreur initialisation DigitalOceanTemplates: ${error.message}`, 'ERROR'); } } /** * Vérifie la configuration Digital Ocean */ checkConfiguration() { const hasCredentials = this.config.accessKey && this.config.secretKey; if (!hasCredentials) { logSh('⚠️ Credentials Digital Ocean manquantes, utilisation cache/fallback uniquement', 'WARNING'); } else { logSh('✅ Configuration Digital Ocean OK', 'DEBUG'); } return hasCredentials; } // ======================================== // RÉCUPÉRATION TEMPLATES // ======================================== /** * Récupère un template XML (avec cache et fallback) */ async getTemplate(filename) { return tracer.run('DigitalOceanTemplates.getTemplate', async () => { if (!filename) { throw new Error('Nom de fichier template requis'); } logSh(`📋 Récupération template: ${filename}`, 'DEBUG'); try { // 1. Vérifier le cache mémoire const memoryCached = this.getFromMemoryCache(filename); if (memoryCached) { logSh(`⚡ Template ${filename} trouvé en cache mémoire`, 'DEBUG'); return memoryCached; } // 2. Vérifier le cache fichier const fileCached = await this.getFromFileCache(filename); if (fileCached) { logSh(`💾 Template ${filename} trouvé en cache fichier`, 'DEBUG'); this.setMemoryCache(filename, fileCached); return fileCached; } // 3. Récupérer depuis Digital Ocean if (this.checkConfiguration()) { try { const template = await this.fetchFromDigitalOcean(filename); if (template) { logSh(`🌊 Template ${filename} récupéré depuis Digital Ocean`, 'INFO'); // Sauvegarder en cache await this.saveToFileCache(filename, template); this.setMemoryCache(filename, template); return template; } } catch (doError) { logSh(`⚠️ Erreur Digital Ocean pour ${filename}: ${doError.message}`, 'WARNING'); } } // 4. Fallback sur template par défaut const defaultTemplate = this.getDefaultTemplateForFile(filename); logSh(`🔄 Utilisation template par défaut pour ${filename}`, 'WARNING'); return defaultTemplate; } catch (error) { logSh(`❌ Erreur récupération template ${filename}: ${error.message}`, 'ERROR'); // Fallback ultime return this.getDefaultTemplate(); } }); } /** * Récupère depuis Digital Ocean Spaces */ async fetchFromDigitalOcean(filename) { return tracer.run('DigitalOceanTemplates.fetchFromDigitalOcean', async () => { const fileKey = `wp-content/XML/${filename}`; logSh(`🌊 Récupération DO avec authentification S3: ${fileKey}`, 'DEBUG'); try { // Configuration S3 pour Digital Ocean Spaces const s3 = new AWS.S3({ endpoint: this.config.endpoint, accessKeyId: this.config.accessKey, secretAccessKey: this.config.secretKey, region: this.config.region, s3ForcePathStyle: false, signatureVersion: 'v4' }); const params = { Bucket: this.config.bucket, Key: fileKey }; logSh(`🔑 S3 getObject: bucket=${this.config.bucket}, key=${fileKey}`, 'DEBUG'); const data = await s3.getObject(params).promise(); const template = data.Body.toString('utf-8'); logSh(`✅ Template ${filename} récupéré depuis DO (${template.length} chars)`, 'INFO'); return template; } catch (error) { logSh(`❌ Digital Ocean S3 error: ${error.message} (code: ${error.code})`, 'WARNING'); throw error; } }); } // ======================================== // GESTION CACHE // ======================================== /** * Récupère depuis le cache mémoire */ getFromMemoryCache(filename) { const cached = this.memoryCache.get(filename); if (cached && Date.now() - cached.timestamp < this.cacheExpiry) { return cached.content; } if (cached) { this.memoryCache.delete(filename); } return null; } /** * Sauvegarde en cache mémoire */ setMemoryCache(filename, content) { this.memoryCache.set(filename, { content, timestamp: Date.now() }); } /** * Récupère depuis le cache fichier */ async getFromFileCache(filename) { try { const cachePath = path.join(this.cacheDir, filename); const stats = await fs.stat(cachePath); // Cache valide pendant 1 heure const maxAge = 60 * 60 * 1000; if (Date.now() - stats.mtime.getTime() < maxAge) { const content = await fs.readFile(cachePath, 'utf8'); return content; } } catch (error) { // Fichier cache n'existe pas ou erreur } return null; } /** * Sauvegarde en cache fichier */ async saveToFileCache(filename, content) { try { const cachePath = path.join(this.cacheDir, filename); await fs.writeFile(cachePath, content, 'utf8'); logSh(`💾 Template ${filename} sauvé en cache`, 'DEBUG'); } catch (error) { logSh(`⚠️ Erreur sauvegarde cache ${filename}: ${error.message}`, 'WARNING'); } } // ======================================== // TEMPLATES PAR DÉFAUT // ======================================== /** * Retourne le template par défaut approprié */ getDefaultTemplateForFile(filename) { const lowerFilename = filename.toLowerCase(); if (lowerFilename.includes('simple')) { return this.defaultTemplates['simple.xml']; } else if (lowerFilename.includes('advanced') || lowerFilename.includes('complet')) { return this.defaultTemplates['advanced.xml']; } return this.defaultTemplates['default.xml']; } /** * Template par défaut standard */ getDefaultTemplate() { 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 qui présente {{MC0}} et donne envie de lire la suite. Ton {{personality.style}}}|

|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}}{Crée un titre H2 informatif sur {{MC+1_2}}. Style {{personality.style}}}|

|Paragraphe_2{{MC+1_2}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_2}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|

|Conclusion{{MC0}}{Conclusion engageante de 2 phrases sur {{MC0}}. Appel à l'action subtil. Ton {{personality.style}}}|
`; } /** * Template simple */ getSimpleTemplate() { return `

|Titre_H1{{T0}}{Titre principal pour {{MC0}}}|

|Introduction{{MC0}}{Introduction pour {{MC0}}}| |Contenu_Principal{{MC0}}{Contenu principal sur {{MC0}}}| |Conclusion{{MC0}}{Conclusion sur {{MC0}}}|
`; } /** * Template avancé */ getAdvancedTemplate() { 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 qui présente {{MC0}} et donne envie de lire la suite. Ton {{personality.style}}}|

|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}}{Crée un titre H2 informatif sur {{MC+1_2}}. Style {{personality.style}}}|

|Paragraphe_2{{MC+1_2}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_2}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|

|Titre_H2_3{{MC+1_3}}{Crée un titre H2 informatif sur {{MC+1_3}}. Style {{personality.style}}}|

|Paragraphe_3{{MC+1_3}}{Explique en 4-5 phrases les avantages de {{MC+1_3}} pour {{MC0}}. Ton {{personality.style}}}|

|FAQ_Titre{Titre de section FAQ accrocheur sur {{MC0}}}|

|Faq_q_1{{MC+1_1}}{Question fréquente sur {{MC+1_1}} et {{MC0}}}| |Faq_a_1{{MC+1_1}}{Réponse claire et précise. 2-3 phrases. Ton {{personality.style}}}| |Faq_q_2{{MC+1_2}}{Question pratique sur {{MC+1_2}} en lien avec {{MC0}}}| |Faq_a_2{{MC+1_2}}{Réponse détaillée et utile. 2-3 phrases explicatives. Ton {{personality.style}}}| |Faq_q_3{{MC+1_3}}{Question sur {{MC+1_3}} que se posent les clients}| |Faq_a_3{{MC+1_3}}{Réponse complète qui rassure et informe. 2-3 phrases. Ton {{personality.style}}}|
|Conclusion{{MC0}}{Conclusion engageante de 2 phrases sur {{MC0}}. Appel à l'action subtil. Ton {{personality.style}}}|
`; } // ======================================== // UTILITAIRES // ======================================== /** * Liste les templates disponibles */ async listAvailableTemplates() { const templates = []; // Templates par défaut Object.keys(this.defaultTemplates).forEach(name => { templates.push({ name, source: 'default', cached: true }); }); // Templates en cache try { const cacheFiles = await fs.readdir(this.cacheDir); cacheFiles.forEach(file => { if (file.endsWith('.xml')) { templates.push({ name: file, source: 'cache', cached: true }); } }); } catch (error) { // Dossier cache n'existe pas } return templates; } /** * Vide le cache */ async clearCache() { try { // Vider cache mémoire this.memoryCache.clear(); // Vider cache fichier const cacheFiles = await fs.readdir(this.cacheDir); for (const file of cacheFiles) { if (file.endsWith('.xml')) { await fs.unlink(path.join(this.cacheDir, file)); } } logSh('🗑️ Cache templates vidé', 'INFO'); } catch (error) { logSh(`❌ Erreur vidage cache: ${error.message}`, 'ERROR'); } } /** * Retourne les statistiques du cache */ getCacheStats() { return { memoryCache: { size: this.memoryCache.size, expiry: this.cacheExpiry }, config: { hasCredentials: this.checkConfiguration(), endpoint: this.config.endpoint, bucket: this.config.bucket, timeout: this.config.timeout }, defaultTemplates: Object.keys(this.defaultTemplates).length }; } } // ============= EXPORTS ============= module.exports = { DigitalOceanTemplates }; /* ┌────────────────────────────────────────────────────────────────────┐ │ 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'); const { DigitalOceanTemplates } = require('./batch/DigitalOceanTemplates'); // Configuration const CONFIG = { openai: { apiKey: process.env.OPENAI_API_KEY, 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, le récupérer depuis Digital Ocean if (xmlTemplateValue && xmlTemplateValue.endsWith('.xml') && xmlTemplateValue.length < 100) { logSh(`🔧 XML filename detected (${xmlTemplateValue}), fetching from Digital Ocean`, 'INFO'); xmlFileName = xmlTemplateValue; // Récupérer le template depuis Digital Ocean try { const doTemplates = new DigitalOceanTemplates(); xmlTemplate = await doTemplates.getTemplate(xmlFileName); logSh(`✅ Template ${xmlFileName} récupéré depuis Digital Ocean (${xmlTemplate?.length || 0} chars)`, 'INFO'); if (!xmlTemplate) { throw new Error('Template vide récupéré'); } } catch (error) { logSh(`⚠️ Erreur récupération ${xmlFileName} depuis DO: ${error.message}. Fallback template par défaut.`, 'WARNING'); xmlTemplate = createDefaultXMLTemplate(); } } return { rowNumber: rowNumber, slug: row[0] || '', // Colonne A t0: row[1] || '', // Colonne B mc0: row[2] || '', // Colonne C tMinus1: row[3] || '', // Colonne D lMinus1: row[4] || '', // Colonne E mcPlus1: row[5] || '', // Colonne F tPlus1: row[6] || '', // Colonne G lPlus1: row[7] || '', // Colonne H xmlTemplate: xmlTemplate, // XML template pour processing xmlFileName: xmlFileName // Nom fichier pour Digital Ocean (si applicable) }; } catch (error) { logSh(`❌ Erreur lecture Google Sheet: ${error.message}`, "ERROR"); throw error; } } /** * RÉCUPÉRER PERSONNALITÉS depuis l'onglet "Personnalites" du Google Sheet * @returns {Array} Liste des personnalités disponibles */ async function getPersonalities() { try { logSh('📊 Lecture personnalités depuis Google Sheet (onglet Personnalites)...', 'INFO'); // Configuration auth Google Sheets - FORCE utilisation fichier JSON pour éviter problème TLS const { google } = require('googleapis'); const keyFilePath = path.join(__dirname, '..', 'seo-generator-470715-85d4a971c1af.json'); const auth = new google.auth.GoogleAuth({ keyFile: keyFilePath, scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'] }); logSh('🔑 Utilisation fichier JSON pour contourner problème TLS OAuth (personnalités)', 'INFO'); const sheets = google.sheets({ version: 'v4', auth }); const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c'; // Récupérer toutes les personnalités (après la ligne d'en-tête) const response = await sheets.spreadsheets.values.get({ spreadsheetId: SHEET_ID, range: 'Personnalites!A2:O' // Colonnes A à O pour inclure les nouvelles colonnes IA }); if (!response.data.values || response.data.values.length === 0) { throw new Error('Aucune personnalité trouvée dans l\'onglet Personnalites'); } const personalities = []; // Traiter chaque ligne de personnalité response.data.values.forEach((row, index) => { if (row[0] && row[0].toString().trim() !== '') { // Si nom existe (colonne A) const personality = { nom: row[0]?.toString().trim() || '', description: row[1]?.toString().trim() || 'Expert généraliste', style: row[2]?.toString().trim() || 'professionnel', // Configuration avancée depuis colonnes Google Sheet motsClesSecteurs: parseCSVField(row[3]), vocabulairePref: parseCSVField(row[4]), connecteursPref: parseCSVField(row[5]), erreursTypiques: parseCSVField(row[6]), longueurPhrases: row[7]?.toString().trim() || 'moyennes', niveauTechnique: row[8]?.toString().trim() || 'moyen', ctaStyle: parseCSVField(row[9]), defautsSimules: parseCSVField(row[10]), // NOUVEAU: Configuration IA par étape depuis Google Sheets (colonnes L-O) aiEtape1Base: row[11]?.toString().trim().toLowerCase() || '', aiEtape2Technique: row[12]?.toString().trim().toLowerCase() || '', aiEtape3Transitions: row[13]?.toString().trim().toLowerCase() || '', aiEtape4Style: row[14]?.toString().trim().toLowerCase() || '', // Backward compatibility motsCles: parseCSVField(row[3] || '') // Utilise motsClesSecteurs }; personalities.push(personality); logSh(`✓ Personnalité chargée: ${personality.nom} (${personality.style})`, 'DEBUG'); } }); logSh(`📊 ${personalities.length} personnalités chargées depuis Google Sheet`, "INFO"); return personalities; } catch (error) { logSh(`❌ ÉCHEC: Impossible de récupérer les personnalités Google Sheets - ${error.message}`, "ERROR"); throw new Error(`FATAL: Personnalités Google Sheets inaccessibles - arrêt du workflow: ${error.message}`); } } /** * PARSER CHAMP CSV - Helper function * @param {string} field - Champ à parser * @returns {Array} Liste des éléments parsés */ function parseCSVField(field) { if (!field || field.toString().trim() === '') return []; return field.toString() .split(',') .map(item => item.trim()) .filter(item => item.length > 0); } /** * Sélectionner un sous-ensemble aléatoire de personnalités * @param {Array} allPersonalities - Liste complète des personnalités * @param {number} percentage - Pourcentage à garder (0.6 = 60%) * @returns {Array} Sous-ensemble aléatoire */ function selectRandomPersonalities(allPersonalities, percentage = 0.6) { const count = Math.ceil(allPersonalities.length * percentage); // Mélanger avec Fisher-Yates shuffle (meilleur que sort()) const shuffled = [...allPersonalities]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled.slice(0, count); } /** * NOUVELLE FONCTION: Sélection de 4 personnalités complémentaires pour le pipeline multi-AI * @param {string} mc0 - Mot-clé principal * @param {string} t0 - Titre principal * @param {Array} personalities - Liste des personnalités * @returns {Array} 4 personnalités sélectionnées pour chaque étape */ async function selectMultiplePersonalitiesWithAI(mc0, t0, personalities) { try { logSh(`🎭 Sélection MULTI-personnalités IA pour: ${mc0}`, "INFO"); // Sélection aléatoire de 80% des personnalités (plus large pour 4 choix) const randomPersonalities = selectRandomPersonalities(personalities, 0.8); const totalCount = personalities.length; const selectedCount = randomPersonalities.length; logSh(`🎲 Pool aléatoire: ${selectedCount}/${totalCount} personnalités disponibles`, "DEBUG"); logSh(`📋 Personnalités dans le pool: ${randomPersonalities.map(p => p.nom).join(', ')}`, "DEBUG"); const prompt = `Choisis 4 personnalités COMPLÉMENTAIRES pour générer du contenu sur "${mc0}": OBJECTIF: Créer une équipe de 4 rédacteurs avec styles différents mais cohérents PERSONNALITÉS DISPONIBLES: ${randomPersonalities.map(p => `- ${p.nom}: ${p.description} (Style: ${p.style})`).join('\n')} RÔLES À ATTRIBUER: 1. GÉNÉRATEUR BASE: Personnalité technique/experte pour la génération initiale 2. ENHANCER TECHNIQUE: Personnalité commerciale/précise pour améliorer les termes techniques 3. FLUIDITÉ: Personnalité créative/littéraire pour améliorer les transitions 4. STYLE FINAL: Personnalité terrain/accessible pour le style final CRITÈRES: - 4 personnalités aux styles DIFFÉRENTS mais complémentaires - Adapté au secteur: ${mc0} - Variabilité maximale pour anti-détection - Éviter les doublons de style FORMAT DE RÉPONSE (EXACTEMENT 4 noms séparés par des virgules): Nom1, Nom2, Nom3, Nom4`; const requestData = { model: "gpt-4o-mini", messages: [{"role": "user", "content": prompt}], max_tokens: 100, temperature: 1.0 }; const response = await axios.post(CONFIG.openai.endpoint, requestData, { headers: { 'Authorization': `Bearer ${CONFIG.openai.apiKey}`, 'Content-Type': 'application/json' }, timeout: 300000 }); const selectedNames = response.data.choices[0].message.content.trim() .split(',') .map(name => name.trim()); logSh(`🔍 Noms retournés par IA: ${selectedNames.join(', ')}`, "DEBUG"); // Mapper aux vraies personnalités const selectedPersonalities = []; selectedNames.forEach(name => { const personality = randomPersonalities.find(p => p.nom === name); if (personality) { selectedPersonalities.push(personality); } }); // Compléter si pas assez de personnalités trouvées (sécurité) while (selectedPersonalities.length < 4 && randomPersonalities.length > selectedPersonalities.length) { const remaining = randomPersonalities.filter(p => !selectedPersonalities.some(selected => selected.nom === p.nom) ); if (remaining.length > 0) { const randomIndex = Math.floor(Math.random() * remaining.length); selectedPersonalities.push(remaining[randomIndex]); } else { break; } } // Garantir exactement 4 personnalités const final4Personalities = selectedPersonalities.slice(0, 4); logSh(`✅ Équipe de 4 personnalités sélectionnée:`, "INFO"); final4Personalities.forEach((p, index) => { const roles = ['BASE', 'TECHNIQUE', 'FLUIDITÉ', 'STYLE']; logSh(` ${index + 1}. ${roles[index]}: ${p.nom} (${p.style})`, "INFO"); }); return final4Personalities; } catch (error) { logSh(`❌ FATAL: Sélection multi-personnalités échouée: ${error.message}`, "ERROR"); throw new Error(`FATAL: Sélection multi-personnalités IA impossible - arrêt du workflow: ${error.message}`); } } /** * FONCTION LEGACY: Sélection personnalité unique (maintenue pour compatibilité) * @param {string} mc0 - Mot-clé principal * @param {string} t0 - Titre principal * @param {Array} personalities - Liste des personnalités * @returns {object} Personnalité sélectionnée */ async function selectPersonalityWithAI(mc0, t0, personalities) { try { logSh(`🤖 Sélection personnalité IA UNIQUE pour: ${mc0}`, "DEBUG"); // Appeler la fonction multi et prendre seulement la première const multiPersonalities = await selectMultiplePersonalitiesWithAI(mc0, t0, personalities); const selectedPersonality = multiPersonalities[0]; logSh(`✅ Personnalité IA sélectionnée (mode legacy): ${selectedPersonality.nom}`, "INFO"); return selectedPersonality; } catch (error) { logSh(`❌ FATAL: Sélection personnalité par IA échouée: ${error.message}`, "ERROR"); throw new Error(`FATAL: Sélection personnalité IA inaccessible - arrêt du workflow: ${error.message}`); } } /** * CRÉER TEMPLATE XML PAR DÉFAUT quand colonne I contient un nom de fichier * Utilise les données CSV disponibles pour créer un template robuste */ function createDefaultXMLTemplate() { return `

|Titre_Principal{{T0}}{Rédige un titre H1 accrocheur de maximum 10 mots pour {{MC0}}. Style {{personality.style}}}|

|Introduction{{MC0}}{Rédige une introduction engageante de 2-3 phrases sur {{MC0}}. Ton {{personality.style}}, utilise {{personality.vocabulairePref}}}|

|Titre_H2_1{{MC+1_1}}{Crée un titre H2 informatif sur {{MC+1_1}}. Style {{personality.style}}}|

|Paragraphe_1{{MC+1_1}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_1}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|

|Titre_H2_2{{MC+1_2}}{Titre H2 pour {{MC+1_2}}. Mets en valeur les points forts. Ton {{personality.style}}}|

|Paragraphe_2{{MC+1_2}}{Paragraphe de 4-5 phrases sur {{MC+1_2}}. Détaille pourquoi c'est important pour {{MC0}}. Ton {{personality.style}}}|

|Titre_H2_3{{MC+1_3}}{Titre H2 sur les bénéfices de {{MC+1_3}}. Accrocheur et informatif}|

|Paragraphe_3{{MC+1_3}}{Explique en 4-5 phrases les avantages de {{MC+1_3}} pour {{MC0}}. Ton {{personality.style}}}|

`; } /** * CRÉER FICHIERS DE DONNÉES D'EXEMPLE * Fonction utilitaire pour initialiser les fichiers JSON */ async function createSampleDataFiles() { try { // Créer répertoire data s'il n'existe pas await fs.mkdir('./data', { recursive: true }); // Exemple instructions.json const sampleInstructions = [ { slug: "plaque-test", t0: "Plaque test signalétique", mc0: "plaque signalétique", "t-1": "Signalétique", "l-1": "/signaletique/", "mc+1": "plaque dibond, plaque aluminium, plaque PVC", "t+1": "Plaque dibond, Plaque alu, Plaque PVC", "l+1": "/plaque-dibond/, /plaque-aluminium/, /plaque-pvc/", xmlFileName: "template-plaque.xml" } ]; // Exemple personalities.json const samplePersonalities = [ { nom: "Marc", description: "Expert technique en signalétique", style: "professionnel et précis", motsClesSecteurs: "technique,dibond,aluminium,impression", vocabulairePref: "précision,qualité,expertise,performance", connecteursPref: "par ailleurs,en effet,notamment,cependant", erreursTypiques: "accord_proximite,repetition_legere", longueurPhrases: "moyennes", niveauTechnique: "élevé", ctaStyle: "découvrir,choisir,commander", defautsSimules: "fatigue_cognitive,hesitation_technique" }, { nom: "Sophie", description: "Passionnée de décoration et design", style: "familier et chaleureux", motsClesSecteurs: "décoration,design,esthétique,tendances", vocabulairePref: "joli,magnifique,tendance,style", connecteursPref: "du coup,en fait,sinon,au fait", erreursTypiques: "familiarite_excessive,expression_populaire", longueurPhrases: "courtes", niveauTechnique: "moyen", ctaStyle: "craquer,adopter,foncer", defautsSimules: "enthousiasme_variable,anecdote_personnelle" } ]; // Écrire les fichiers await fs.writeFile('./data/instructions.json', JSON.stringify(sampleInstructions, null, 2)); await fs.writeFile('./data/personalities.json', JSON.stringify(samplePersonalities, null, 2)); logSh('✅ Fichiers de données d\'exemple créés dans ./data/', "INFO"); } catch (error) { logSh(`❌ Erreur création fichiers exemple: ${error.message}`, "ERROR"); } } // ============= EXPORTS NODE.JS ============= module.exports = { getBrainConfig, getPersonalities, selectPersonalityWithAI, selectMultiplePersonalitiesWithAI, // NOUVEAU: Export de la fonction multi-personnalités selectRandomPersonalities, parseCSVField, readInstructionsData, createSampleDataFiles, createDefaultXMLTemplate, CONFIG }; // ============= TEST RAPIDE SI LANCÉ DIRECTEMENT ============= if (require.main === module) { (async () => { try { logSh('🧪 Test BrainConfig Node.js...', "INFO"); // Créer fichiers exemple si nécessaire try { await fs.access('./data/instructions.json'); } catch { await createSampleDataFiles(); } // Test de la fonction principale const result = await getBrainConfig(2); if (result.success) { logSh(`✅ Test réussi: ${result.data.personality.nom} pour ${result.data.mc0}`, "INFO"); } else { logSh(`❌ Test échoué: ${result.error}`, "ERROR"); } } catch (error) { logSh(`❌ Erreur test: ${error.message}`, "ERROR"); } })(); } /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/LLMManager.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: LLMManager.js // Description: Hub central pour tous les appels LLM (Version Node.js) // Support: Claude, OpenAI, Gemini, Deepseek, Moonshot, Mistral // ======================================== const fetch = globalThis.fetch.bind(globalThis); const { logSh } = require('./ErrorReporting'); // Charger les variables d'environnement require('dotenv').config(); // ============= CONFIGURATION CENTRALISÉE ============= const LLM_CONFIG = { openai: { apiKey: process.env.OPENAI_API_KEY, endpoint: 'https://api.openai.com/v1/chat/completions', model: 'gpt-4o-mini', headers: { 'Authorization': 'Bearer {API_KEY}', 'Content-Type': 'application/json' }, temperature: 0.7, timeout: 300000, // 5 minutes retries: 3 }, claude: { apiKey: process.env.ANTHROPIC_API_KEY, endpoint: 'https://api.anthropic.com/v1/messages', model: 'claude-sonnet-4-20250514', headers: { 'x-api-key': '{API_KEY}', 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01' }, temperature: 0.7, maxTokens: 6000, timeout: 300000, // 5 minutes retries: 6 }, deepseek: { apiKey: process.env.DEEPSEEK_API_KEY, endpoint: 'https://api.deepseek.com/v1/chat/completions', model: 'deepseek-chat', headers: { 'Authorization': 'Bearer {API_KEY}', 'Content-Type': 'application/json' }, temperature: 0.7, timeout: 300000, // 5 minutes retries: 3 }, moonshot: { apiKey: process.env.MOONSHOT_API_KEY, endpoint: 'https://api.moonshot.ai/v1/chat/completions', model: 'moonshot-v1-32k', headers: { 'Authorization': 'Bearer {API_KEY}', 'Content-Type': 'application/json' }, temperature: 0.7, timeout: 300000, // 5 minutes retries: 3 }, mistral: { apiKey: process.env.MISTRAL_API_KEY, endpoint: 'https://api.mistral.ai/v1/chat/completions', model: 'mistral-small-latest', headers: { 'Authorization': 'Bearer {API_KEY}', 'Content-Type': 'application/json' }, max_tokens: 5000, temperature: 0.7, timeout: 300000, // 5 minutes retries: 3 } }; // Alias pour compatibilité avec le code existant LLM_CONFIG.gpt4 = LLM_CONFIG.openai; // ============= HELPER FUNCTIONS ============= const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // ============= INTERFACE UNIVERSELLE ============= /** * Fonction principale pour appeler n'importe quel LLM * @param {string} llmProvider - claude|openai|deepseek|moonshot|mistral * @param {string} prompt - Le prompt à envoyer * @param {object} options - Options personnalisées (température, tokens, etc.) * @param {object} personality - Personnalité pour contexte système * @returns {Promise} - Réponse générée */ async function callLLM(llmProvider, prompt, options = {}, personality = null) { const startTime = Date.now(); try { // Vérifier si le provider existe if (!LLM_CONFIG[llmProvider]) { throw new Error(`Provider LLM inconnu: ${llmProvider}`); } // Vérifier si l'API key est configurée const config = LLM_CONFIG[llmProvider]; if (!config.apiKey || config.apiKey.startsWith('VOTRE_CLE_')) { throw new Error(`Clé API manquante pour ${llmProvider}`); } logSh(`🤖 Appel LLM: ${llmProvider.toUpperCase()} (${config.model}) | Personnalité: ${personality?.nom || 'aucune'}`, 'DEBUG'); // 📢 AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA logSh(`\n🔍 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT'); logSh(prompt, 'PROMPT'); // 📤 LOG LLM REQUEST COMPLET logSh(`📤 LLM REQUEST [${llmProvider.toUpperCase()}] (${config.model}) | Personnalité: ${personality?.nom || 'AUCUNE'}`, 'LLM'); logSh(prompt, 'LLM'); // Préparer la requête selon le provider const requestData = buildRequestData(llmProvider, prompt, options, personality); // Effectuer l'appel avec retry logic const response = await callWithRetry(llmProvider, requestData, config); // Parser la réponse selon le format du provider const content = parseResponse(llmProvider, response); // 📥 LOG LLM RESPONSE COMPLET logSh(`📥 LLM RESPONSE [${llmProvider.toUpperCase()}] (${config.model}) | Durée: ${Date.now() - startTime}ms`, 'LLM'); logSh(content, 'LLM'); const duration = Date.now() - startTime; logSh(`✅ ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms`, 'INFO'); // Enregistrer les stats d'usage await recordUsageStats(llmProvider, prompt.length, content.length, duration); return content; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ Erreur ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}): ${error.toString()}`, 'ERROR'); // Enregistrer l'échec await recordUsageStats(llmProvider, prompt.length, 0, duration, error.toString()); throw error; } } // ============= CONSTRUCTION DES REQUÊTES ============= function buildRequestData(provider, prompt, options, personality) { const config = LLM_CONFIG[provider]; const temperature = options.temperature || config.temperature; const maxTokens = options.maxTokens || config.maxTokens; // Construire le système prompt si personnalité fournie const systemPrompt = personality ? `Tu es ${personality.nom}. ${personality.description}. Style: ${personality.style}` : 'Tu es un assistant expert.'; switch (provider) { case 'openai': case 'gpt4': case 'deepseek': case 'moonshot': case 'mistral': return { model: config.model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ], max_tokens: maxTokens, temperature: temperature, stream: false }; case 'claude': return { model: config.model, max_tokens: maxTokens, temperature: temperature, system: systemPrompt, messages: [ { role: 'user', content: prompt } ] }; 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 standard let url = config.endpoint; const options = { method: 'POST', headers: headers, body: JSON.stringify(requestData), timeout: config.timeout }; const response = await fetch(url, options); const responseText = await response.text(); if (response.ok) { return JSON.parse(responseText); } else if (response.status === 429) { // Rate limiting - attendre plus longtemps const waitTime = Math.pow(2, attempt) * 1000; // Exponential backoff logSh(`⏳ Rate limit ${provider.toUpperCase()}, attente ${waitTime}ms`, 'WARNING'); await sleep(waitTime); continue; } else { throw new Error(`HTTP ${response.status}: ${responseText}`); } } catch (error) { lastError = error; if (attempt < config.retries) { const waitTime = 1000 * attempt; logSh(`⚠ Erreur tentative ${attempt}: ${error.toString()}, retry dans ${waitTime}ms`, 'WARNING'); await sleep(waitTime); } } } throw new Error(`Échec après ${config.retries} tentatives: ${lastError.toString()}`); } // ============= PARSING DES RÉPONSES ============= function parseResponse(provider, responseData) { try { switch (provider) { case 'openai': case 'gpt4': case 'deepseek': case 'moonshot': case 'mistral': return responseData.choices[0].message.content.trim(); case 'claude': return responseData.content[0].text.trim(); 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 + '/5)', '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/selective-enhancement/SelectiveUtils.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // SELECTIVE UTILS - UTILITAIRES MODULAIRES // Responsabilité: Fonctions utilitaires partagées par tous les modules selective // Architecture: Helper functions réutilisables et composables // ======================================== const { logSh } = require('../ErrorReporting'); /** * ANALYSEURS DE CONTENU SELECTIVE */ /** * Analyser qualité technique d'un contenu */ function analyzeTechnicalQuality(content, contextualTerms = []) { if (!content || typeof content !== 'string') return { score: 0, details: {} }; const analysis = { score: 0, details: { technicalTermsFound: 0, technicalTermsExpected: contextualTerms.length, genericWordsCount: 0, hasSpecifications: false, hasDimensions: false, contextIntegration: 0 } }; const lowerContent = content.toLowerCase(); // 1. Compter termes techniques présents contextualTerms.forEach(term => { if (lowerContent.includes(term.toLowerCase())) { analysis.details.technicalTermsFound++; } }); // 2. Détecter mots génériques const genericWords = ['produit', 'solution', 'service', 'offre', 'article', 'élément']; analysis.details.genericWordsCount = genericWords.filter(word => lowerContent.includes(word) ).length; // 3. Vérifier spécifications techniques analysis.details.hasSpecifications = /\b(norme|iso|din|ce)\b/i.test(content); // 4. Vérifier dimensions/données techniques analysis.details.hasDimensions = /\d+\s*(mm|cm|m|%|°|kg|g)\b/i.test(content); // 5. Calculer score global (0-100) const termRatio = contextualTerms.length > 0 ? (analysis.details.technicalTermsFound / contextualTerms.length) * 40 : 20; const genericPenalty = Math.min(20, analysis.details.genericWordsCount * 5); const specificationBonus = analysis.details.hasSpecifications ? 15 : 0; const dimensionBonus = analysis.details.hasDimensions ? 15 : 0; const lengthBonus = content.length > 100 ? 10 : 0; analysis.score = Math.max(0, Math.min(100, termRatio + specificationBonus + dimensionBonus + lengthBonus - genericPenalty )); return analysis; } /** * Analyser fluidité des transitions */ function analyzeTransitionFluidity(content) { if (!content || typeof content !== 'string') return { score: 0, details: {} }; const sentences = content.split(/[.!?]+/) .map(s => s.trim()) .filter(s => s.length > 5); if (sentences.length < 2) { return { score: 100, details: { reason: 'Contenu trop court pour analyse transitions' } }; } const analysis = { score: 0, details: { sentencesCount: sentences.length, connectorsFound: 0, repetitiveConnectors: 0, abruptTransitions: 0, averageSentenceLength: 0, lengthVariation: 0 } }; // 1. Analyser connecteurs const commonConnectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite']; const connectorCounts = {}; commonConnectors.forEach(connector => { const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []); connectorCounts[connector] = matches.length; analysis.details.connectorsFound += matches.length; if (matches.length > 1) analysis.details.repetitiveConnectors++; }); // 2. Détecter transitions abruptes for (let i = 1; i < sentences.length; i++) { const sentence = sentences[i].toLowerCase().trim(); const hasConnector = commonConnectors.some(connector => sentence.startsWith(connector) || sentence.includes(` ${connector} `) ); if (!hasConnector && sentence.length > 20) { analysis.details.abruptTransitions++; } } // 3. Analyser variation de longueur const lengths = sentences.map(s => s.split(/\s+/).length); analysis.details.averageSentenceLength = lengths.reduce((a, b) => a + b, 0) / lengths.length; const variance = lengths.reduce((acc, len) => acc + Math.pow(len - analysis.details.averageSentenceLength, 2), 0 ) / lengths.length; analysis.details.lengthVariation = Math.sqrt(variance); // 4. Calculer score fluidité (0-100) const connectorScore = Math.min(30, (analysis.details.connectorsFound / sentences.length) * 100); const repetitionPenalty = Math.min(20, analysis.details.repetitiveConnectors * 5); const abruptPenalty = Math.min(30, (analysis.details.abruptTransitions / sentences.length) * 50); const variationScore = Math.min(20, analysis.details.lengthVariation * 2); analysis.score = Math.max(0, Math.min(100, connectorScore + variationScore - repetitionPenalty - abruptPenalty + 50 )); return analysis; } /** * Analyser cohérence de style */ function analyzeStyleConsistency(content, expectedPersonality = null) { if (!content || typeof content !== 'string') return { score: 0, details: {} }; const analysis = { score: 0, details: { personalityAlignment: 0, toneConsistency: 0, vocabularyLevel: 'standard', formalityScore: 0, personalityWordsFound: 0 } }; // 1. Analyser alignement personnalité if (expectedPersonality && expectedPersonality.vocabulairePref) { const personalityWords = expectedPersonality.vocabulairePref.toLowerCase().split(','); const contentLower = content.toLowerCase(); personalityWords.forEach(word => { if (word.trim() && contentLower.includes(word.trim())) { analysis.details.personalityWordsFound++; } }); analysis.details.personalityAlignment = personalityWords.length > 0 ? (analysis.details.personalityWordsFound / personalityWords.length) * 100 : 0; } // 2. Analyser niveau vocabulaire const technicalWords = content.match(/\b\w{8,}\b/g) || []; const totalWords = content.split(/\s+/).length; const techRatio = technicalWords.length / totalWords; if (techRatio > 0.15) analysis.details.vocabularyLevel = 'expert'; else if (techRatio < 0.05) analysis.details.vocabularyLevel = 'accessible'; else analysis.details.vocabularyLevel = 'standard'; // 3. Analyser formalité const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois']; const casualIndicators = ['du coup', 'sympa', 'cool', 'nickel']; let formalCount = formalIndicators.filter(indicator => content.toLowerCase().includes(indicator) ).length; let casualCount = casualIndicators.filter(indicator => content.toLowerCase().includes(indicator) ).length; analysis.details.formalityScore = formalCount - casualCount; // Positif = formel, négatif = casual // 4. Calculer score cohérence (0-100) let baseScore = 50; if (expectedPersonality) { baseScore += analysis.details.personalityAlignment * 0.3; // Ajustements selon niveau technique attendu const expectedLevel = expectedPersonality.niveauTechnique || 'standard'; if (expectedLevel === analysis.details.vocabularyLevel) { baseScore += 20; } else { baseScore -= 10; } } // Bonus cohérence tonale const sentences = content.split(/[.!?]+/).filter(s => s.length > 10); if (sentences.length > 1) { baseScore += Math.min(20, analysis.details.lengthVariation || 10); } analysis.score = Math.max(0, Math.min(100, baseScore)); return analysis; } /** * COMPARATEURS ET MÉTRIQUES */ /** * Comparer deux contenus et calculer taux amélioration */ function compareContentImprovement(original, enhanced, analysisType = 'general') { if (!original || !enhanced) return { improvementRate: 0, details: {} }; const comparison = { improvementRate: 0, details: { lengthChange: ((enhanced.length - original.length) / original.length) * 100, wordCountChange: 0, structuralChanges: 0, contentPreserved: true } }; // 1. Analyser changements structurels const originalSentences = original.split(/[.!?]+/).length; const enhancedSentences = enhanced.split(/[.!?]+/).length; comparison.details.structuralChanges = Math.abs(enhancedSentences - originalSentences); // 2. Analyser changements de mots const originalWords = original.toLowerCase().split(/\s+/).filter(w => w.length > 2); const enhancedWords = enhanced.toLowerCase().split(/\s+/).filter(w => w.length > 2); comparison.details.wordCountChange = enhancedWords.length - originalWords.length; // 3. Vérifier préservation du contenu principal const originalKeyWords = originalWords.filter(w => w.length > 4); const preservedWords = originalKeyWords.filter(w => enhanced.toLowerCase().includes(w)); comparison.details.contentPreserved = (preservedWords.length / originalKeyWords.length) > 0.7; // 4. Calculer taux amélioration selon type d'analyse switch (analysisType) { case 'technical': const originalTech = analyzeTechnicalQuality(original); const enhancedTech = analyzeTechnicalQuality(enhanced); comparison.improvementRate = enhancedTech.score - originalTech.score; break; case 'transitions': const originalFluid = analyzeTransitionFluidity(original); const enhancedFluid = analyzeTransitionFluidity(enhanced); comparison.improvementRate = enhancedFluid.score - originalFluid.score; break; case 'style': const originalStyle = analyzeStyleConsistency(original); const enhancedStyle = analyzeStyleConsistency(enhanced); comparison.improvementRate = enhancedStyle.score - originalStyle.score; break; default: // Amélioration générale (moyenne pondérée) comparison.improvementRate = Math.min(50, Math.abs(comparison.details.lengthChange) * 0.1 + (comparison.details.contentPreserved ? 20 : -20) + Math.min(15, Math.abs(comparison.details.wordCountChange))); } return comparison; } /** * UTILITAIRES DE CONTENU */ /** * Nettoyer contenu généré par LLM */ function cleanGeneratedContent(content, cleaningLevel = 'standard') { if (!content || typeof content !== 'string') return content; let cleaned = content.trim(); // Nettoyage de base cleaned = cleaned.replace(/^(voici\s+)?le\s+contenu\s+(amélioré|modifié|réécrit)[:\s]*/gi, ''); cleaned = cleaned.replace(/^(bon,?\s*)?(alors,?\s*)?(voici\s+)?/gi, ''); cleaned = cleaned.replace(/^(avec\s+les?\s+)?améliorations?\s*[:\s]*/gi, ''); // Nettoyage formatage cleaned = cleaned.replace(/\*\*([^*]+)\*\*/g, '$1'); // Gras markdown → texte normal cleaned = cleaned.replace(/\s{2,}/g, ' '); // Espaces multiples cleaned = cleaned.replace(/([.!?])\s*([.!?])/g, '$1 '); // Double ponctuation if (cleaningLevel === 'intensive') { // Nettoyage intensif cleaned = cleaned.replace(/^\s*[-*+]\s*/gm, ''); // Puces en début de ligne cleaned = cleaned.replace(/^(pour\s+)?(ce\s+)?(contenu\s*)?[,:]?\s*/gi, ''); cleaned = cleaned.replace(/\([^)]*\)/g, ''); // Parenthèses et contenu } // Nettoyage final cleaned = cleaned.replace(/^[,.\s]+/, ''); // Début cleaned = cleaned.replace(/[,\s]+$/, ''); // Fin cleaned = cleaned.trim(); return cleaned; } /** * Valider contenu selective */ function validateSelectiveContent(content, originalContent, criteria = {}) { const validation = { isValid: true, score: 0, issues: [], suggestions: [] }; const { minLength = 20, maxLengthChange = 50, // % de changement maximum preserveContent = true, checkTechnicalTerms = true } = criteria; // 1. Vérifier longueur if (!content || content.length < minLength) { validation.isValid = false; validation.issues.push('Contenu trop court'); validation.suggestions.push('Augmenter la longueur du contenu généré'); } else { validation.score += 25; } // 2. Vérifier changements de longueur if (originalContent) { const lengthChange = Math.abs((content.length - originalContent.length) / originalContent.length) * 100; if (lengthChange > maxLengthChange) { validation.issues.push('Changement de longueur excessif'); validation.suggestions.push('Réduire l\'intensité d\'amélioration'); } else { validation.score += 25; } // 3. Vérifier préservation du contenu if (preserveContent) { const preservation = compareContentImprovement(originalContent, content); if (!preservation.details.contentPreserved) { validation.isValid = false; validation.issues.push('Contenu original non préservé'); validation.suggestions.push('Améliorer conservation du sens original'); } else { validation.score += 25; } } } // 4. Vérifications spécifiques if (checkTechnicalTerms) { const technicalQuality = analyzeTechnicalQuality(content); if (technicalQuality.score > 60) { validation.score += 25; } else if (technicalQuality.score < 30) { validation.issues.push('Qualité technique insuffisante'); validation.suggestions.push('Ajouter plus de termes techniques spécialisés'); } } // Score final et validation validation.score = Math.min(100, validation.score); validation.isValid = validation.isValid && validation.score >= 60; return validation; } /** * UTILITAIRES TECHNIQUES */ /** * Chunk array avec gestion intelligente */ function chunkArray(array, chunkSize, smartChunking = false) { if (!Array.isArray(array)) return []; if (array.length <= chunkSize) return [array]; const chunks = []; if (smartChunking) { // Chunking intelligent : éviter de séparer éléments liés let currentChunk = []; for (let i = 0; i < array.length; i++) { currentChunk.push(array[i]); // Conditions de fin de chunk intelligente const isChunkFull = currentChunk.length >= chunkSize; const isLastElement = i === array.length - 1; const nextElementRelated = i < array.length - 1 && array[i].tag && array[i + 1].tag && array[i].tag.includes('FAQ') && array[i + 1].tag.includes('FAQ'); if ((isChunkFull && !nextElementRelated) || isLastElement) { chunks.push([...currentChunk]); currentChunk = []; } } // Ajouter chunk restant si non vide if (currentChunk.length > 0) { if (chunks.length > 0 && chunks[chunks.length - 1].length + currentChunk.length <= chunkSize * 1.2) { // Merger avec dernier chunk si pas trop gros chunks[chunks.length - 1].push(...currentChunk); } else { chunks.push(currentChunk); } } } else { // Chunking standard for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)); } } return chunks; } /** * Sleep avec logging optionnel */ async function sleep(ms, logMessage = null) { if (logMessage) { logSh(`⏳ ${logMessage} (${ms}ms)`, 'DEBUG'); } return new Promise(resolve => setTimeout(resolve, ms)); } /** * Mesurer performance d'opération */ function measurePerformance(operationName, startTime = Date.now()) { const endTime = Date.now(); const duration = endTime - startTime; const performance = { operationName, startTime, endTime, duration, durationFormatted: formatDuration(duration) }; return performance; } /** * Formater durée en format lisible */ function formatDuration(ms) { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; } /** * GÉNÉRATION SIMPLE (REMPLACE CONTENTGENERATION.JS) */ /** * Génération simple Claude uniquement (compatible avec l'ancien système) */ async function generateSimple(hierarchy, csvData) { const { LLMManager } = require('../LLMManager'); logSh(`🔥 Génération simple Claude uniquement`, 'INFO'); if (!hierarchy || Object.keys(hierarchy).length === 0) { throw new Error('Hiérarchie vide ou invalide'); } const result = { content: {}, stats: { processed: 0, enhanced: 0, duration: 0, llmProvider: 'claude' } }; const startTime = Date.now(); try { // Générer chaque élément avec Claude for (const [tag, instruction] of Object.entries(hierarchy)) { try { logSh(`🎯 Génération: ${tag}`, 'DEBUG'); const prompt = `Tu es un expert en rédaction SEO. Tu dois générer du contenu professionnel et naturel. CONTEXTE: - Mot-clé principal: ${csvData.mc0} - Titre principal: ${csvData.t0} - Personnalité: ${csvData.personality?.nom} (${csvData.personality?.style}) INSTRUCTION SPÉCIFIQUE: ${instruction} CONSIGNES: - Contenu naturel et engageant - Intégration naturelle du mot-clé "${csvData.mc0}" - Style ${csvData.personality?.style || 'professionnel'} - Pas de formatage markdown - Réponse directe sans préambule RÉPONSE:`; const response = await LLMManager.callLLM('claude', prompt, { temperature: 0.9, maxTokens: 300, timeout: 30000 }); if (response && response.trim()) { result.content[tag] = cleanGeneratedContent(response.trim()); result.stats.processed++; result.stats.enhanced++; } else { logSh(`⚠️ Réponse vide pour ${tag}`, 'WARNING'); result.content[tag] = `Contenu ${tag} généré automatiquement`; } } catch (error) { logSh(`❌ Erreur génération ${tag}: ${error.message}`, 'ERROR'); result.content[tag] = `Contenu ${tag} - Erreur de génération`; } } result.stats.duration = Date.now() - startTime; logSh(`✅ Génération simple terminée: ${result.stats.enhanced}/${result.stats.processed} éléments (${result.stats.duration}ms)`, 'INFO'); return result; } catch (error) { result.stats.duration = Date.now() - startTime; logSh(`❌ Échec génération simple: ${error.message}`, 'ERROR'); throw error; } } /** * STATISTIQUES ET RAPPORTS */ /** * Générer rapport amélioration */ function generateImprovementReport(originalContent, enhancedContent, layerType = 'general') { const report = { layerType, timestamp: new Date().toISOString(), summary: { elementsProcessed: 0, elementsImproved: 0, averageImprovement: 0, totalExecutionTime: 0 }, details: { byElement: [], qualityMetrics: {}, recommendations: [] } }; // Analyser chaque élément Object.keys(originalContent).forEach(tag => { const original = originalContent[tag]; const enhanced = enhancedContent[tag]; if (original && enhanced) { report.summary.elementsProcessed++; const improvement = compareContentImprovement(original, enhanced, layerType); if (improvement.improvementRate > 0) { report.summary.elementsImproved++; } report.summary.averageImprovement += improvement.improvementRate; report.details.byElement.push({ tag, improvementRate: improvement.improvementRate, lengthChange: improvement.details.lengthChange, contentPreserved: improvement.details.contentPreserved }); } }); // Calculer moyennes if (report.summary.elementsProcessed > 0) { report.summary.averageImprovement = report.summary.averageImprovement / report.summary.elementsProcessed; } // Métriques qualité globales const fullOriginal = Object.values(originalContent).join(' '); const fullEnhanced = Object.values(enhancedContent).join(' '); report.details.qualityMetrics = { technical: analyzeTechnicalQuality(fullEnhanced), transitions: analyzeTransitionFluidity(fullEnhanced), style: analyzeStyleConsistency(fullEnhanced) }; // Recommandations if (report.summary.averageImprovement < 10) { report.details.recommendations.push('Augmenter l\'intensité d\'amélioration'); } if (report.details.byElement.some(e => !e.contentPreserved)) { report.details.recommendations.push('Améliorer préservation du contenu original'); } return report; } module.exports = { // Analyseurs analyzeTechnicalQuality, analyzeTransitionFluidity, analyzeStyleConsistency, // Comparateurs compareContentImprovement, // Utilitaires contenu cleanGeneratedContent, validateSelectiveContent, // Utilitaires techniques chunkArray, sleep, measurePerformance, formatDuration, // Génération simple (remplace ContentGeneration.js) generateSimple, // Rapports generateImprovementReport }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/selective-enhancement/TechnicalLayer.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // TECHNICAL LAYER - COUCHE TECHNIQUE MODULAIRE // Responsabilité: Amélioration technique modulaire réutilisable // LLM: GPT-4o-mini (précision technique optimale) // ======================================== const { callLLM } = require('../LLMManager'); const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { chunkArray, sleep } = require('./SelectiveUtils'); /** * COUCHE TECHNIQUE MODULAIRE */ class TechnicalLayer { constructor() { this.name = 'TechnicalEnhancement'; this.defaultLLM = 'openai'; this.priority = 1; // Haute priorité - appliqué en premier généralement } /** * MAIN METHOD - Appliquer amélioration technique */ async apply(content, config = {}) { return await tracer.run('TechnicalLayer.apply()', async () => { const { llmProvider = this.defaultLLM, intensity = 1.0, // 0.0-2.0 intensité d'amélioration analysisMode = true, // Analyser avant d'appliquer csvData = null, preserveStructure = true, targetTerms = null // Termes techniques ciblés } = config; await tracer.annotate({ technicalLayer: true, llmProvider, intensity, elementsCount: Object.keys(content).length, mc0: csvData?.mc0 }); const startTime = Date.now(); logSh(`⚙️ TECHNICAL LAYER: Amélioration technique (${llmProvider})`, 'INFO'); logSh(` 📊 ${Object.keys(content).length} éléments | Intensité: ${intensity}`, 'INFO'); try { let enhancedContent = {}; let elementsProcessed = 0; let elementsEnhanced = 0; if (analysisMode) { // 1. Analyser éléments nécessitant amélioration technique const analysis = await this.analyzeTechnicalNeeds(content, csvData, targetTerms); logSh(` 📋 Analyse: ${analysis.candidates.length}/${Object.keys(content).length} éléments candidats`, 'DEBUG'); if (analysis.candidates.length === 0) { logSh(`✅ TECHNICAL LAYER: Aucune amélioration nécessaire`, 'INFO'); return { content, stats: { processed: Object.keys(content).length, enhanced: 0, analysisSkipped: true, duration: Date.now() - startTime } }; } // 2. Améliorer les éléments sélectionnés const improvedResults = await this.enhanceTechnicalElements( analysis.candidates, csvData, { llmProvider, intensity, preserveStructure } ); // 3. Merger avec contenu original enhancedContent = { ...content }; Object.keys(improvedResults).forEach(tag => { if (improvedResults[tag] !== content[tag]) { enhancedContent[tag] = improvedResults[tag]; elementsEnhanced++; } }); elementsProcessed = analysis.candidates.length; } else { // Mode direct : améliorer tous les éléments enhancedContent = await this.enhanceAllElementsDirect( content, csvData, { llmProvider, intensity, preserveStructure } ); elementsProcessed = Object.keys(content).length; elementsEnhanced = this.countDifferences(content, enhancedContent); } const duration = Date.now() - startTime; const stats = { processed: elementsProcessed, enhanced: elementsEnhanced, total: Object.keys(content).length, enhancementRate: (elementsEnhanced / Math.max(elementsProcessed, 1)) * 100, duration, llmProvider, intensity }; logSh(`✅ TECHNICAL LAYER TERMINÉE: ${elementsEnhanced}/${elementsProcessed} améliorés (${duration}ms)`, 'INFO'); await tracer.event('Technical layer appliquée', stats); return { content: enhancedContent, stats }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ TECHNICAL LAYER ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR'); throw error; } }, { content: Object.keys(content), config }); } /** * ANALYSER BESOINS TECHNIQUES */ async analyzeTechnicalNeeds(content, csvData, targetTerms = null) { logSh(`🔍 Analyse besoins techniques`, 'DEBUG'); const analysis = { candidates: [], technicalTermsFound: [], missingTerms: [], globalScore: 0 }; // Définir termes techniques selon contexte const contextualTerms = this.getContextualTechnicalTerms(csvData?.mc0, targetTerms); // Analyser chaque élément Object.entries(content).forEach(([tag, text]) => { const elementAnalysis = this.analyzeTechnicalElement(text, contextualTerms, csvData); if (elementAnalysis.needsImprovement) { analysis.candidates.push({ tag, content: text, technicalTerms: elementAnalysis.foundTerms, missingTerms: elementAnalysis.missingTerms, score: elementAnalysis.score, improvements: elementAnalysis.improvements }); analysis.globalScore += elementAnalysis.score; } analysis.technicalTermsFound.push(...elementAnalysis.foundTerms); }); analysis.globalScore = analysis.globalScore / Math.max(Object.keys(content).length, 1); analysis.technicalTermsFound = [...new Set(analysis.technicalTermsFound)]; logSh(` 📊 Score global technique: ${analysis.globalScore.toFixed(2)}`, 'DEBUG'); return analysis; } /** * AMÉLIORER ÉLÉMENTS TECHNIQUES SÉLECTIONNÉS */ async enhanceTechnicalElements(candidates, csvData, config) { logSh(`🛠️ Amélioration ${candidates.length} éléments techniques`, 'DEBUG'); logSh(`🔍 Candidates reçus: ${JSON.stringify(candidates.map(c => c.tag))}`, 'DEBUG'); const results = {}; const chunks = chunkArray(candidates, 4); // Chunks de 4 pour GPT-4 for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; try { logSh(` 📦 Chunk technique ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); const enhancementPrompt = this.createTechnicalEnhancementPrompt(chunk, csvData, config); const response = await callLLM(config.llmProvider, enhancementPrompt, { temperature: 0.4, // Précision technique maxTokens: 3000 }, csvData?.personality); const chunkResults = this.parseTechnicalResponse(response, chunk); Object.assign(results, chunkResults); logSh(` ✅ Chunk technique ${chunkIndex + 1}: ${Object.keys(chunkResults).length} améliorés`, 'DEBUG'); // Délai entre chunks if (chunkIndex < chunks.length - 1) { await sleep(1500); } } catch (error) { logSh(` ❌ Chunk technique ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR'); // Fallback: conserver contenu original chunk.forEach(element => { results[element.tag] = element.content; }); } } return results; } /** * AMÉLIORER TOUS ÉLÉMENTS MODE DIRECT */ async enhanceAllElementsDirect(content, csvData, config) { const allElements = Object.entries(content).map(([tag, text]) => ({ tag, content: text, technicalTerms: [], improvements: ['amélioration_générale_technique'], missingTerms: [] // Ajout de la propriété manquante })); return await this.enhanceTechnicalElements(allElements, csvData, config); } // ============= HELPER METHODS ============= /** * Analyser élément technique individuel */ analyzeTechnicalElement(text, contextualTerms, csvData) { let score = 0; const foundTerms = []; const missingTerms = []; const improvements = []; // 1. Détecter termes techniques présents contextualTerms.forEach(term => { if (text.toLowerCase().includes(term.toLowerCase())) { foundTerms.push(term); } else if (text.length > 100) { // Seulement pour textes longs missingTerms.push(term); } }); // 2. Évaluer manque de précision technique if (foundTerms.length === 0 && text.length > 80) { score += 0.4; improvements.push('ajout_termes_techniques'); } // 3. Détecter vocabulaire trop générique const genericWords = ['produit', 'solution', 'service', 'offre', 'article']; const genericCount = genericWords.filter(word => text.toLowerCase().includes(word) ).length; if (genericCount > 1) { score += 0.3; improvements.push('spécialisation_vocabulaire'); } // 4. Manque de données techniques (dimensions, etc.) if (text.length > 50 && !(/\d+\s*(mm|cm|m|%|°|kg|g)/.test(text))) { score += 0.2; improvements.push('ajout_données_techniques'); } // 5. Contexte métier spécifique if (csvData?.mc0 && !text.toLowerCase().includes(csvData.mc0.toLowerCase().split(' ')[0])) { score += 0.1; improvements.push('intégration_contexte_métier'); } return { needsImprovement: score > 0.3, score, foundTerms, missingTerms: missingTerms.slice(0, 3), // Limiter à 3 termes manquants improvements }; } /** * Obtenir termes techniques contextuels */ getContextualTechnicalTerms(mc0, targetTerms) { // Termes de base signalétique const baseTerms = [ 'dibond', 'aluminium', 'PMMA', 'acrylique', 'plexiglas', 'impression', 'gravure', 'découpe', 'fraisage', 'perçage', 'adhésif', 'fixation', 'visserie', 'support' ]; // Termes spécifiques selon contexte const contextualTerms = []; if (mc0) { const mc0Lower = mc0.toLowerCase(); if (mc0Lower.includes('plaque')) { contextualTerms.push('épaisseur 3mm', 'format standard', 'finition brossée', 'anodisation'); } if (mc0Lower.includes('signalétique')) { contextualTerms.push('norme ISO', 'pictogramme', 'contraste visuel', 'lisibilité'); } if (mc0Lower.includes('personnalisée')) { contextualTerms.push('découpe forme', 'impression numérique', 'quadrichromie', 'pantone'); } } // Ajouter termes ciblés si fournis if (targetTerms && Array.isArray(targetTerms)) { contextualTerms.push(...targetTerms); } return [...baseTerms, ...contextualTerms]; } /** * Créer prompt amélioration technique */ createTechnicalEnhancementPrompt(chunk, csvData, config) { const personality = csvData?.personality; let prompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces contenus. CONTEXTE: ${csvData?.mc0 || 'Signalétique personnalisée'} - Secteur: impression/signalétique ${personality ? `PERSONNALITÉ: ${personality.nom} (${personality.style})` : ''} INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif) ÉLÉMENTS À AMÉLIORER TECHNIQUEMENT: ${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} CONTENU: "${item.content}" AMÉLIORATIONS: ${item.improvements.join(', ')} ${item.missingTerms.length > 0 ? `TERMES À INTÉGRER: ${item.missingTerms.join(', ')}` : ''}`).join('\n\n')} CONSIGNES TECHNIQUES: - GARDE exactement le même message et ton${personality ? ` ${personality.style}` : ''} - AJOUTE précision technique naturelle et vocabulaire spécialisé - INTÈGRE termes métier : matériaux, procédés, normes, dimensions - REMPLACE vocabulaire générique par termes techniques appropriés - ÉVITE jargon incompréhensible, reste accessible - PRESERVE longueur approximative (±15%) VOCABULAIRE TECHNIQUE RECOMMANDÉ: - Matériaux: dibond, aluminium anodisé, PMMA coulé, PVC expansé - Procédés: impression UV, gravure laser, découpe numérique, fraisage CNC - Finitions: brossé, poli, texturé, laqué - Fixations: perçage, adhésif double face, vis inox, plots de fixation FORMAT RÉPONSE: [1] Contenu avec amélioration technique précise [2] Contenu avec amélioration technique précise etc... IMPORTANT: Réponse DIRECTE par les contenus améliorés, pas d'explication.`; return prompt; } /** * Parser réponse technique */ parseTechnicalResponse(response, chunk) { const results = {}; const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs; let match; let index = 0; while ((match = regex.exec(response)) && index < chunk.length) { let technicalContent = match[2].trim(); const element = chunk[index]; // Nettoyer contenu technique technicalContent = this.cleanTechnicalContent(technicalContent); if (technicalContent && technicalContent.length > 10) { results[element.tag] = technicalContent; logSh(`✅ Amélioré technique [${element.tag}]: "${technicalContent.substring(0, 60)}..."`, 'DEBUG'); } else { results[element.tag] = element.content; // Fallback logSh(`⚠️ Fallback technique [${element.tag}]: amélioration invalide`, 'WARNING'); } index++; } // Compléter les manquants while (index < chunk.length) { const element = chunk[index]; results[element.tag] = element.content; index++; } return results; } /** * Nettoyer contenu technique généré */ cleanTechnicalContent(content) { if (!content) return content; // Supprimer préfixes indésirables content = content.replace(/^(voici\s+)?le\s+contenu\s+amélioré\s*[:.]?\s*/gi, ''); content = content.replace(/^(avec\s+)?amélioration\s+technique\s*[:.]?\s*/gi, ''); content = content.replace(/^(bon,?\s*)?(alors,?\s*)?pour\s+/gi, ''); // Nettoyer formatage content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown content = content.replace(/\s{2,}/g, ' '); // Espaces multiples content = content.trim(); return content; } /** * Compter différences entre contenus */ countDifferences(original, enhanced) { let count = 0; Object.keys(original).forEach(tag => { if (enhanced[tag] && enhanced[tag] !== original[tag]) { count++; } }); return count; } } module.exports = { TechnicalLayer }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/selective-enhancement/TransitionLayer.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // TRANSITION LAYER - COUCHE TRANSITIONS MODULAIRE - DISABLED // Responsabilité: Amélioration fluidité modulaire réutilisable // LLM: Gemini (DISABLED - remplacé par style) // ======================================== 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 = 'mistral'; // Changed from gemini to mistral 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'); const { TrendManager } = require('../trend-prompts/TrendManager'); /** * 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 = {}, trendId = null, // ID de tendance à appliquer trendManager = null // Instance TrendManager (optionnel) } = config; // Initialiser TrendManager si tendance spécifiée let activeTrendManager = trendManager; if (trendId && !activeTrendManager) { activeTrendManager = new TrendManager(); await activeTrendManager.setTrend(trendId); } 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 avec configuration tendance switch (layerType) { case 'technical': const technicalConfig = activeTrendManager ? activeTrendManager.getLayerConfig('technical', { ...config, llmProvider: selectedLLM }) : { ...config, llmProvider: selectedLLM }; const technicalResult = await applyTechnicalEnhancement(existingContent, technicalConfig); enhancedContent = technicalResult.content; layerStats = technicalResult.stats; break; case 'transitions': const transitionConfig = activeTrendManager ? activeTrendManager.getLayerConfig('transitions', { ...config, llmProvider: selectedLLM }) : { ...config, llmProvider: selectedLLM }; const transitionResult = await applyTransitionEnhancement(existingContent, transitionConfig); enhancedContent = transitionResult.content; layerStats = transitionResult.stats; break; case 'style': const styleConfig = activeTrendManager ? activeTrendManager.getLayerConfig('style', { ...config, llmProvider: selectedLLM }) : { ...config, llmProvider: selectedLLM }; const styleResult = await applyStyleEnhancement(existingContent, styleConfig); 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/adversarial-generation/DetectorStrategies.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // DETECTOR STRATEGIES - NIVEAU 3 // Responsabilité: Stratégies spécialisées par détecteur IA // Anti-détection: Techniques ciblées contre chaque analyseur // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); /** * STRATÉGIES DÉTECTEUR PAR DÉTECTEUR * Chaque classe implémente une approche spécialisée */ class BaseDetectorStrategy { constructor(name) { this.name = name; this.effectiveness = 0.8; this.targetMetrics = []; } /** * Générer instructions spécifiques pour ce détecteur */ generateInstructions(elementType, personality, csvData) { throw new Error('generateInstructions must be implemented by subclass'); } /** * Obtenir instructions anti-détection (NOUVEAU pour modularité) */ getInstructions(intensity = 1.0) { throw new Error('getInstructions must be implemented by subclass'); } /** * Obtenir conseils d'amélioration (NOUVEAU pour modularité) */ getEnhancementTips(intensity = 1.0) { throw new Error('getEnhancementTips must be implemented by subclass'); } /** * Analyser efficacité contre ce détecteur */ analyzeEffectiveness(content) { return { detector: this.name, effectiveness: this.effectiveness, metrics: this.analyzeContent(content) }; } /** * Analyser contenu selon métriques de ce détecteur */ analyzeContent(content) { return { wordCount: content.split(/\s+/).length, sentenceCount: content.split(/[.!?]+/).length }; } } /** * STRATÉGIE ANTI-GPTZERO * Focus: Imprévisibilité et variation syntaxique */ class GPTZeroStrategy extends BaseDetectorStrategy { constructor() { super('GPTZero'); this.effectiveness = 0.9; this.targetMetrics = ['perplexity', 'burstiness', 'unpredictability']; this.techniques = { syntaxVariation: { name: 'Variation syntaxique drastique', rules: [ 'Alterne phrases simples (5-8 mots) et complexes (25-35 mots)', 'Utilise structures atypiques : inversion, anacoluthe, ellipse', 'Intercale incises, parenthèses, tirets pour briser linéarité', 'Varie types : déclarative → interrogative → exclamative' ] }, lexicalUnpredictability: { name: 'Imprévisibilité lexicale', rules: [ 'Synonymes rares ou détournés : "performant" → "qui tient la route"', 'Néologismes justifiés : "sur-mesurable" pour "très mesurable"', 'Registres mélangés : soutenu puis familier dans même paragraphe', 'Évite collocations attendues : "haute qualité" → "qualité qui dépote"' ] }, narrativeDisruption: { name: 'Disruption narrative', rules: [ 'Apartés personnels : "(j\'ai testé, c\'est bluffant)"', 'Questions rhétoriques inattendues au milieu des faits', 'Changements de perspective soudains : technique → humain → commercial', 'Digressions courtes puis retour au sujet' ] } }; } generateInstructions(elementType, personality, csvData) { const instructions = []; instructions.push(`ANTI-GPTZERO - MAXIMUM IMPRÉVISIBILITÉ:`); // Techniques syntaxiques instructions.push(`\nSYNTAXE VARIABLE:`); this.techniques.syntaxVariation.rules.forEach(rule => { instructions.push(`• ${rule}`); }); // Imprévisibilité lexicale instructions.push(`\nLEXIQUE IMPRÉVISIBLE:`); this.techniques.lexicalUnpredictability.rules.forEach(rule => { instructions.push(`• ${rule}`); }); // Adaptation selon type élément if (elementType === 'titre_h1' || elementType === 'titre_h2') { instructions.push(`\nTITRES ANTI-GPTZERO:`); instructions.push(`• Évite formules attendues : "Guide complet" → "Le vrai topo sur"`); instructions.push(`• Structure atypique : question, exclamation, fragment`); } else if (elementType === 'texte') { instructions.push(`\nTEXTES ANTI-GPTZERO:`); this.techniques.narrativeDisruption.rules.forEach(rule => { instructions.push(`• ${rule}`); }); } // Adaptation personnalité if (personality) { instructions.push(`\nAVEC STYLE ${personality.nom.toUpperCase()}:`); instructions.push(`• Pousse ${personality.style} vers extrêmes : plus marqué, moins lisse`); instructions.push(`• Accentue tics de langage : répétitions, expressions favorites`); } return instructions.join('\n'); } /** * Instructions modulaires pour AdversarialCore */ getInstructions(intensity = 1.0) { const baseRules = [ 'Surprends avec tournures inattendues et constructions atypiques', 'Évite phrases trop prévisibles ou patterns répétitifs', 'Utilise synonymes moins évidents et vocabulaire moins commun', 'Varie drastiquement la complexité syntaxique entre phrases' ]; const intensiveRules = [ 'Intercale observations personnelles ou détours narratifs', 'Alterne registres de langue (soutenu → familier → technique)', 'Brise la logique linéaire avec des parenthèses, incises, apartés', 'Créé ruptures narratives et changements de perspective soudains' ]; return intensity >= 1.0 ? [...baseRules, ...intensiveRules] : baseRules; } /** * Conseils d'amélioration pour enhancement method */ getEnhancementTips(intensity = 1.0) { return [ 'Remplace connecteurs prévisibles par variations inattendues', 'Ajoute incises courtes pour briser la linéarité', 'Varie longueurs phrases dans même paragraphe', 'Utilise synonymes moins courants mais naturels', ...(intensity > 0.8 ? [ 'Insère questions rhétoriques ponctuelles', 'Ajoute nuances et hésitations authentiques' ] : []) ]; } analyzeContent(content) { const baseMetrics = super.analyzeContent(content); // Analyse perplexité approximative const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 5); const sentenceLengths = sentences.map(s => s.split(/\s+/).length); // Variance longueur (proxy pour burstiness) const avgLength = sentenceLengths.reduce((a, b) => a + b, 0) / sentenceLengths.length; const variance = sentenceLengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / sentenceLengths.length; const burstiness = Math.sqrt(variance) / avgLength; // Diversité lexicale (proxy pour imprévisibilité) const words = content.toLowerCase().split(/\s+/).filter(w => w.length > 2); const uniqueWords = [...new Set(words)]; const lexicalDiversity = uniqueWords.length / words.length; return { ...baseMetrics, burstiness: Math.round(burstiness * 100) / 100, lexicalDiversity: Math.round(lexicalDiversity * 100) / 100, avgSentenceLength: Math.round(avgLength), gptZeroRiskLevel: this.calculateGPTZeroRisk(burstiness, lexicalDiversity) }; } calculateGPTZeroRisk(burstiness, lexicalDiversity) { // Heuristique : GPTZero détecte uniformité faible + diversité faible const uniformityScore = Math.min(burstiness, 1) * 100; const diversityScore = lexicalDiversity * 100; const combinedScore = (uniformityScore + diversityScore) / 2; if (combinedScore > 70) return 'low'; if (combinedScore > 40) return 'medium'; return 'high'; } } /** * STRATÉGIE ANTI-ORIGINALITY * Focus: Diversité sémantique et originalité */ class OriginalityStrategy extends BaseDetectorStrategy { constructor() { super('Originality'); this.effectiveness = 0.85; this.targetMetrics = ['semantic_diversity', 'originality_score', 'vocabulary_range']; this.techniques = { semanticCreativity: { name: 'Créativité sémantique', rules: [ 'Métaphores inattendues : "cette plaque, c\'est le passeport de votre façade"', 'Comparaisons originales : évite clichés, invente analogies', 'Reformulations créatives : "résistant aux intempéries" → "qui brave les saisons"', 'Néologismes justifiés et expressifs' ] }, perspectiveShifting: { name: 'Changements de perspective', rules: [ 'Angles multiples sur même info : technique → esthétique → pratique', 'Points de vue variés : fabricant, utilisateur, installateur, voisin', 'Temporalités mélangées : présent, futur proche, retour d\'expérience', 'Niveaux d\'abstraction : détail précis puis vue d\'ensemble' ] }, linguisticInventiveness: { name: 'Inventivité linguistique', rules: [ 'Jeux de mots subtils et expressions détournées', 'Régionalismes et références culturelles précises', 'Vocabulaire technique humanisé avec créativité', 'Rythmes et sonorités travaillés : allitérations, assonances' ] } }; } generateInstructions(elementType, personality, csvData) { const instructions = []; instructions.push(`ANTI-ORIGINALITY - MAXIMUM CRÉATIVITÉ SÉMANTIQUE:`); // Créativité sémantique instructions.push(`\nCRÉATIVITÉ SÉMANTIQUE:`); this.techniques.semanticCreativity.rules.forEach(rule => { instructions.push(`• ${rule}`); }); // Changements de perspective instructions.push(`\nPERSPECTIVES MULTIPLES:`); this.techniques.perspectiveShifting.rules.forEach(rule => { instructions.push(`• ${rule}`); }); // Spécialisation par élément if (elementType === 'intro') { instructions.push(`\nINTROS ANTI-ORIGINALITY:`); instructions.push(`• Commence par angle totalement inattendu pour le sujet`); instructions.push(`• Évite intro-types, réinvente présentation du sujet`); instructions.push(`• Crée surprise puis retour naturel au cœur du sujet`); } else if (elementType.includes('faq')) { instructions.push(`\nFAQ ANTI-ORIGINALITY:`); instructions.push(`• Questions vraiment originales, pas standard secteur`); instructions.push(`• Réponses avec angles créatifs et exemples inédits`); } // Contexte métier créatif if (csvData && csvData.mc0) { instructions.push(`\nCRÉATIVITÉ CONTEXTUELLE ${csvData.mc0.toUpperCase()}:`); instructions.push(`• Réinvente façon de parler de ${csvData.mc0}`); instructions.push(`• Évite vocabulaire convenu du secteur, invente expressions`); instructions.push(`• Trouve analogies originales spécifiques à ${csvData.mc0}`); } // Inventivité linguistique instructions.push(`\nINVENTIVITÉ LINGUISTIQUE:`); this.techniques.linguisticInventiveness.rules.forEach(rule => { instructions.push(`• ${rule}`); }); return instructions.join('\n'); } /** * Instructions modulaires pour AdversarialCore */ getInstructions(intensity = 1.0) { const baseRules = [ 'Vocabulaire TRÈS varié : évite répétitions même de synonymes', 'Structures phrases délibérément irrégulières et asymétriques', 'Changements angles fréquents : technique → personnel → général', 'Créativité sémantique : métaphores, comparaisons inattendues' ]; const intensiveRules = [ 'Évite formulations académiques ou trop structurées', 'Intègre références culturelles, expressions régionales', 'Subvertis les attentes : commence par la fin, questionne l\'évidence', 'Réinvente façon de présenter informations basiques' ]; return intensity >= 1.0 ? [...baseRules, ...intensiveRules] : baseRules; } /** * Conseils d'amélioration pour enhancement method */ getEnhancementTips(intensity = 1.0) { return [ 'Trouve synonymes créatifs et expressions détournées', 'Ajoute métaphores subtiles et comparaisons originales', 'Varie angles d\'approche dans même contenu', 'Utilise vocabulaire technique humanisé', ...(intensity > 0.8 ? [ 'Insère références culturelles ou régionalismes', 'Crée néologismes justifiés et expressifs' ] : []) ]; } analyzeContent(content) { const baseMetrics = super.analyzeContent(content); // Analyse diversité sémantique const words = content.toLowerCase().split(/\s+/).filter(w => w.length > 3); const uniqueWords = [...new Set(words)]; const semanticDiversity = uniqueWords.length / words.length; // Détection créativité (heuristique) const creativityIndicators = [ 'comme', 'tel', 'sorte de', 'façon de', 'manière de', // métaphores '(', ')', '"', // originalité structure '?', '!', // variation tonale ]; const creativityCount = creativityIndicators.reduce((count, indicator) => { return count + (content.match(new RegExp(indicator.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length; }, 0); const creativityScore = Math.min(100, (creativityCount / words.length) * 1000); return { ...baseMetrics, semanticDiversity: Math.round(semanticDiversity * 100) / 100, creativityScore: Math.round(creativityScore), uniqueWordRatio: Math.round((uniqueWords.length / words.length) * 100), originalityRiskLevel: this.calculateOriginalityRisk(semanticDiversity, creativityScore) }; } calculateOriginalityRisk(semanticDiversity, creativityScore) { const diversityScore = semanticDiversity * 100; const combinedScore = (diversityScore + creativityScore) / 2; if (combinedScore > 60) return 'low'; if (combinedScore > 35) return 'medium'; return 'high'; } } /** * STRATÉGIE GÉNÉRALE * Équilibre entre toutes les techniques */ class GeneralStrategy extends BaseDetectorStrategy { constructor() { super('General'); this.effectiveness = 0.75; this.targetMetrics = ['overall_naturalness', 'human_likeness']; } generateInstructions(elementType, personality, csvData) { const instructions = []; instructions.push(`ANTI-DÉTECTION GÉNÉRALE - ÉQUILIBRE OPTIMAL:`); // Instructions générales équilibrées instructions.push(`\nNATUREL ET VARIÉ:`); instructions.push(`• Évite mots typiques IA : "optimal", "comprehensive", "seamless", "robust"`); instructions.push(`• Mélange phrases courtes (5-10 mots) et longues (15-25 mots) naturellement`); instructions.push(`• Utilise expressions idiomatiques françaises authentiques`); instructions.push(`• Intègre imperfections humaines : répétitions légères, hésitations`); instructions.push(`• Change angle ou ton 1-2 fois selon longueur du contenu`); // Adaptation personnalité équilibrée if (personality) { instructions.push(`\nSTYLE ${personality.nom} NATUREL:`); instructions.push(`• Applique style ${personality.style} de façon authentique, pas forcée`); instructions.push(`• Respecte vocabulaire naturel : ${personality.vocabulairePref || 'varié'}`); instructions.push(`• Utilise connecteurs variés : ${personality.connecteursPref || 'naturels'}`); } return instructions.join('\n'); } /** * Instructions modulaires pour AdversarialCore */ getInstructions(intensity = 1.0) { const baseRules = [ 'Évite ces mots typiques IA : "optimal", "comprehensive", "seamless", "robust", "leverage"', 'Varie longueurs phrases : mélange phrases courtes (5-10 mots) ET longues (20-30 mots)', 'Utilise expressions françaises familières et tournures idiomatiques', 'Ajoute imperfections naturelles : répétitions légères, hésitations, reformulations' ]; const intensiveRules = [ 'Change angle ou perspective 1-2 fois dans le texte', 'Préfère synonymes moins évidents : "bon" plutôt qu\'excellent", "solide" plutôt qu\'optimisé"', 'Intègre connecteurs variés et naturels selon le contexte', 'Simule variation naturelle d\'humeur et d\'énergie rédactionnelle' ]; return intensity >= 0.8 ? [...baseRules, ...intensiveRules] : baseRules; } /** * Conseils d'amélioration pour enhancement method */ getEnhancementTips(intensity = 1.0) { return [ 'Remplace mots typiques IA par synonymes plus naturels', 'Ajoute nuances et hésitations : "peut-être", "généralement", "souvent"', 'Varie connecteurs pour éviter répétitions mécaniques', 'Personnalise avec observations subjectives légères', ...(intensity > 0.7 ? [ 'Intègre "erreurs" humaines : corrections, précisions', 'Simule changement léger de ton ou d\'énergie' ] : []) ]; } analyzeContent(content) { const baseMetrics = super.analyzeContent(content); // Métrique naturalité générale const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 5); const avgWordsPerSentence = baseMetrics.wordCount / baseMetrics.sentenceCount; // Détection mots typiques IA const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage']; const aiWordCount = aiWords.reduce((count, word) => { return count + (content.toLowerCase().match(new RegExp(`\\b${word}\\b`, 'g')) || []).length; }, 0); const aiWordDensity = aiWordCount / baseMetrics.wordCount * 100; const naturalness = Math.max(0, 100 - (aiWordDensity * 10) - Math.abs(avgWordsPerSentence - 15)); return { ...baseMetrics, avgWordsPerSentence: Math.round(avgWordsPerSentence), aiWordCount, aiWordDensity: Math.round(aiWordDensity * 100) / 100, naturalnessScore: Math.round(naturalness), generalRiskLevel: naturalness > 70 ? 'low' : naturalness > 40 ? 'medium' : 'high' }; } } /** * FACTORY POUR CRÉER STRATÉGIES */ class DetectorStrategyFactory { static strategies = { 'general': GeneralStrategy, 'gptZero': GPTZeroStrategy, 'originality': OriginalityStrategy }; static createStrategy(detectorName) { const StrategyClass = this.strategies[detectorName]; if (!StrategyClass) { logSh(`⚠️ Stratégie inconnue: ${detectorName}, fallback vers général`, 'WARNING'); return new GeneralStrategy(); } return new StrategyClass(); } static getSupportedDetectors() { return Object.keys(this.strategies).map(name => { const strategy = this.createStrategy(name); return { name, displayName: strategy.name, effectiveness: strategy.effectiveness, targetMetrics: strategy.targetMetrics }; }); } static analyzeContentAgainstAllDetectors(content) { const results = {}; Object.keys(this.strategies).forEach(detectorName => { const strategy = this.createStrategy(detectorName); results[detectorName] = strategy.analyzeEffectiveness(content); }); return results; } } /** * FONCTION UTILITAIRE - SÉLECTION STRATÉGIE OPTIMALE */ function selectOptimalStrategy(elementType, personality, previousResults = {}) { // Logique de sélection intelligente // Si résultats précédents disponibles, adapter if (previousResults.gptZero && previousResults.gptZero.effectiveness < 0.6) { return 'gptZero'; // Renforcer anti-GPTZero } if (previousResults.originality && previousResults.originality.effectiveness < 0.6) { return 'originality'; // Renforcer anti-Originality } // Sélection par type d'élément if (elementType === 'titre_h1' || elementType === 'titre_h2') { return 'gptZero'; // Titres bénéficient imprévisibilité } if (elementType === 'intro' || elementType === 'texte') { return 'originality'; // Corps bénéficie créativité sémantique } if (elementType.includes('faq')) { return 'general'; // FAQ équilibre naturalité } // Par personnalité if (personality) { if (personality.style === 'créatif' || personality.style === 'original') { return 'originality'; } if (personality.style === 'technique' || personality.style === 'expert') { return 'gptZero'; } } return 'general'; // Fallback } module.exports = { DetectorStrategyFactory, GPTZeroStrategy, OriginalityStrategy, GeneralStrategy, selectOptimalStrategy, BaseDetectorStrategy }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/adversarial-generation/AdversarialCore.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // ADVERSARIAL CORE - MOTEUR MODULAIRE // Responsabilité: Moteur adversarial réutilisable sur tout contenu // Architecture: Couches applicables à la demande // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { callLLM } = require('../LLMManager'); // Import stratégies et utilitaires const { DetectorStrategyFactory, selectOptimalStrategy } = require('./DetectorStrategies'); /** * MAIN ENTRY POINT - APPLICATION COUCHE ADVERSARIALE * Input: contenu existant + configuration adversariale * Output: contenu avec couche adversariale appliquée */ async function applyAdversarialLayer(existingContent, config = {}) { return await tracer.run('AdversarialCore.applyAdversarialLayer()', async () => { const { detectorTarget = 'general', intensity = 1.0, method = 'regeneration', // 'regeneration' | 'enhancement' | 'hybrid' preserveStructure = true, csvData = null, context = {} } = config; await tracer.annotate({ adversarialLayer: true, detectorTarget, intensity, method, elementsCount: Object.keys(existingContent).length }); const startTime = Date.now(); logSh(`🎯 APPLICATION COUCHE ADVERSARIALE: ${detectorTarget} (${method})`, 'INFO'); logSh(` 📊 ${Object.keys(existingContent).length} éléments | Intensité: ${intensity}`, 'INFO'); try { // Initialiser stratégie détecteur const strategy = DetectorStrategyFactory.createStrategy(detectorTarget); // Appliquer méthode adversariale choisie let adversarialContent = {}; switch (method) { case 'regeneration': adversarialContent = await applyRegenerationMethod(existingContent, config, strategy); break; case 'enhancement': adversarialContent = await applyEnhancementMethod(existingContent, config, strategy); break; case 'hybrid': adversarialContent = await applyHybridMethod(existingContent, config, strategy); break; default: throw new Error(`Méthode adversariale inconnue: ${method}`); } const duration = Date.now() - startTime; const stats = { elementsProcessed: Object.keys(existingContent).length, elementsModified: countModifiedElements(existingContent, adversarialContent), detectorTarget, intensity, method, duration }; logSh(`✅ COUCHE ADVERSARIALE APPLIQUÉE: ${stats.elementsModified}/${stats.elementsProcessed} modifiés (${duration}ms)`, 'INFO'); await tracer.event('Couche adversariale appliquée', stats); return { content: adversarialContent, stats, original: existingContent, config }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ COUCHE ADVERSARIALE ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR'); // Fallback: retourner contenu original logSh(`🔄 Fallback: contenu original conservé`, 'WARNING'); return { content: existingContent, stats: { fallback: true, duration }, original: existingContent, config, error: error.message }; } }, { existingContent: Object.keys(existingContent), config }); } /** * MÉTHODE RÉGÉNÉRATION - Réécrire complètement avec prompts adversariaux */ async function applyRegenerationMethod(existingContent, config, strategy) { logSh(`🔄 Méthode régénération adversariale`, 'DEBUG'); const results = {}; const contentEntries = Object.entries(existingContent); // Traiter en chunks pour éviter timeouts const chunks = chunkArray(contentEntries, 4); for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; logSh(` 📦 Régénération chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); try { const regenerationPrompt = createRegenerationPrompt(chunk, config, strategy); const response = await callLLM('claude', regenerationPrompt, { temperature: 0.7 + (config.intensity * 0.2), // Température variable selon intensité maxTokens: 2000 * chunk.length }, config.csvData?.personality); const chunkResults = parseRegenerationResponse(response, chunk); Object.assign(results, chunkResults); logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} éléments régénérés`, 'DEBUG'); // Délai entre chunks if (chunkIndex < chunks.length - 1) { await sleep(1500); } } catch (error) { logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR'); // Fallback: garder contenu original pour ce chunk chunk.forEach(([tag, content]) => { results[tag] = content; }); } } return results; } /** * MÉTHODE ENHANCEMENT - Améliorer sans réécrire complètement */ async function applyEnhancementMethod(existingContent, config, strategy) { logSh(`🔧 Méthode enhancement adversarial`, 'DEBUG'); const results = { ...existingContent }; // Base: contenu original const elementsToEnhance = selectElementsForEnhancement(existingContent, config); if (elementsToEnhance.length === 0) { logSh(` ⏭️ Aucun élément nécessite enhancement`, 'DEBUG'); return results; } logSh(` 📋 ${elementsToEnhance.length} éléments sélectionnés pour enhancement`, 'DEBUG'); const enhancementPrompt = createEnhancementPrompt(elementsToEnhance, config, strategy); try { const response = await callLLM('gpt4', enhancementPrompt, { temperature: 0.5 + (config.intensity * 0.3), maxTokens: 3000 }, config.csvData?.personality); const enhancedResults = parseEnhancementResponse(response, elementsToEnhance); // Appliquer améliorations Object.keys(enhancedResults).forEach(tag => { if (enhancedResults[tag] !== existingContent[tag]) { results[tag] = enhancedResults[tag]; } }); return results; } catch (error) { logSh(`❌ Enhancement échoué: ${error.message}`, 'ERROR'); return results; // Fallback: contenu original } } /** * MÉTHODE HYBRIDE - Combinaison régénération + enhancement */ async function applyHybridMethod(existingContent, config, strategy) { logSh(`⚡ Méthode hybride adversariale`, 'DEBUG'); // 1. Enhancement léger sur tout le contenu const enhancedContent = await applyEnhancementMethod(existingContent, { ...config, intensity: config.intensity * 0.6 // Intensité réduite pour enhancement }, strategy); // 2. Régénération ciblée sur éléments clés const keyElements = selectKeyElementsForRegeneration(enhancedContent, config); if (keyElements.length === 0) { return enhancedContent; } const keyElementsContent = {}; keyElements.forEach(tag => { keyElementsContent[tag] = enhancedContent[tag]; }); const regeneratedElements = await applyRegenerationMethod(keyElementsContent, { ...config, intensity: config.intensity * 1.2 // Intensité augmentée pour régénération }, strategy); // 3. Merger résultats const hybridContent = { ...enhancedContent }; Object.keys(regeneratedElements).forEach(tag => { hybridContent[tag] = regeneratedElements[tag]; }); return hybridContent; } // ============= HELPER FUNCTIONS ============= /** * Créer prompt de régénération adversariale */ function createRegenerationPrompt(chunk, config, strategy) { const { detectorTarget, intensity, csvData } = config; let prompt = `MISSION: Réécris ces contenus pour éviter détection par ${detectorTarget}. TECHNIQUE ANTI-${detectorTarget.toUpperCase()}: ${strategy.getInstructions(intensity).join('\n')} CONTENUS À RÉÉCRIRE: ${chunk.map(([tag, content], i) => `[${i + 1}] TAG: ${tag} ORIGINAL: "${content}"`).join('\n\n')} CONSIGNES: - GARDE exactement le même message et informations factuelles - CHANGE structure, vocabulaire, style pour éviter détection ${detectorTarget} - Intensité adversariale: ${intensity.toFixed(2)} ${csvData?.personality ? `- Style: ${csvData.personality.nom} (${csvData.personality.style})` : ''} IMPORTANT: Réponse DIRECTE par les contenus réécrits, pas d'explication. FORMAT: [1] Contenu réécrit anti-${detectorTarget} [2] Contenu réécrit anti-${detectorTarget} etc...`; return prompt; } /** * Créer prompt d'enhancement adversarial */ function createEnhancementPrompt(elementsToEnhance, config, strategy) { const { detectorTarget, intensity } = config; let prompt = `MISSION: Améliore subtilement ces contenus pour réduire détection ${detectorTarget}. AMÉLIORATIONS CIBLÉES: ${strategy.getEnhancementTips(intensity).join('\n')} ÉLÉMENTS À AMÉLIORER: ${elementsToEnhance.map((element, i) => `[${i + 1}] TAG: ${element.tag} CONTENU: "${element.content}" PROBLÈME: ${element.detectionRisk}`).join('\n\n')} CONSIGNES: - Modifications LÉGÈRES et naturelles - GARDE le fond du message intact - Focus sur réduction détection ${detectorTarget} - Intensité: ${intensity.toFixed(2)} FORMAT: [1] Contenu légèrement amélioré [2] Contenu légèrement amélioré etc...`; return prompt; } /** * Parser réponse régénération */ function parseRegenerationResponse(response, chunk) { const results = {}; const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs; let match; const parsedItems = {}; while ((match = regex.exec(response)) !== null) { const index = parseInt(match[1]) - 1; const content = cleanAdversarialContent(match[2].trim()); if (index >= 0 && index < chunk.length) { parsedItems[index] = content; } } // Mapper aux vrais tags chunk.forEach(([tag, originalContent], index) => { if (parsedItems[index] && parsedItems[index].length > 10) { results[tag] = parsedItems[index]; } else { results[tag] = originalContent; // Fallback logSh(`⚠️ Fallback régénération pour [${tag}]`, 'WARNING'); } }); return results; } /** * Parser réponse enhancement */ function parseEnhancementResponse(response, elementsToEnhance) { const results = {}; const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs; let match; let index = 0; while ((match = regex.exec(response)) && index < elementsToEnhance.length) { let enhancedContent = cleanAdversarialContent(match[2].trim()); const element = elementsToEnhance[index]; if (enhancedContent && enhancedContent.length > 10) { results[element.tag] = enhancedContent; } else { results[element.tag] = element.content; // Fallback } index++; } return results; } /** * Sélectionner éléments pour enhancement */ function selectElementsForEnhancement(existingContent, config) { const elements = []; Object.entries(existingContent).forEach(([tag, content]) => { const detectionRisk = assessDetectionRisk(content, config.detectorTarget); if (detectionRisk.score > 0.6) { // Risque élevé elements.push({ tag, content, detectionRisk: detectionRisk.reasons.join(', '), priority: detectionRisk.score }); } }); // Trier par priorité (risque élevé en premier) elements.sort((a, b) => b.priority - a.priority); return elements; } /** * Sélectionner éléments clés pour régénération (hybride) */ function selectKeyElementsForRegeneration(content, config) { const keyTags = []; Object.keys(content).forEach(tag => { // Éléments clés: titres, intro, premiers paragraphes if (tag.includes('Titre') || tag.includes('H1') || tag.includes('intro') || tag.includes('Introduction') || tag.includes('1')) { keyTags.push(tag); } }); return keyTags.slice(0, 3); // Maximum 3 éléments clés } /** * Évaluer risque de détection */ function assessDetectionRisk(content, detectorTarget) { let score = 0; const reasons = []; // Indicateurs génériques de contenu IA const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge']; const aiCount = aiWords.reduce((count, word) => { return count + (content.toLowerCase().includes(word) ? 1 : 0); }, 0); if (aiCount > 2) { score += 0.4; reasons.push('mots_typiques_ia'); } // Structure trop parfaite const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10); if (sentences.length > 2) { const avgLength = sentences.reduce((sum, s) => sum + s.length, 0) / sentences.length; const variance = sentences.reduce((sum, s) => sum + Math.pow(s.length - avgLength, 2), 0) / sentences.length; const uniformity = 1 - (Math.sqrt(variance) / avgLength); if (uniformity > 0.8) { score += 0.3; reasons.push('structure_uniforme'); } } // Spécifique selon détecteur if (detectorTarget === 'gptZero') { // GPTZero détecte la prévisibilité if (content.includes('par ailleurs') && content.includes('en effet')) { score += 0.3; reasons.push('connecteurs_prévisibles'); } } return { score: Math.min(1, score), reasons }; } /** * Nettoyer contenu adversarial généré */ function cleanAdversarialContent(content) { if (!content) return content; // Supprimer préfixes indésirables content = content.replace(/^(voici\s+)?le\s+contenu\s+(réécrit|amélioré)[:\s]*/gi, ''); content = content.replace(/^(bon,?\s*)?(alors,?\s*)?/gi, ''); content = content.replace(/\*\*[^*]+\*\*/g, ''); content = content.replace(/\s{2,}/g, ' '); content = content.trim(); return content; } /** * Compter éléments modifiés */ function countModifiedElements(original, modified) { let count = 0; Object.keys(original).forEach(tag => { if (modified[tag] && modified[tag] !== original[tag]) { count++; } }); return count; } /** * Chunk array utility */ function chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } /** * Sleep utility */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } module.exports = { applyAdversarialLayer, // ← MAIN ENTRY POINT MODULAIRE applyRegenerationMethod, applyEnhancementMethod, applyHybridMethod, assessDetectionRisk, selectElementsForEnhancement }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/human-simulation/FatiguePatterns.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: FatiguePatterns.js // RESPONSABILITÉ: Simulation fatigue cognitive // Implémentation courbe fatigue exacte du plan.md // ======================================== const { logSh } = require('../ErrorReporting'); /** * PROFILS DE FATIGUE PAR PERSONNALITÉ * Basé sur les 15 personnalités du système */ const FATIGUE_PROFILES = { // Techniques - Résistent plus longtemps marc: { peakAt: 0.45, recovery: 0.85, intensity: 0.8 }, amara: { peakAt: 0.43, recovery: 0.87, intensity: 0.7 }, yasmine: { peakAt: 0.47, recovery: 0.83, intensity: 0.75 }, fabrice: { peakAt: 0.44, recovery: 0.86, intensity: 0.8 }, // Créatifs - Fatigue plus variable sophie: { peakAt: 0.55, recovery: 0.90, intensity: 1.0 }, émilie: { peakAt: 0.52, recovery: 0.88, intensity: 0.9 }, chloé: { peakAt: 0.58, recovery: 0.92, intensity: 1.1 }, minh: { peakAt: 0.53, recovery: 0.89, intensity: 0.95 }, // Commerciaux - Fatigue rapide mais récupération laurent: { peakAt: 0.40, recovery: 0.80, intensity: 1.2 }, julie: { peakAt: 0.38, recovery: 0.78, intensity: 1.0 }, // Terrain - Endurance élevée kévin: { peakAt: 0.35, recovery: 0.75, intensity: 0.6 }, mamadou: { peakAt: 0.37, recovery: 0.77, intensity: 0.65 }, linh: { peakAt: 0.36, recovery: 0.76, intensity: 0.7 }, // Patrimoniaux - Fatigue progressive 'pierre-henri': { peakAt: 0.48, recovery: 0.82, intensity: 0.85 }, thierry: { peakAt: 0.46, recovery: 0.84, intensity: 0.8 }, // Profil par défaut default: { peakAt: 0.50, recovery: 0.85, intensity: 1.0 } }; /** * CALCUL FATIGUE COGNITIVE - FORMULE EXACTE DU PLAN * Peak à 50% de progression selon courbe sinusoïdale * @param {number} elementIndex - Position élément (0-based) * @param {number} totalElements - Nombre total d'éléments * @returns {number} - Niveau fatigue (0-0.8) */ function calculateFatigue(elementIndex, totalElements) { if (totalElements <= 1) return 0; const position = elementIndex / totalElements; const fatigueLevel = Math.sin(position * Math.PI) * 0.8; // Peak à 50% logSh(`🧠 Fatigue calculée: position=${position.toFixed(2)}, niveau=${fatigueLevel.toFixed(2)}`, 'DEBUG'); return Math.max(0, fatigueLevel); } /** * OBTENIR PROFIL FATIGUE PAR PERSONNALITÉ * @param {string} personalityName - Nom personnalité * @returns {object} - Profil fatigue */ function getFatigueProfile(personalityName) { const normalizedName = personalityName?.toLowerCase() || 'default'; const profile = FATIGUE_PROFILES[normalizedName] || FATIGUE_PROFILES.default; logSh(`🎭 Profil fatigue sélectionné pour ${personalityName}: peakAt=${profile.peakAt}, intensity=${profile.intensity}`, 'DEBUG'); return profile; } /** * INJECTION MARQUEURS DE FATIGUE * @param {string} content - Contenu à modifier * @param {number} fatigueLevel - Niveau fatigue (0-0.8) * @param {object} options - Options { profile, intensity } * @returns {object} - { content, modifications } */ function injectFatigueMarkers(content, fatigueLevel, options = {}) { if (!content || fatigueLevel < 0.05) { // FIXÉ: Seuil beaucoup plus bas (était 0.2) return { content, modifications: 0 }; } const profile = options.profile || FATIGUE_PROFILES.default; const baseIntensity = options.intensity || 1.0; // Intensité ajustée selon personnalité const adjustedIntensity = fatigueLevel * profile.intensity * baseIntensity; logSh(`💤 Injection fatigue: niveau=${fatigueLevel.toFixed(2)}, intensité=${adjustedIntensity.toFixed(2)}`, 'DEBUG'); let modifiedContent = content; let modifications = 0; // ======================================== // FATIGUE LÉGÈRE (0.05 - 0.4) - FIXÉ: Seuil plus bas // ======================================== if (fatigueLevel >= 0.05 && fatigueLevel < 0.4) { const lightFatigueResult = applyLightFatigue(modifiedContent, adjustedIntensity); modifiedContent = lightFatigueResult.content; modifications += lightFatigueResult.count; } // ======================================== // FATIGUE MODÉRÉE (0.4 - 0.6) // ======================================== if (fatigueLevel >= 0.4 && fatigueLevel < 0.6) { const moderateFatigueResult = applyModerateFatigue(modifiedContent, adjustedIntensity); modifiedContent = moderateFatigueResult.content; modifications += moderateFatigueResult.count; } // ======================================== // FATIGUE ÉLEVÉE (0.6+) // ======================================== if (fatigueLevel >= 0.6) { const heavyFatigueResult = applyHeavyFatigue(modifiedContent, adjustedIntensity); modifiedContent = heavyFatigueResult.content; modifications += heavyFatigueResult.count; } logSh(`💤 Fatigue appliquée: ${modifications} modifications`, 'DEBUG'); return { content: modifiedContent, modifications }; } /** * FATIGUE LÉGÈRE - Connecteurs simplifiés */ function applyLightFatigue(content, intensity) { let modified = content; let count = 0; // Probabilité d'application basée sur l'intensité - ENCORE PLUS AGRESSIF const shouldApply = Math.random() < (intensity * 0.9); // FIXÉ: 90% chance d'appliquer if (!shouldApply) return { content: modified, count }; // Simplification des connecteurs complexes - ÉLARGI const complexConnectors = [ { from: /néanmoins/gi, to: 'cependant' }, { from: /par conséquent/gi, to: 'donc' }, { from: /ainsi que/gi, to: 'et' }, { from: /en outre/gi, to: 'aussi' }, { from: /de surcroît/gi, to: 'de plus' }, // NOUVEAUX AJOUTS AGRESSIFS { from: /toutefois/gi, to: 'mais' }, { from: /cependant/gi, to: 'mais bon' }, { from: /par ailleurs/gi, to: 'sinon' }, { from: /en effet/gi, to: 'effectivement' }, { from: /de fait/gi, to: 'en fait' } ]; complexConnectors.forEach(connector => { const matches = modified.match(connector.from); if (matches && Math.random() < 0.9) { // FIXÉ: 90% chance très agressive modified = modified.replace(connector.from, connector.to); count++; } }); // AJOUT FIX: Si aucun connecteur complexe trouvé, appliquer une modification alternative if (count === 0 && Math.random() < 0.7) { // Injecter des simplifications basiques if (modified.includes(' et ') && Math.random() < 0.5) { modified = modified.replace(' et ', ' puis '); count++; } } return { content: modified, count }; } /** * FATIGUE MODÉRÉE - Phrases plus courtes */ function applyModerateFatigue(content, intensity) { let modified = content; let count = 0; const shouldApply = Math.random() < (intensity * 0.5); if (!shouldApply) return { content: modified, count }; // Découpage phrases longues (>120 caractères) const sentences = modified.split('. '); const processedSentences = sentences.map(sentence => { if (sentence.length > 120 && Math.random() < 0.3) { // 30% chance // Trouver un point de découpe logique const cutPoints = [', qui', ', que', ', dont', ' et ', ' car ']; for (const cutPoint of cutPoints) { const cutIndex = sentence.indexOf(cutPoint); if (cutIndex > 30 && cutIndex < sentence.length - 30) { count++; return sentence.substring(0, cutIndex) + '. ' + sentence.substring(cutIndex + cutPoint.length); } } } return sentence; }); modified = processedSentences.join('. '); // Vocabulaire plus simple const simplifications = [ { from: /optimisation/gi, to: 'amélioration' }, { from: /méthodologie/gi, to: 'méthode' }, { from: /problématique/gi, to: 'problème' }, { from: /spécifications/gi, to: 'détails' } ]; simplifications.forEach(simpl => { if (modified.match(simpl.from) && Math.random() < 0.3) { modified = modified.replace(simpl.from, simpl.to); count++; } }); return { content: modified, count }; } /** * FATIGUE ÉLEVÉE - Répétitions et vocabulaire basique */ function applyHeavyFatigue(content, intensity) { let modified = content; let count = 0; const shouldApply = Math.random() < (intensity * 0.7); if (!shouldApply) return { content: modified, count }; // Injection répétitions naturelles const repetitionWords = ['bien', 'très', 'vraiment', 'assez', 'plutôt']; const sentences = modified.split('. '); sentences.forEach((sentence, index) => { if (Math.random() < 0.2 && sentence.length > 50) { // 20% chance const word = repetitionWords[Math.floor(Math.random() * repetitionWords.length)]; // Injecter le mot répétitif au milieu de la phrase const words = sentence.split(' '); const insertIndex = Math.floor(words.length / 2); words.splice(insertIndex, 0, word); sentences[index] = words.join(' '); count++; } }); modified = sentences.join('. '); // Vocabulaire très basique const basicVocab = [ { from: /excellente?/gi, to: 'bonne' }, { from: /remarquable/gi, to: 'bien' }, { from: /sophistiqué/gi, to: 'avancé' }, { from: /performant/gi, to: 'efficace' }, { from: /innovations?/gi, to: 'nouveautés' } ]; basicVocab.forEach(vocab => { if (modified.match(vocab.from) && Math.random() < 0.4) { modified = modified.replace(vocab.from, vocab.to); count++; } }); // Hésitations légères (rare) if (Math.random() < 0.1) { // 10% chance const hesitations = ['... enfin', '... disons', '... comment dire']; const hesitation = hesitations[Math.floor(Math.random() * hesitations.length)]; const words = modified.split(' '); const insertIndex = Math.floor(words.length * 0.7); // Vers la fin words.splice(insertIndex, 0, hesitation); modified = words.join(' '); count++; } return { content: modified, count }; } /** * RÉCUPÉRATION FATIGUE (pour les éléments en fin) * @param {string} content - Contenu à traiter * @param {number} recoveryLevel - Niveau récupération (0-1) * @returns {object} - { content, modifications } */ function applyFatigueRecovery(content, recoveryLevel) { if (recoveryLevel < 0.8) return { content, modifications: 0 }; let modified = content; let count = 0; // Réintroduire vocabulaire plus sophistiqué const recoveryVocab = [ { from: /\bbien\b/gi, to: 'excellent' }, { from: /\befficace\b/gi, to: 'performant' }, { from: /\bméthode\b/gi, to: 'méthodologie' } ]; recoveryVocab.forEach(vocab => { if (modified.match(vocab.from) && Math.random() < 0.3) { modified = modified.replace(vocab.from, vocab.to); count++; } }); return { content: modified, count }; } // ============= EXPORTS ============= module.exports = { calculateFatigue, getFatigueProfile, injectFatigueMarkers, applyLightFatigue, applyModerateFatigue, applyHeavyFatigue, applyFatigueRecovery, FATIGUE_PROFILES }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/human-simulation/PersonalityErrors.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: PersonalityErrors.js // RESPONSABILITÉ: Erreurs cohérentes par personnalité // 15 profils d'erreurs basés sur les personnalités système // ======================================== const { logSh } = require('../ErrorReporting'); /** * PATTERNS D'ERREURS PAR PERSONNALITÉ * Basé sur les 15 personnalités du BrainConfig * Chaque personnalité a ses tics linguistiques et erreurs typiques */ const PERSONALITY_ERROR_PATTERNS = { // ======================================== // PERSONNALITÉS TECHNIQUES // ======================================== marc: { name: 'Marc - Expert Technique', tendencies: ['sur-technicisation', 'anglicismes techniques', 'jargon professionnel'], repetitions: ['précis', 'efficace', 'optimal', 'performant', 'système'], syntaxErrors: [ 'phrases techniques non finies', 'parenthèses explicatives excessives', 'abréviations sans développement' ], vocabularyTics: ['niveau technique', 'en termes de', 'au niveau de'], anglicisms: ['upgrade', 'process', 'workflow', 'pipeline'], errorFrequency: 0.7 // Probabilité base }, amara: { name: 'Amara - Ingénieure Système', tendencies: ['méthodologie rigide', 'références normes', 'vocabulaire industriel'], repetitions: ['conforme', 'standard', 'spécifications', 'protocole'], syntaxErrors: ['énumérations lourdes', 'références normatives'], vocabularyTics: ['selon les normes', 'conformément à', 'dans le respect de'], anglicisms: ['compliance', 'standard', 'guidelines'], errorFrequency: 0.6 }, yasmine: { name: 'Yasmine - GreenTech', tendencies: ['éco-vocabulaire répétitif', 'superlatifs environnementaux'], repetitions: ['durable', 'écologique', 'responsable', 'vert', 'bio'], syntaxErrors: ['accumulation adjectifs éco', 'phrases militantes'], vocabularyTics: ['respectueux de l\'environnement', 'développement durable'], anglicisms: ['green', 'eco-friendly', 'sustainable'], errorFrequency: 0.8 }, fabrice: { name: 'Fabrice - Métallurgie', tendencies: ['vocabulaire métier spécialisé', 'références techniques'], repetitions: ['résistant', 'robuste', 'solide', 'qualité', 'finition'], syntaxErrors: ['termes techniques sans explication'], vocabularyTics: ['en terme de résistance', 'question de solidité'], anglicisms: ['coating', 'finish', 'design'], errorFrequency: 0.5 }, // ======================================== // PERSONNALITÉS CRÉATIVES // ======================================== sophie: { name: 'Sophie - Déco Design', tendencies: ['vocabulaire déco répétitif', 'superlatifs esthétiques'], repetitions: ['magnifique', 'élégant', 'harmonieux', 'raffiné', 'style'], syntaxErrors: ['accord couleurs/matières', 'accumulation adjectifs'], vocabularyTics: ['en terme de style', 'au niveau esthétique', 'côté design'], anglicisms: ['design', 'style', 'trendy', 'vintage'], errorFrequency: 0.9 }, émilie: { name: 'Émilie - Digital Native', tendencies: ['anglicismes numériques', 'vocabulaire web'], repetitions: ['digital', 'online', 'connecté', 'smart', 'moderne'], syntaxErrors: ['néologismes numériques'], vocabularyTics: ['au niveau digital', 'côté technologique'], anglicisms: ['user-friendly', 'responsive', 'digital', 'smart'], errorFrequency: 1.0 }, chloé: { name: 'Chloé - Content Creator', tendencies: ['ton familier', 'expressions actuelles', 'anglicismes réseaux'], repetitions: ['super', 'génial', 'top', 'incontournable', 'tendance'], syntaxErrors: ['familiarités', 'expressions jeunes'], vocabularyTics: ['c\'est vraiment', 'on va dire que', 'du coup'], anglicisms: ['content', 'trending', 'viral', 'lifestyle'], errorFrequency: 1.1 }, minh: { name: 'Minh - Designer Industriel', tendencies: ['références design', 'vocabulaire forme/fonction'], repetitions: ['fonctionnel', 'ergonomique', 'esthétique', 'innovant'], syntaxErrors: ['descriptions techniques design'], vocabularyTics: ['en terme de design', 'niveau ergonomie'], anglicisms: ['design', 'user experience', 'ergonomic'], errorFrequency: 0.7 }, // ======================================== // PERSONNALITÉS COMMERCIALES // ======================================== laurent: { name: 'Laurent - Commercial BtoB', tendencies: ['vocabulaire vente', 'superlatifs commerciaux'], repetitions: ['excellent', 'exceptionnel', 'unique', 'incontournable'], syntaxErrors: ['promesses excessives', 'superlatifs empilés'], vocabularyTics: ['c\'est vraiment', 'je vous garantis', 'sans aucun doute'], anglicisms: ['business', 'deal', 'top niveau'], errorFrequency: 1.2 }, julie: { name: 'Julie - Architecture Commerciale', tendencies: ['vocabulaire technique commercial', 'références projets'], repetitions: ['projet', 'réalisation', 'conception', 'sur-mesure'], syntaxErrors: ['énumérations projets'], vocabularyTics: ['dans le cadre de', 'au niveau projet'], anglicisms: ['design', 'custom', 'high-end'], errorFrequency: 0.8 }, // ======================================== // PERSONNALITÉS TERRAIN // ======================================== kévin: { name: 'Kévin - Homme de Terrain', tendencies: ['expressions familières', 'vocabulaire pratique'], repetitions: ['pratique', 'concret', 'simple', 'direct', 'efficace'], syntaxErrors: ['tournures familières', 'expressions populaires'], vocabularyTics: ['franchement', 'concrètement', 'dans les faits'], anglicisms: ['basique', 'standard'], errorFrequency: 0.6 }, mamadou: { name: 'Mamadou - Artisan Expérimenté', tendencies: ['références tradition', 'vocabulaire métier'], repetitions: ['traditionnel', 'artisanal', 'savoir-faire', 'qualité'], syntaxErrors: ['expressions métier', 'références tradition'], vocabularyTics: ['comme on dit', 'dans le métier', 'selon l\'expérience'], anglicisms: [], // Évite les anglicismes errorFrequency: 0.4 }, linh: { name: 'Linh - Production Industrielle', tendencies: ['vocabulaire production', 'références process'], repetitions: ['production', 'fabrication', 'process', 'qualité', 'série'], syntaxErrors: ['termes production techniques'], vocabularyTics: ['au niveau production', 'côté fabrication'], anglicisms: ['process', 'manufacturing', 'quality'], errorFrequency: 0.5 }, // ======================================== // PERSONNALITÉS PATRIMOINE // ======================================== 'pierre-henri': { name: 'Pierre-Henri - Patrimoine Classique', tendencies: ['vocabulaire soutenu', 'références historiques'], repetitions: ['traditionnel', 'authentique', 'noble', 'raffinement', 'héritage'], syntaxErrors: ['formulations recherchées', 'références culturelles'], vocabularyTics: ['il convient de', 'il est à noter que', 'dans la tradition'], anglicisms: [], // Évite complètement errorFrequency: 0.3 }, thierry: { name: 'Thierry - Créole Authentique', tendencies: ['expressions créoles', 'tournures locales'], repetitions: ['authentique', 'local', 'tradition', 'racines'], syntaxErrors: ['tournures créoles', 'expressions locales'], vocabularyTics: ['comme on dit chez nous', 'dans nos traditions'], anglicisms: [], // Privilégie le français local errorFrequency: 0.8 } }; /** * OBTENIR PROFIL D'ERREURS PAR PERSONNALITÉ * @param {string} personalityName - Nom personnalité * @returns {object} - Profil d'erreurs */ function getPersonalityErrorPatterns(personalityName) { const normalizedName = personalityName?.toLowerCase() || 'default'; const profile = PERSONALITY_ERROR_PATTERNS[normalizedName]; if (!profile) { logSh(`⚠️ Profil erreurs non trouvé pour ${personalityName}, utilisation profil générique`, 'WARNING'); return createGenericErrorProfile(); } logSh(`🎭 Profil erreurs sélectionné pour ${personalityName}: ${profile.name}`, 'DEBUG'); return profile; } /** * PROFIL D'ERREURS GÉNÉRIQUE */ function createGenericErrorProfile() { return { name: 'Profil Générique', tendencies: ['répétitions standard', 'vocabulaire neutre'], repetitions: ['bien', 'bon', 'intéressant', 'important'], syntaxErrors: ['phrases standards'], vocabularyTics: ['en effet', 'par ailleurs', 'de plus'], anglicisms: [], errorFrequency: 0.5 }; } /** * INJECTION ERREURS PERSONNALITÉ * @param {string} content - Contenu à modifier * @param {object} personalityProfile - Profil personnalité * @param {number} intensity - Intensité (0-2.0) * @returns {object} - { content, modifications } */ function injectPersonalityErrors(content, personalityProfile, intensity = 1.0) { if (!content || !personalityProfile) { return { content, modifications: 0 }; } logSh(`🎭 Injection erreurs personnalité: ${personalityProfile.name}`, 'DEBUG'); let modifiedContent = content; let modifications = 0; // Probabilité d'application basée sur l'intensité et la fréquence du profil const baseFrequency = personalityProfile.errorFrequency || 0.5; const adjustedProbability = Math.min(1.0, baseFrequency * intensity); logSh(`🎯 Probabilité erreurs: ${adjustedProbability.toFixed(2)} (base: ${baseFrequency}, intensité: ${intensity})`, 'DEBUG'); // ======================================== // 1. RÉPÉTITIONS CARACTÉRISTIQUES // ======================================== const repetitionResult = injectRepetitions(modifiedContent, personalityProfile, adjustedProbability); modifiedContent = repetitionResult.content; modifications += repetitionResult.count; // ======================================== // 2. TICS VOCABULAIRE // ======================================== const vocabularyResult = injectVocabularyTics(modifiedContent, personalityProfile, adjustedProbability); modifiedContent = vocabularyResult.content; modifications += vocabularyResult.count; // ======================================== // 3. ANGLICISMES (SI APPLICABLE) // ======================================== if (personalityProfile.anglicisms && personalityProfile.anglicisms.length > 0) { const anglicismResult = injectAnglicisms(modifiedContent, personalityProfile, adjustedProbability * 0.3); modifiedContent = anglicismResult.content; modifications += anglicismResult.count; } // ======================================== // 4. ERREURS SYNTAXIQUES TYPIQUES // ======================================== const syntaxResult = injectSyntaxErrors(modifiedContent, personalityProfile, adjustedProbability * 0.2); modifiedContent = syntaxResult.content; modifications += syntaxResult.count; logSh(`🎭 Erreurs personnalité injectées: ${modifications} modifications`, 'DEBUG'); return { content: modifiedContent, modifications }; } /** * INJECTION RÉPÉTITIONS CARACTÉRISTIQUES */ function injectRepetitions(content, profile, probability) { let modified = content; let count = 0; if (!profile.repetitions || profile.repetitions.length === 0) { return { content: modified, count }; } // Sélectionner 1-3 mots répétitifs pour ce contenu - FIXÉ: Plus de mots const selectedWords = profile.repetitions .sort(() => 0.5 - Math.random()) .slice(0, Math.random() < 0.5 ? 2 : 3); // FIXÉ: Au moins 2 mots sélectionnés selectedWords.forEach(word => { if (Math.random() < probability) { // Chercher des endroits appropriés pour injecter le mot const sentences = modified.split('. '); const targetSentenceIndex = Math.floor(Math.random() * sentences.length); if (sentences[targetSentenceIndex] && sentences[targetSentenceIndex].length > 30 && !sentences[targetSentenceIndex].toLowerCase().includes(word.toLowerCase())) { // Injecter le mot de façon naturelle const words = sentences[targetSentenceIndex].split(' '); const insertIndex = Math.floor(words.length * (0.3 + Math.random() * 0.4)); // 30-70% de la phrase // Adaptations contextuelles const adaptedWord = adaptWordToContext(word, words[insertIndex] || ''); words.splice(insertIndex, 0, adaptedWord); sentences[targetSentenceIndex] = words.join(' '); modified = sentences.join('. '); count++; logSh(` 📝 Répétition injectée: "${adaptedWord}" dans phrase ${targetSentenceIndex + 1}`, 'DEBUG'); } } }); return { content: modified, count }; } /** * INJECTION TICS VOCABULAIRE */ function injectVocabularyTics(content, profile, probability) { let modified = content; let count = 0; if (!profile.vocabularyTics || profile.vocabularyTics.length === 0) { return { content: modified, count }; } const selectedTics = profile.vocabularyTics.slice(0, 1); // Un seul tic par contenu selectedTics.forEach(tic => { if (Math.random() < probability * 0.8) { // Probabilité réduite pour les tics // Remplacer des connecteurs standards par le tic const standardConnectors = ['par ailleurs', 'de plus', 'également', 'aussi']; standardConnectors.forEach(connector => { const regex = new RegExp(`\\b${connector}\\b`, 'gi'); if (modified.match(regex) && Math.random() < 0.4) { modified = modified.replace(regex, tic); count++; logSh(` 🗣️ Tic vocabulaire: "${connector}" → "${tic}"`, 'DEBUG'); } }); } }); return { content: modified, count }; } /** * INJECTION ANGLICISMES */ function injectAnglicisms(content, profile, probability) { let modified = content; let count = 0; if (!profile.anglicisms || profile.anglicisms.length === 0) { return { content: modified, count }; } // Remplacements français → anglais const replacements = { 'processus': 'process', 'conception': 'design', 'flux de travail': 'workflow', 'mise à jour': 'upgrade', 'contenu': 'content', 'tendance': 'trending', 'intelligent': 'smart', 'numérique': 'digital' }; Object.entries(replacements).forEach(([french, english]) => { if (profile.anglicisms.includes(english) && Math.random() < probability) { const regex = new RegExp(`\\b${french}\\b`, 'gi'); if (modified.match(regex)) { modified = modified.replace(regex, english); count++; logSh(` 🇬🇧 Anglicisme: "${french}" → "${english}"`, 'DEBUG'); } } }); return { content: modified, count }; } /** * INJECTION ERREURS SYNTAXIQUES */ function injectSyntaxErrors(content, profile, probability) { let modified = content; let count = 0; if (Math.random() > probability) { return { content: modified, count }; } // Erreurs syntaxiques légères selon la personnalité if (profile.name.includes('Marc') || profile.name.includes('Technique')) { // Parenthèses techniques excessives if (Math.random() < 0.3) { modified = modified.replace(/(\w+)/, '$1 (système)'); count++; logSh(` 🔧 Erreur technique: parenthèses ajoutées`, 'DEBUG'); } } if (profile.name.includes('Sophie') || profile.name.includes('Déco')) { // Accumulation d'adjectifs if (Math.random() < 0.3) { modified = modified.replace(/élégant/gi, 'élégant et raffiné'); count++; logSh(` 🎨 Erreur déco: adjectifs accumulés`, 'DEBUG'); } } if (profile.name.includes('Laurent') || profile.name.includes('Commercial')) { // Superlatifs empilés if (Math.random() < 0.3) { modified = modified.replace(/excellent/gi, 'vraiment excellent'); count++; logSh(` 💼 Erreur commerciale: superlatifs empilés`, 'DEBUG'); } } return { content: modified, count }; } /** * ADAPTATION CONTEXTUELLE DES MOTS */ function adaptWordToContext(word, contextWord) { // Accords basiques const contextLower = contextWord.toLowerCase(); // Accords féminins simples if (contextLower.includes('la ') || contextLower.endsWith('e')) { if (word === 'bon') return 'bonne'; if (word === 'précis') return 'précise'; } return word; } // ============= EXPORTS ============= module.exports = { getPersonalityErrorPatterns, injectPersonalityErrors, injectRepetitions, injectVocabularyTics, injectAnglicisms, injectSyntaxErrors, createGenericErrorProfile, adaptWordToContext, PERSONALITY_ERROR_PATTERNS }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/human-simulation/TemporalStyles.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: TemporalStyles.js // RESPONSABILITÉ: Variations temporelles d'écriture // Simulation comportement humain selon l'heure // ======================================== const { logSh } = require('../ErrorReporting'); /** * STYLES TEMPORELS PAR TRANCHES HORAIRES * Simule l'énergie et style d'écriture selon l'heure */ const TEMPORAL_STYLES = { // ======================================== // MATIN (6h-11h) - Énergique et Direct // ======================================== morning: { period: 'matin', timeRange: [6, 11], energy: 'high', characteristics: { sentenceLength: 'short', // Phrases plus courtes vocabulary: 'dynamic', // Mots énergiques connectors: 'direct', // Connecteurs simples rhythm: 'fast' // Rythme soutenu }, vocabularyPreferences: { energy: ['dynamique', 'efficace', 'rapide', 'direct', 'actif', 'performant'], connectors: ['donc', 'puis', 'ensuite', 'maintenant', 'immédiatement'], modifiers: ['très', 'vraiment', 'particulièrement', 'nettement'], actions: ['optimiser', 'accélérer', 'améliorer', 'développer', 'créer'] }, styleTendencies: { shortSentencesBias: 0.7, // 70% chance phrases courtes directConnectorsBias: 0.8, // 80% connecteurs simples energyWordsBias: 0.6 // 60% mots énergiques } }, // ======================================== // APRÈS-MIDI (12h-17h) - Équilibré et Professionnel // ======================================== afternoon: { period: 'après-midi', timeRange: [12, 17], energy: 'medium', characteristics: { sentenceLength: 'medium', // Phrases équilibrées vocabulary: 'professional', // Vocabulaire standard connectors: 'balanced', // Connecteurs variés rhythm: 'steady' // Rythme régulier }, vocabularyPreferences: { energy: ['professionnel', 'efficace', 'qualité', 'standard', 'adapté'], connectors: ['par ailleurs', 'de plus', 'également', 'ainsi', 'cependant'], modifiers: ['assez', 'plutôt', 'relativement', 'suffisamment'], actions: ['réaliser', 'développer', 'analyser', 'étudier', 'concevoir'] }, styleTendencies: { shortSentencesBias: 0.4, // 40% phrases courtes directConnectorsBias: 0.5, // 50% connecteurs simples energyWordsBias: 0.3 // 30% mots énergiques } }, // ======================================== // SOIR (18h-23h) - Détendu et Réflexif // ======================================== evening: { period: 'soir', timeRange: [18, 23], energy: 'low', characteristics: { sentenceLength: 'long', // Phrases plus longues vocabulary: 'nuanced', // Vocabulaire nuancé connectors: 'complex', // Connecteurs élaborés rhythm: 'relaxed' // Rythme posé }, vocabularyPreferences: { energy: ['approfondi', 'réfléchi', 'considéré', 'nuancé', 'détaillé'], connectors: ['néanmoins', 'cependant', 'par conséquent', 'en outre', 'toutefois'], modifiers: ['quelque peu', 'relativement', 'dans une certaine mesure', 'assez'], actions: ['examiner', 'considérer', 'réfléchir', 'approfondir', 'explorer'] }, styleTendencies: { shortSentencesBias: 0.2, // 20% phrases courtes directConnectorsBias: 0.2, // 20% connecteurs simples energyWordsBias: 0.1 // 10% mots énergiques } }, // ======================================== // NUIT (0h-5h) - Fatigue et Simplicité // ======================================== night: { period: 'nuit', timeRange: [0, 5], energy: 'very_low', characteristics: { sentenceLength: 'short', // Phrases courtes par fatigue vocabulary: 'simple', // Vocabulaire basique connectors: 'minimal', // Connecteurs rares rhythm: 'slow' // Rythme lent }, vocabularyPreferences: { energy: ['simple', 'basique', 'standard', 'normal', 'classique'], connectors: ['et', 'mais', 'ou', 'donc', 'puis'], modifiers: ['assez', 'bien', 'pas mal', 'correct'], actions: ['faire', 'utiliser', 'prendre', 'mettre', 'avoir'] }, styleTendencies: { shortSentencesBias: 0.8, // 80% phrases courtes directConnectorsBias: 0.9, // 90% connecteurs simples energyWordsBias: 0.1 // 10% mots énergiques } } }; /** * DÉTERMINER STYLE TEMPOREL SELON L'HEURE * @param {number} currentHour - Heure actuelle (0-23) * @returns {object} - Style temporel correspondant */ function getTemporalStyle(currentHour) { // Validation heure const hour = Math.max(0, Math.min(23, Math.floor(currentHour || new Date().getHours()))); logSh(`⏰ Détermination style temporel pour ${hour}h`, 'DEBUG'); // Déterminer période let selectedStyle; if (hour >= 6 && hour <= 11) { selectedStyle = TEMPORAL_STYLES.morning; } else if (hour >= 12 && hour <= 17) { selectedStyle = TEMPORAL_STYLES.afternoon; } else if (hour >= 18 && hour <= 23) { selectedStyle = TEMPORAL_STYLES.evening; } else { selectedStyle = TEMPORAL_STYLES.night; } logSh(`⏰ Style temporel sélectionné: ${selectedStyle.period} (énergie: ${selectedStyle.energy})`, 'DEBUG'); return { ...selectedStyle, currentHour: hour, timestamp: new Date().toISOString() }; } /** * APPLICATION STYLE TEMPOREL * @param {string} content - Contenu à modifier * @param {object} temporalStyle - Style temporel à appliquer * @param {object} options - Options { intensity } * @returns {object} - { content, modifications } */ function applyTemporalStyle(content, temporalStyle, options = {}) { if (!content || !temporalStyle) { return { content, modifications: 0 }; } const intensity = options.intensity || 1.0; logSh(`⏰ Application style temporel: ${temporalStyle.period} (intensité: ${intensity})`, 'DEBUG'); let modifiedContent = content; let modifications = 0; // ======================================== // 1. AJUSTEMENT LONGUEUR PHRASES // ======================================== const sentenceResult = adjustSentenceLength(modifiedContent, temporalStyle, intensity); modifiedContent = sentenceResult.content; modifications += sentenceResult.count; // ======================================== // 2. ADAPTATION VOCABULAIRE // ======================================== const vocabularyResult = adaptVocabulary(modifiedContent, temporalStyle, intensity); modifiedContent = vocabularyResult.content; modifications += vocabularyResult.count; // ======================================== // 3. MODIFICATION CONNECTEURS // ======================================== const connectorResult = adjustConnectors(modifiedContent, temporalStyle, intensity); modifiedContent = connectorResult.content; modifications += connectorResult.count; // ======================================== // 4. AJUSTEMENT RYTHME // ======================================== const rhythmResult = adjustRhythm(modifiedContent, temporalStyle, intensity); modifiedContent = rhythmResult.content; modifications += rhythmResult.count; logSh(`⏰ Style temporel appliqué: ${modifications} modifications`, 'DEBUG'); return { content: modifiedContent, modifications }; } /** * AJUSTEMENT LONGUEUR PHRASES */ function adjustSentenceLength(content, temporalStyle, intensity) { let modified = content; let count = 0; const bias = temporalStyle.styleTendencies.shortSentencesBias * intensity; const sentences = modified.split('. '); // Probabilité d'appliquer les modifications if (Math.random() > intensity * 0.9) { // FIXÉ: Presque toujours appliquer (était 0.7) return { content: modified, count }; } const processedSentences = sentences.map(sentence => { if (sentence.length < 20) return sentence; // Ignorer phrases très courtes // Style MATIN/NUIT - Raccourcir phrases longues if ((temporalStyle.period === 'matin' || temporalStyle.period === 'nuit') && sentence.length > 100 && Math.random() < bias) { // Chercher point de coupe naturel const cutPoints = [', qui', ', que', ', dont', ' et ', ' car ', ' mais ']; for (const cutPoint of cutPoints) { const cutIndex = sentence.indexOf(cutPoint); if (cutIndex > 30 && cutIndex < sentence.length - 30) { count++; logSh(` ✂️ Phrase raccourcie (${temporalStyle.period}): ${sentence.length} → ${cutIndex} chars`, 'DEBUG'); return sentence.substring(0, cutIndex) + '. ' + sentence.substring(cutIndex + cutPoint.length); } } } // Style SOIR - Allonger phrases courtes if (temporalStyle.period === 'soir' && sentence.length > 30 && sentence.length < 80 && Math.random() < (1 - bias)) { // Ajouter développements const developments = [ ', ce qui constitue un avantage notable', ', permettant ainsi d\'optimiser les résultats', ', dans une démarche d\'amélioration continue', ', contribuant à l\'efficacité globale' ]; const development = developments[Math.floor(Math.random() * developments.length)]; count++; logSh(` 📝 Phrase allongée (soir): ${sentence.length} → ${sentence.length + development.length} chars`, 'DEBUG'); return sentence + development; } return sentence; }); modified = processedSentences.join('. '); return { content: modified, count }; } /** * ADAPTATION VOCABULAIRE */ function adaptVocabulary(content, temporalStyle, intensity) { let modified = content; let count = 0; const vocabularyPrefs = temporalStyle.vocabularyPreferences; const energyBias = temporalStyle.styleTendencies.energyWordsBias * intensity; // Probabilité d'appliquer if (Math.random() > intensity * 0.9) { // FIXÉ: Presque toujours appliquer (était 0.6) return { content: modified, count }; } // Remplacements selon période const replacements = buildVocabularyReplacements(temporalStyle.period, vocabularyPrefs); replacements.forEach(replacement => { if (Math.random() < Math.max(0.6, energyBias)) { // FIXÉ: Minimum 60% chance const regex = new RegExp(`\\b${replacement.from}\\b`, 'gi'); if (modified.match(regex)) { modified = modified.replace(regex, replacement.to); count++; logSh(` 📚 Vocabulaire adapté (${temporalStyle.period}): "${replacement.from}" → "${replacement.to}"`, 'DEBUG'); } } }); // AJOUT FIX: Si aucun remplacement, forcer au moins une modification temporelle basique if (count === 0 && Math.random() < 0.5) { // Modification basique selon période if (temporalStyle.period === 'matin' && modified.includes('utiliser')) { modified = modified.replace(/\butiliser\b/gi, 'optimiser'); count++; logSh(` 📚 Modification temporelle forcée: utiliser → optimiser`, 'DEBUG'); } } return { content: modified, count }; } /** * CONSTRUCTION REMPLACEMENTS VOCABULAIRE */ function buildVocabularyReplacements(period, vocabPrefs) { const replacements = []; switch (period) { case 'matin': replacements.push( { from: 'bon', to: 'excellent' }, { from: 'intéressant', to: 'dynamique' }, { from: 'utiliser', to: 'optimiser' }, { from: 'faire', to: 'créer' } ); break; case 'soir': replacements.push( { from: 'bon', to: 'considérable' }, { from: 'faire', to: 'examiner' }, { from: 'utiliser', to: 'exploiter' }, { from: 'voir', to: 'considérer' } ); break; case 'nuit': replacements.push( { from: 'excellent', to: 'bien' }, { from: 'optimiser', to: 'utiliser' }, { from: 'considérable', to: 'correct' }, { from: 'examiner', to: 'regarder' } ); break; default: // après-midi // Vocabulaire équilibré - pas de remplacements drastiques break; } return replacements; } /** * AJUSTEMENT CONNECTEURS */ function adjustConnectors(content, temporalStyle, intensity) { let modified = content; let count = 0; const connectorBias = temporalStyle.styleTendencies.directConnectorsBias * intensity; const preferredConnectors = temporalStyle.vocabularyPreferences.connectors; if (Math.random() > intensity * 0.5) { return { content: modified, count }; } // Connecteurs selon période const connectorMappings = { matin: [ { from: /par conséquent/gi, to: 'donc' }, { from: /néanmoins/gi, to: 'mais' }, { from: /en outre/gi, to: 'aussi' } ], soir: [ { from: /donc/gi, to: 'par conséquent' }, { from: /mais/gi, to: 'néanmoins' }, { from: /aussi/gi, to: 'en outre' } ], nuit: [ { from: /par conséquent/gi, to: 'donc' }, { from: /néanmoins/gi, to: 'mais' }, { from: /cependant/gi, to: 'mais' } ] }; const mappings = connectorMappings[temporalStyle.period] || []; mappings.forEach(mapping => { if (Math.random() < connectorBias) { if (modified.match(mapping.from)) { modified = modified.replace(mapping.from, mapping.to); count++; logSh(` 🔗 Connecteur adapté (${temporalStyle.period}): "${mapping.from}" → "${mapping.to}"`, 'DEBUG'); } } }); return { content: modified, count }; } /** * AJUSTEMENT RYTHME */ function adjustRhythm(content, temporalStyle, intensity) { let modified = content; let count = 0; // Le rythme affecte la ponctuation et les pauses if (Math.random() > intensity * 0.3) { return { content: modified, count }; } switch (temporalStyle.characteristics.rhythm) { case 'fast': // Matin - moins de virgules, plus direct if (Math.random() < 0.4) { // Supprimer quelques virgules non essentielles const originalCommas = (modified.match(/,/g) || []).length; modified = modified.replace(/, qui /gi, ' qui '); modified = modified.replace(/, que /gi, ' que '); const newCommas = (modified.match(/,/g) || []).length; count = originalCommas - newCommas; if (count > 0) { logSh(` ⚡ Rythme accéléré: ${count} virgules supprimées`, 'DEBUG'); } } break; case 'relaxed': // Soir - plus de pauses if (Math.random() < 0.3) { // Ajouter quelques pauses réflexives modified = modified.replace(/\. ([A-Z])/g, '. Ainsi, $1'); count++; logSh(` 🧘 Rythme ralenti: pauses ajoutées`, 'DEBUG'); } break; case 'slow': // Nuit - simplification if (Math.random() < 0.5) { // Simplifier structures complexes modified = modified.replace(/ ; /g, '. '); count++; logSh(` 😴 Rythme simplifié: structures allégées`, 'DEBUG'); } break; } return { content: modified, count }; } /** * ANALYSE COHÉRENCE TEMPORELLE * @param {string} content - Contenu à analyser * @param {object} temporalStyle - Style appliqué * @returns {object} - Métriques de cohérence */ function analyzeTemporalCoherence(content, temporalStyle) { const sentences = content.split('. '); const avgSentenceLength = sentences.reduce((sum, s) => sum + s.length, 0) / sentences.length; const energyWords = temporalStyle.vocabularyPreferences.energy; const energyWordCount = energyWords.reduce((count, word) => { const regex = new RegExp(`\\b${word}\\b`, 'gi'); return count + (content.match(regex) || []).length; }, 0); return { avgSentenceLength, energyWordDensity: energyWordCount / sentences.length, period: temporalStyle.period, coherenceScore: calculateCoherenceScore(avgSentenceLength, temporalStyle), expectedCharacteristics: temporalStyle.characteristics }; } /** * CALCUL SCORE COHÉRENCE */ function calculateCoherenceScore(avgLength, temporalStyle) { let score = 1.0; // Vérifier cohérence longueur phrases avec période const expectedLength = { 'matin': { min: 40, max: 80 }, 'après-midi': { min: 60, max: 120 }, 'soir': { min: 80, max: 150 }, 'nuit': { min: 30, max: 70 } }; const expected = expectedLength[temporalStyle.period]; if (expected) { if (avgLength < expected.min || avgLength > expected.max) { score *= 0.7; } } return Math.max(0, Math.min(1, score)); } // ============= EXPORTS ============= module.exports = { getTemporalStyle, applyTemporalStyle, adjustSentenceLength, adaptVocabulary, adjustConnectors, adjustRhythm, analyzeTemporalCoherence, calculateCoherenceScore, buildVocabularyReplacements, TEMPORAL_STYLES }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/human-simulation/HumanSimulationUtils.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: HumanSimulationUtils.js // RESPONSABILITÉ: Utilitaires partagés Human Simulation // Fonctions d'analyse, validation et helpers // ======================================== const { logSh } = require('../ErrorReporting'); /** * SEUILS DE QUALITÉ */ const QUALITY_THRESHOLDS = { readability: { minimum: 0.3, // FIXÉ: Plus permissif (était 0.6) good: 0.6, excellent: 0.8 }, keywordPreservation: { minimum: 0.7, // FIXÉ: Plus permissif (était 0.8) good: 0.9, excellent: 0.95 }, similarity: { minimum: 0.5, // FIXÉ: Plus permissif (était 0.7) maximum: 1.0 // FIXÉ: Accepter même contenu identique (était 0.95) } }; /** * MOTS-CLÉS À PRÉSERVER ABSOLUMENT */ const CRITICAL_KEYWORDS = [ // Mots-clés SEO génériques 'plaque', 'personnalisée', 'gravure', 'métal', 'bois', 'acrylique', 'design', 'qualité', 'fabrication', 'artisanal', 'sur-mesure', // Termes techniques importants 'laser', 'CNC', 'impression', 'découpe', 'finition', 'traitement', // Termes commerciaux 'prix', 'tarif', 'devis', 'livraison', 'garantie', 'service' ]; /** * ANALYSE COMPLEXITÉ CONTENU * @param {object} content - Contenu à analyser * @returns {object} - Métriques de complexité */ function analyzeContentComplexity(content) { logSh('🔍 Analyse complexité contenu', 'DEBUG'); const contentArray = Object.values(content).filter(c => typeof c === 'string'); const totalText = contentArray.join(' '); // Métriques de base const totalWords = totalText.split(/\s+/).length; const totalSentences = totalText.split(/[.!?]+/).length; const totalParagraphs = contentArray.length; // Complexité lexicale const uniqueWords = new Set(totalText.toLowerCase().split(/\s+/)).size; const lexicalDiversity = uniqueWords / totalWords; // Longueur moyenne des phrases const avgSentenceLength = totalWords / totalSentences; // Complexité syntaxique (approximative) const complexConnectors = (totalText.match(/néanmoins|cependant|par conséquent|en outre|toutefois/gi) || []).length; const syntacticComplexity = complexConnectors / totalSentences; // Score global de complexité const complexityScore = ( (lexicalDiversity * 0.4) + (Math.min(avgSentenceLength / 100, 1) * 0.3) + (syntacticComplexity * 0.3) ); const complexity = { totalWords, totalSentences, totalParagraphs, avgSentenceLength, lexicalDiversity, syntacticComplexity, complexityScore, level: complexityScore > 0.7 ? 'high' : complexityScore > 0.4 ? 'medium' : 'low' }; logSh(` 📊 Complexité: ${complexity.level} (score: ${complexityScore.toFixed(2)})`, 'DEBUG'); logSh(` 📝 ${totalWords} mots, ${totalSentences} phrases, diversité: ${lexicalDiversity.toFixed(2)}`, 'DEBUG'); return complexity; } /** * CALCUL SCORE LISIBILITÉ * Approximation de l'index Flesch-Kincaid adapté au français * @param {string} text - Texte à analyser * @returns {number} - Score lisibilité (0-1) */ function calculateReadabilityScore(text) { if (!text || text.trim().length === 0) { return 0; } // Nettoyage du texte const cleanText = text.replace(/[^\w\s.!?]/gi, ''); // Comptages de base const sentences = cleanText.split(/[.!?]+/).filter(s => s.trim().length > 0); const words = cleanText.split(/\s+/).filter(w => w.length > 0); const syllables = countSyllables(cleanText); if (sentences.length === 0 || words.length === 0) { return 0; } // Métriques Flesch-Kincaid adaptées français const avgWordsPerSentence = words.length / sentences.length; const avgSyllablesPerWord = syllables / words.length; // Formule adaptée (plus clémente que l'originale) const fleschScore = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllablesPerWord); // Normalisation 0-1 (100 = parfait en Flesch) const normalizedScore = Math.max(0, Math.min(1, fleschScore / 100)); logSh(` 📖 Lisibilité: ${normalizedScore.toFixed(2)} (mots/phrase: ${avgWordsPerSentence.toFixed(1)}, syll/mot: ${avgSyllablesPerWord.toFixed(1)})`, 'DEBUG'); return normalizedScore; } /** * COMPTAGE SYLLABES (APPROXIMATIF FRANÇAIS) */ function countSyllables(text) { // Approximation pour le français const vowels = /[aeiouyàáâäèéêëìíîïòóôöùúûü]/gi; const vowelGroups = text.match(vowels) || []; // Approximation: 1 groupe de voyelles ≈ 1 syllabe // Ajustements pour le français let syllables = vowelGroups.length; // Corrections courantes const corrections = [ { pattern: /ion/gi, adjustment: 0 }, // "tion" = 1 syllabe, pas 2 { pattern: /ieu/gi, adjustment: -1 }, // "ieux" = 1 syllabe { pattern: /eau/gi, adjustment: -1 }, // "eau" = 1 syllabe { pattern: /ai/gi, adjustment: -1 }, // "ai" = 1 syllabe { pattern: /ou/gi, adjustment: -1 }, // "ou" = 1 syllabe { pattern: /e$/gi, adjustment: -0.5 } // "e" final muet ]; corrections.forEach(correction => { const matches = text.match(correction.pattern) || []; syllables += matches.length * correction.adjustment; }); return Math.max(1, Math.round(syllables)); } /** * PRÉSERVATION MOTS-CLÉS * @param {string} originalText - Texte original * @param {string} modifiedText - Texte modifié * @returns {number} - Score préservation (0-1) */ function preserveKeywords(originalText, modifiedText) { if (!originalText || !modifiedText) { return 0; } const originalLower = originalText.toLowerCase(); const modifiedLower = modifiedText.toLowerCase(); // Extraire mots-clés du texte original const originalKeywords = extractKeywords(originalLower); // Vérifier préservation let preservedCount = 0; let criticalPreservedCount = 0; let criticalTotalCount = 0; originalKeywords.forEach(keyword => { const isCritical = CRITICAL_KEYWORDS.some(ck => keyword.toLowerCase().includes(ck.toLowerCase()) || ck.toLowerCase().includes(keyword.toLowerCase()) ); if (isCritical) { criticalTotalCount++; } // Vérifier présence dans texte modifié const keywordRegex = new RegExp(`\\b${keyword}\\b`, 'gi'); if (modifiedLower.match(keywordRegex)) { preservedCount++; if (isCritical) { criticalPreservedCount++; } } }); // Score avec bonus pour mots-clés critiques const basicPreservation = preservedCount / Math.max(1, originalKeywords.length); const criticalPreservation = criticalTotalCount > 0 ? criticalPreservedCount / criticalTotalCount : 1.0; const finalScore = (basicPreservation * 0.6) + (criticalPreservation * 0.4); logSh(` 🔑 Mots-clés: ${preservedCount}/${originalKeywords.length} préservés (${criticalPreservedCount}/${criticalTotalCount} critiques)`, 'DEBUG'); logSh(` 🎯 Score préservation: ${finalScore.toFixed(2)}`, 'DEBUG'); return finalScore; } /** * EXTRACTION MOTS-CLÉS SIMPLES */ function extractKeywords(text) { // Mots de plus de 3 caractères, non vides const words = text.match(/\b\w{4,}\b/g) || []; // Filtrer mots courants français const stopWords = [ 'avec', 'dans', 'pour', 'cette', 'sont', 'tout', 'mais', 'plus', 'très', 'bien', 'encore', 'aussi', 'comme', 'après', 'avant', 'entre', 'depuis' ]; const keywords = words .filter(word => !stopWords.includes(word.toLowerCase())) .filter((word, index, array) => array.indexOf(word) === index) // Unique .slice(0, 20); // Limiter à 20 mots-clés return keywords; } /** * VALIDATION QUALITÉ SIMULATION * @param {string} originalContent - Contenu original * @param {string} simulatedContent - Contenu simulé * @param {number} qualityThreshold - Seuil qualité minimum * @returns {object} - Résultat validation */ function validateSimulationQuality(originalContent, simulatedContent, qualityThreshold = 0.7) { if (!originalContent || !simulatedContent) { return { acceptable: false, reason: 'Contenu manquant' }; } logSh('🎯 Validation qualité simulation', 'DEBUG'); // Métriques de qualité const readabilityScore = calculateReadabilityScore(simulatedContent); const keywordScore = preserveKeywords(originalContent, simulatedContent); const similarityScore = calculateSimilarity(originalContent, simulatedContent); // Score global pondéré const globalScore = ( readabilityScore * 0.4 + keywordScore * 0.4 + (similarityScore > QUALITY_THRESHOLDS.similarity.minimum && similarityScore < QUALITY_THRESHOLDS.similarity.maximum ? 0.2 : 0) ); const acceptable = globalScore >= qualityThreshold; const validation = { acceptable, globalScore, readabilityScore, keywordScore, similarityScore, reason: acceptable ? 'Qualité acceptable' : determineQualityIssue(readabilityScore, keywordScore, similarityScore), details: { readabilityOk: readabilityScore >= QUALITY_THRESHOLDS.readability.minimum, keywordsOk: keywordScore >= QUALITY_THRESHOLDS.keywordPreservation.minimum, similarityOk: similarityScore >= QUALITY_THRESHOLDS.similarity.minimum && similarityScore <= QUALITY_THRESHOLDS.similarity.maximum } }; logSh(` 🎯 Validation: ${acceptable ? 'ACCEPTÉ' : 'REJETÉ'} (score: ${globalScore.toFixed(2)})`, acceptable ? 'INFO' : 'WARNING'); logSh(` 📊 Lisibilité: ${readabilityScore.toFixed(2)} | Mots-clés: ${keywordScore.toFixed(2)} | Similarité: ${similarityScore.toFixed(2)}`, 'DEBUG'); return validation; } /** * CALCUL SIMILARITÉ APPROXIMATIVE */ function calculateSimilarity(text1, text2) { // Similarité basée sur les mots partagés (simple mais efficace) const words1 = new Set(text1.toLowerCase().split(/\s+/)); const words2 = new Set(text2.toLowerCase().split(/\s+/)); const intersection = new Set([...words1].filter(word => words2.has(word))); const union = new Set([...words1, ...words2]); return intersection.size / union.size; } /** * DÉTERMINER PROBLÈME QUALITÉ */ function determineQualityIssue(readabilityScore, keywordScore, similarityScore) { if (readabilityScore < QUALITY_THRESHOLDS.readability.minimum) { return 'Lisibilité insuffisante'; } if (keywordScore < QUALITY_THRESHOLDS.keywordPreservation.minimum) { return 'Mots-clés mal préservés'; } if (similarityScore < QUALITY_THRESHOLDS.similarity.minimum) { return 'Trop différent de l\'original'; } if (similarityScore > QUALITY_THRESHOLDS.similarity.maximum) { return 'Pas assez modifié'; } return 'Score global insuffisant'; } /** * GÉNÉRATION RAPPORT QUALITÉ DÉTAILLÉ * @param {object} content - Contenu à analyser * @param {object} simulationStats - Stats simulation * @returns {object} - Rapport détaillé */ function generateQualityReport(content, simulationStats) { const report = { timestamp: new Date().toISOString(), contentAnalysis: analyzeContentComplexity(content), simulationStats, qualityMetrics: {}, recommendations: [] }; // Analyse par élément Object.entries(content).forEach(([key, elementContent]) => { if (typeof elementContent === 'string') { const readability = calculateReadabilityScore(elementContent); const complexity = analyzeContentComplexity({ [key]: elementContent }); report.qualityMetrics[key] = { readability, complexity: complexity.complexityScore, wordCount: elementContent.split(/\s+/).length }; } }); // Recommandations automatiques if (report.contentAnalysis.complexityScore > 0.8) { report.recommendations.push('Simplifier le vocabulaire pour améliorer la lisibilité'); } if (simulationStats.fatigueModifications < 1) { report.recommendations.push('Augmenter l\'intensité de simulation fatigue'); } return report; } /** * HELPERS STATISTIQUES */ function calculateStatistics(values) { const sorted = values.slice().sort((a, b) => a - b); const length = values.length; return { mean: values.reduce((sum, val) => sum + val, 0) / length, median: length % 2 === 0 ? (sorted[length / 2 - 1] + sorted[length / 2]) / 2 : sorted[Math.floor(length / 2)], min: sorted[0], max: sorted[length - 1], stdDev: calculateStandardDeviation(values) }; } function calculateStandardDeviation(values) { const mean = values.reduce((sum, val) => sum + val, 0) / values.length; const squaredDifferences = values.map(val => Math.pow(val - mean, 2)); const variance = squaredDifferences.reduce((sum, val) => sum + val, 0) / values.length; return Math.sqrt(variance); } // ============= EXPORTS ============= module.exports = { analyzeContentComplexity, calculateReadabilityScore, preserveKeywords, validateSimulationQuality, generateQualityReport, calculateStatistics, calculateStandardDeviation, countSyllables, extractKeywords, calculateSimilarity, determineQualityIssue, QUALITY_THRESHOLDS, CRITICAL_KEYWORDS }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/human-simulation/HumanSimulationCore.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: HumanSimulationCore.js // RESPONSABILITÉ: Orchestrateur principal Human Simulation // Niveau 5: Temporal & Personality Injection // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { calculateFatigue, injectFatigueMarkers, getFatigueProfile } = require('./FatiguePatterns'); const { injectPersonalityErrors, getPersonalityErrorPatterns } = require('./PersonalityErrors'); const { applyTemporalStyle, getTemporalStyle } = require('./TemporalStyles'); const { analyzeContentComplexity, calculateReadabilityScore, preserveKeywords, validateSimulationQuality } = require('./HumanSimulationUtils'); /** * CONFIGURATION PAR DÉFAUT */ const DEFAULT_CONFIG = { fatigueEnabled: true, personalityErrorsEnabled: true, temporalStyleEnabled: true, imperfectionIntensity: 0.8, // FIXÉ: Plus d'intensité (était 0.3) naturalRepetitions: true, qualityThreshold: 0.4, // FIXÉ: Seuil plus bas (était 0.7) maxModificationsPerElement: 5 // FIXÉ: Plus de modifs possibles (était 3) }; /** * ORCHESTRATEUR PRINCIPAL - Human Simulation Layer * @param {object} content - Contenu généré à simuler * @param {object} options - Options de simulation * @returns {object} - { content, stats, fallback } */ async function applyHumanSimulationLayer(content, options = {}) { return await tracer.run('HumanSimulationCore.applyHumanSimulationLayer()', async () => { const startTime = Date.now(); await tracer.annotate({ contentKeys: Object.keys(content).length, elementIndex: options.elementIndex, totalElements: options.totalElements, currentHour: options.currentHour, personality: options.csvData?.personality?.nom }); logSh(`🧠 HUMAN SIMULATION - Début traitement`, 'INFO'); logSh(` 📊 ${Object.keys(content).length} éléments | Position: ${options.elementIndex}/${options.totalElements}`, 'DEBUG'); try { // Configuration fusionnée const config = { ...DEFAULT_CONFIG, ...options }; // Stats de simulation const simulationStats = { elementsProcessed: 0, fatigueModifications: 0, personalityModifications: 0, temporalModifications: 0, totalModifications: 0, qualityScore: 0, fallbackUsed: false }; // Contenu simulé let simulatedContent = { ...content }; // ======================================== // 1. ANALYSE CONTEXTE GLOBAL // ======================================== const globalContext = await analyzeGlobalContext(content, config); logSh(` 🔍 Contexte: fatigue=${globalContext.fatigueLevel.toFixed(2)}, heure=${globalContext.currentHour}h, personnalité=${globalContext.personalityName}`, 'DEBUG'); // ======================================== // 2. TRAITEMENT PAR ÉLÉMENT // ======================================== for (const [elementKey, elementContent] of Object.entries(content)) { await tracer.run(`HumanSimulation.processElement(${elementKey})`, async () => { logSh(` 🎯 Traitement élément: ${elementKey}`, 'DEBUG'); let processedContent = elementContent; let elementModifications = 0; try { // 2a. Simulation Fatigue Cognitive if (config.fatigueEnabled && globalContext.fatigueLevel > 0.1) { // FIXÉ: Seuil plus bas (était 0.3) const fatigueResult = await applyFatigueSimulation(processedContent, globalContext, config); processedContent = fatigueResult.content; elementModifications += fatigueResult.modifications; simulationStats.fatigueModifications += fatigueResult.modifications; logSh(` 💤 Fatigue: ${fatigueResult.modifications} modifications (niveau: ${globalContext.fatigueLevel.toFixed(2)})`, 'DEBUG'); } // 2b. Erreurs Personnalité if (config.personalityErrorsEnabled && globalContext.personalityProfile) { const personalityResult = await applyPersonalitySimulation(processedContent, globalContext, config); processedContent = personalityResult.content; elementModifications += personalityResult.modifications; simulationStats.personalityModifications += personalityResult.modifications; logSh(` 🎭 Personnalité: ${personalityResult.modifications} erreurs injectées`, 'DEBUG'); } // 2c. Style Temporel if (config.temporalStyleEnabled && globalContext.temporalStyle) { const temporalResult = await applyTemporalSimulation(processedContent, globalContext, config); processedContent = temporalResult.content; elementModifications += temporalResult.modifications; simulationStats.temporalModifications += temporalResult.modifications; logSh(` ⏰ Temporel: ${temporalResult.modifications} ajustements (${globalContext.temporalStyle.period})`, 'DEBUG'); } // 2d. Validation Qualité const qualityCheck = validateSimulationQuality(elementContent, processedContent, config.qualityThreshold); if (qualityCheck.acceptable) { simulatedContent[elementKey] = processedContent; simulationStats.elementsProcessed++; simulationStats.totalModifications += elementModifications; logSh(` ✅ Élément simulé: ${elementModifications} modifications totales`, 'DEBUG'); } else { // Fallback: garder contenu original simulatedContent[elementKey] = elementContent; simulationStats.fallbackUsed = true; logSh(` ⚠️ Qualité insuffisante, fallback vers contenu original`, 'WARNING'); } } catch (elementError) { logSh(` ❌ Erreur simulation élément ${elementKey}: ${elementError.message}`, 'WARNING'); simulatedContent[elementKey] = elementContent; // Fallback simulationStats.fallbackUsed = true; } }, { elementKey, originalLength: elementContent?.length }); } // ======================================== // 3. CALCUL SCORE QUALITÉ GLOBAL // ======================================== simulationStats.qualityScore = calculateGlobalQualityScore(content, simulatedContent); // ======================================== // 4. RÉSULTATS FINAUX // ======================================== const duration = Date.now() - startTime; const success = simulationStats.elementsProcessed > 0 && !simulationStats.fallbackUsed; logSh(`🧠 HUMAN SIMULATION - Terminé (${duration}ms)`, 'INFO'); logSh(` ✅ ${simulationStats.elementsProcessed}/${Object.keys(content).length} éléments simulés`, 'INFO'); logSh(` 📊 ${simulationStats.fatigueModifications} fatigue | ${simulationStats.personalityModifications} personnalité | ${simulationStats.temporalModifications} temporel`, 'INFO'); logSh(` 🎯 Score qualité: ${simulationStats.qualityScore.toFixed(2)} | Fallback: ${simulationStats.fallbackUsed ? 'OUI' : 'NON'}`, 'INFO'); await tracer.event('Human Simulation terminée', { success, duration, stats: simulationStats }); return { content: simulatedContent, stats: simulationStats, fallback: simulationStats.fallbackUsed, qualityScore: simulationStats.qualityScore, duration }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ HUMAN SIMULATION ÉCHOUÉE (${duration}ms): ${error.message}`, 'ERROR'); await tracer.event('Human Simulation échouée', { error: error.message, duration, contentKeys: Object.keys(content).length }); // Fallback complet return { content, stats: { fallbackUsed: true, error: error.message }, fallback: true, qualityScore: 0, duration }; } }, { contentElements: Object.keys(content).length, elementIndex: options.elementIndex, personality: options.csvData?.personality?.nom }); } /** * ANALYSE CONTEXTE GLOBAL */ async function analyzeGlobalContext(content, config) { const elementIndex = config.elementIndex || 0; const totalElements = config.totalElements || Object.keys(content).length; const currentHour = config.currentHour || new Date().getHours(); const personality = config.csvData?.personality; return { fatigueLevel: calculateFatigue(elementIndex, totalElements), fatigueProfile: personality ? getFatigueProfile(personality.nom) : null, personalityName: personality?.nom || 'unknown', personalityProfile: personality ? getPersonalityErrorPatterns(personality.nom) : null, temporalStyle: getTemporalStyle(currentHour), currentHour, elementIndex, totalElements, contentComplexity: analyzeContentComplexity(content) }; } /** * APPLICATION SIMULATION FATIGUE */ async function applyFatigueSimulation(content, globalContext, config) { const fatigueResult = injectFatigueMarkers(content, globalContext.fatigueLevel, { profile: globalContext.fatigueProfile, intensity: config.imperfectionIntensity }); return { content: fatigueResult.content, modifications: fatigueResult.modifications || 0 }; } /** * APPLICATION SIMULATION PERSONNALITÉ */ async function applyPersonalitySimulation(content, globalContext, config) { const personalityResult = injectPersonalityErrors( content, globalContext.personalityProfile, config.imperfectionIntensity ); return { content: personalityResult.content, modifications: personalityResult.modifications || 0 }; } /** * APPLICATION SIMULATION TEMPORELLE */ async function applyTemporalSimulation(content, globalContext, config) { const temporalResult = applyTemporalStyle(content, globalContext.temporalStyle, { intensity: config.imperfectionIntensity }); return { content: temporalResult.content, modifications: temporalResult.modifications || 0 }; } /** * CALCUL SCORE QUALITÉ GLOBAL */ function calculateGlobalQualityScore(originalContent, simulatedContent) { let totalScore = 0; let elementCount = 0; for (const [key, original] of Object.entries(originalContent)) { const simulated = simulatedContent[key]; if (simulated) { const elementScore = calculateReadabilityScore(simulated) * 0.7 + preserveKeywords(original, simulated) * 0.3; totalScore += elementScore; elementCount++; } } return elementCount > 0 ? totalScore / elementCount : 0; } // ============= EXPORTS ============= module.exports = { applyHumanSimulationLayer, analyzeGlobalContext, applyFatigueSimulation, applyPersonalitySimulation, applyTemporalSimulation, calculateGlobalQualityScore, DEFAULT_CONFIG }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/pattern-breaking/SyntaxVariations.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: SyntaxVariations.js // RESPONSABILITÉ: Variations syntaxiques pour casser patterns LLM // Techniques: découpage, fusion, restructuration phrases // ======================================== const { logSh } = require('../ErrorReporting'); /** * PATTERNS SYNTAXIQUES TYPIQUES LLM À ÉVITER */ const LLM_SYNTAX_PATTERNS = { // Structures trop prévisibles repetitiveStarts: [ /^Il est important de/gi, /^Il convient de/gi, /^Il faut noter que/gi, /^Dans ce contexte/gi, /^Par ailleurs/gi ], // Phrases trop parfaites perfectStructures: [ /^De plus, .+ En outre, .+ Enfin,/gi, /^Premièrement, .+ Deuxièmement, .+ Troisièmement,/gi ], // Longueurs trop régulières (détection pattern) uniformLengths: true // Détecté dynamiquement }; /** * VARIATION STRUCTURES SYNTAXIQUES - FONCTION PRINCIPALE * @param {string} text - Texte à varier * @param {number} intensity - Intensité variation (0-1) * @param {object} options - Options { preserveReadability, maxModifications } * @returns {object} - { content, modifications, stats } */ function varyStructures(text, intensity = 0.3, options = {}) { if (!text || text.trim().length === 0) { return { content: text, modifications: 0 }; } const config = { preserveReadability: true, maxModifications: 3, ...options }; logSh(`📝 Variation syntaxique: intensité ${intensity}, préservation: ${config.preserveReadability}`, 'DEBUG'); let modifiedText = text; let totalModifications = 0; const stats = { sentencesSplit: 0, sentencesMerged: 0, structuresReorganized: 0, repetitiveStartsFixed: 0 }; try { // 1. Analyser structure phrases const sentences = analyzeSentenceStructure(modifiedText); logSh(` 📊 ${sentences.length} phrases analysées`, 'DEBUG'); // 2. Découper phrases longues if (Math.random() < intensity) { const splitResult = splitLongSentences(modifiedText, intensity); modifiedText = splitResult.content; totalModifications += splitResult.modifications; stats.sentencesSplit = splitResult.modifications; } // 3. Fusionner phrases courtes if (Math.random() < intensity * 0.7) { const mergeResult = mergeShorter(modifiedText, intensity); modifiedText = mergeResult.content; totalModifications += mergeResult.modifications; stats.sentencesMerged = mergeResult.modifications; } // 4. Réorganiser structures prévisibles if (Math.random() < intensity * 0.8) { const reorganizeResult = reorganizeStructures(modifiedText, intensity); modifiedText = reorganizeResult.content; totalModifications += reorganizeResult.modifications; stats.structuresReorganized = reorganizeResult.modifications; } // 5. Corriger débuts répétitifs if (Math.random() < intensity * 0.6) { const repetitiveResult = fixRepetitiveStarts(modifiedText); modifiedText = repetitiveResult.content; totalModifications += repetitiveResult.modifications; stats.repetitiveStartsFixed = repetitiveResult.modifications; } // 6. Limitation sécurité if (totalModifications > config.maxModifications) { logSh(` ⚠️ Limitation appliquée: ${totalModifications} → ${config.maxModifications} modifications`, 'DEBUG'); totalModifications = config.maxModifications; } logSh(`📝 Syntaxe modifiée: ${totalModifications} changements (${stats.sentencesSplit} splits, ${stats.sentencesMerged} merges)`, 'DEBUG'); } catch (error) { logSh(`❌ Erreur variation syntaxique: ${error.message}`, 'WARNING'); return { content: text, modifications: 0, stats: {} }; } return { content: modifiedText, modifications: totalModifications, stats }; } /** * ANALYSE STRUCTURE PHRASES */ function analyzeSentenceStructure(text) { const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0); return sentences.map((sentence, index) => ({ index, content: sentence.trim(), length: sentence.trim().length, wordCount: sentence.trim().split(/\s+/).length, isLong: sentence.trim().length > 120, isShort: sentence.trim().length < 40, hasComplexStructure: sentence.includes(',') && sentence.includes(' qui ') || sentence.includes(' que ') })); } /** * DÉCOUPAGE PHRASES LONGUES */ function splitLongSentences(text, intensity) { let modified = text; let modifications = 0; const sentences = modified.split('. '); const processedSentences = sentences.map(sentence => { // Phrases longues (>100 chars) et probabilité selon intensité - PLUS AGRESSIF if (sentence.length > 100 && Math.random() < (intensity * 0.6)) { // Points de découpe naturels const cutPoints = [ { pattern: /, qui (.+)/, replacement: '. Celui-ci $1' }, { pattern: /, que (.+)/, replacement: '. Cela $1' }, { pattern: /, dont (.+)/, replacement: '. Celui-ci $1' }, { pattern: / et (.{30,})/, replacement: '. De plus, $1' }, { pattern: /, car (.+)/, replacement: '. En effet, $1' }, { pattern: /, mais (.+)/, replacement: '. Cependant, $1' } ]; for (const cutPoint of cutPoints) { if (sentence.match(cutPoint.pattern)) { const newSentence = sentence.replace(cutPoint.pattern, cutPoint.replacement); if (newSentence !== sentence) { modifications++; logSh(` ✂️ Phrase découpée: ${sentence.length} → ${newSentence.length} chars`, 'DEBUG'); return newSentence; } } } } return sentence; }); return { content: processedSentences.join('. '), modifications }; } /** * FUSION PHRASES COURTES */ function mergeShorter(text, intensity) { let modified = text; let modifications = 0; const sentences = modified.split('. '); const processedSentences = []; for (let i = 0; i < sentences.length; i++) { const current = sentences[i]; const next = sentences[i + 1]; // Si phrase courte (<50 chars) et phrase suivante existe - PLUS AGRESSIF if (current && current.length < 50 && next && next.length < 70 && Math.random() < (intensity * 0.5)) { // Connecteurs pour fusion naturelle const connectors = [', de plus,', ', également,', ', aussi,', ' et']; const connector = connectors[Math.floor(Math.random() * connectors.length)]; const merged = current + connector + ' ' + next.toLowerCase(); processedSentences.push(merged); modifications++; logSh(` 🔗 Phrases fusionnées: ${current.length} + ${next.length} → ${merged.length} chars`, 'DEBUG'); i++; // Passer la phrase suivante car fusionnée } else { processedSentences.push(current); } } return { content: processedSentences.join('. '), modifications }; } /** * RÉORGANISATION STRUCTURES PRÉVISIBLES */ function reorganizeStructures(text, intensity) { let modified = text; let modifications = 0; // Détecter énumérations prévisibles const enumerationPatterns = [ { pattern: /Premièrement, (.+?)\. Deuxièmement, (.+?)\. Troisièmement, (.+?)\./gi, replacement: 'D\'abord, $1. Ensuite, $2. Enfin, $3.' }, { pattern: /D\'une part, (.+?)\. D\'autre part, (.+?)\./gi, replacement: 'Tout d\'abord, $1. Par ailleurs, $2.' }, { pattern: /En premier lieu, (.+?)\. En second lieu, (.+?)\./gi, replacement: 'Dans un premier temps, $1. Puis, $2.' } ]; enumerationPatterns.forEach(pattern => { if (modified.match(pattern.pattern) && Math.random() < intensity) { modified = modified.replace(pattern.pattern, pattern.replacement); modifications++; logSh(` 🔄 Structure réorganisée: énumération variée`, 'DEBUG'); } }); return { content: modified, modifications }; } /** * CORRECTION DÉBUTS RÉPÉTITIFS */ function fixRepetitiveStarts(text) { let modified = text; let modifications = 0; const sentences = modified.split('. '); const startWords = []; // Analyser débuts de phrases sentences.forEach(sentence => { const words = sentence.trim().split(/\s+/); if (words.length > 0) { startWords.push(words[0].toLowerCase()); } }); // Détecter répétitions const startCounts = {}; startWords.forEach(word => { startCounts[word] = (startCounts[word] || 0) + 1; }); // Remplacer débuts répétitifs const alternatives = { 'il': ['Cet élément', 'Cette solution', 'Ce produit'], 'cette': ['Cette option', 'Cette approche', 'Cette méthode'], 'pour': ['Afin de', 'Dans le but de', 'En vue de'], 'avec': ['Grâce à', 'Au moyen de', 'En utilisant'], 'dans': ['Au sein de', 'À travers', 'Parmi'] }; const processedSentences = sentences.map(sentence => { const firstWord = sentence.trim().split(/\s+/)[0]?.toLowerCase(); if (firstWord && startCounts[firstWord] > 2 && alternatives[firstWord] && Math.random() < 0.4) { const replacement = alternatives[firstWord][Math.floor(Math.random() * alternatives[firstWord].length)]; const newSentence = sentence.replace(/^\w+/, replacement); modifications++; logSh(` 🔄 Début varié: "${firstWord}" → "${replacement}"`, 'DEBUG'); return newSentence; } return sentence; }); return { content: processedSentences.join('. '), modifications }; } /** * DÉTECTION UNIFORMITÉ LONGUEURS (Pattern LLM) */ function detectUniformLengths(text) { const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0); if (sentences.length < 3) return { uniform: false, variance: 0 }; const lengths = sentences.map(s => s.trim().length); const avgLength = lengths.reduce((sum, len) => sum + len, 0) / lengths.length; // Calculer variance const variance = lengths.reduce((sum, len) => sum + Math.pow(len - avgLength, 2), 0) / lengths.length; const standardDev = Math.sqrt(variance); // Uniformité si écart-type faible par rapport à moyenne const coefficientVariation = standardDev / avgLength; const uniform = coefficientVariation < 0.3; // Seuil arbitraire return { uniform, variance: coefficientVariation, avgLength, standardDev, sentenceCount: sentences.length }; } /** * AJOUT VARIATIONS MICRO-SYNTAXIQUES */ function addMicroVariations(text, intensity) { let modified = text; let modifications = 0; // Micro-variations subtiles const microPatterns = [ { from: /\btrès (.+?)\b/g, to: 'particulièrement $1', probability: 0.3 }, { from: /\bassez (.+?)\b/g, to: 'plutôt $1', probability: 0.4 }, { from: /\bbeaucoup de/g, to: 'de nombreux', probability: 0.3 }, { from: /\bpermets de/g, to: 'permet de', probability: 0.8 }, // Correction fréquente { from: /\bien effet\b/g, to: 'effectivement', probability: 0.2 } ]; microPatterns.forEach(pattern => { if (Math.random() < (intensity * pattern.probability)) { const before = modified; modified = modified.replace(pattern.from, pattern.to); if (modified !== before) { modifications++; logSh(` 🔧 Micro-variation: ${pattern.from} → ${pattern.to}`, 'DEBUG'); } } }); return { content: modified, modifications }; } // ============= EXPORTS ============= module.exports = { varyStructures, splitLongSentences, mergeShorter, reorganizeStructures, fixRepetitiveStarts, analyzeSentenceStructure, detectUniformLengths, addMicroVariations, LLM_SYNTAX_PATTERNS }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/pattern-breaking/LLMFingerprints.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: LLMFingerprints.js // RESPONSABILITÉ: Remplacement mots et expressions typiques LLM // Identification et remplacement des "fingerprints" IA // ======================================== const { logSh } = require('../ErrorReporting'); /** * MOTS ET EXPRESSIONS TYPIQUES LLM À REMPLACER * Classés par niveau de suspicion et fréquence d'usage LLM */ const LLM_FINGERPRINTS = { // ======================================== // NIVEAU CRITIQUE - Très suspects // ======================================== critical: { adjectives: [ { word: 'comprehensive', alternatives: ['complet', 'détaillé', 'approfondi', 'exhaustif'], suspicion: 0.95 }, { word: 'robust', alternatives: ['solide', 'fiable', 'résistant', 'durable'], suspicion: 0.92 }, { word: 'seamless', alternatives: ['fluide', 'harmonieux', 'sans accroc', 'naturel'], suspicion: 0.90 }, { word: 'optimal', alternatives: ['idéal', 'parfait', 'excellent', 'adapté'], suspicion: 0.88 }, { word: 'cutting-edge', alternatives: ['innovant', 'moderne', 'récent', 'avancé'], suspicion: 0.87 }, { word: 'state-of-the-art', alternatives: ['dernier cri', 'moderne', 'récent'], suspicion: 0.95 } ], expressions: [ { phrase: 'il est important de noter que', alternatives: ['remarquons que', 'signalons que', 'précisons que'], suspicion: 0.85 }, { phrase: 'dans le paysage actuel', alternatives: ['actuellement', 'de nos jours', 'aujourd\'hui'], suspicion: 0.82 }, { phrase: 'il convient de souligner', alternatives: ['il faut noter', 'soulignons', 'remarquons'], suspicion: 0.80 }, { phrase: 'en fin de compte', alternatives: ['finalement', 'au final', 'pour conclure'], suspicion: 0.75 } ] }, // ======================================== // NIVEAU ÉLEVÉ - Souvent suspects // ======================================== high: { adjectives: [ { word: 'innovative', alternatives: ['novateur', 'créatif', 'original', 'moderne'], suspicion: 0.75 }, { word: 'efficient', alternatives: ['efficace', 'performant', 'rapide', 'pratique'], suspicion: 0.70 }, { word: 'versatile', alternatives: ['polyvalent', 'adaptable', 'flexible', 'modulable'], suspicion: 0.68 }, { word: 'sophisticated', alternatives: ['raffiné', 'élaboré', 'avancé', 'complexe'], suspicion: 0.65 }, { word: 'compelling', alternatives: ['convaincant', 'captivant', 'intéressant'], suspicion: 0.72 } ], verbs: [ { word: 'leverage', alternatives: ['utiliser', 'exploiter', 'tirer parti de', 'employer'], suspicion: 0.80 }, { word: 'optimize', alternatives: ['améliorer', 'perfectionner', 'ajuster'], suspicion: 0.65 }, { word: 'streamline', alternatives: ['simplifier', 'rationaliser', 'organiser'], suspicion: 0.75 }, { word: 'enhance', alternatives: ['améliorer', 'enrichir', 'renforcer'], suspicion: 0.60 } ], expressions: [ { phrase: 'par ailleurs', alternatives: ['de plus', 'également', 'aussi', 'en outre'], suspicion: 0.65 }, { phrase: 'en outre', alternatives: ['de plus', 'également', 'aussi'], suspicion: 0.70 }, { phrase: 'cela dit', alternatives: ['néanmoins', 'toutefois', 'cependant'], suspicion: 0.60 } ] }, // ======================================== // NIVEAU MODÉRÉ - Parfois suspects // ======================================== moderate: { adjectives: [ { word: 'significant', alternatives: ['important', 'notable', 'considérable', 'marquant'], suspicion: 0.55 }, { word: 'essential', alternatives: ['indispensable', 'crucial', 'vital', 'nécessaire'], suspicion: 0.50 }, { word: 'comprehensive', alternatives: ['complet', 'global', 'détaillé'], suspicion: 0.58 }, { word: 'effective', alternatives: ['efficace', 'performant', 'réussi'], suspicion: 0.45 } ], expressions: [ { phrase: 'il est essentiel de', alternatives: ['il faut', 'il importe de', 'il est crucial de'], suspicion: 0.55 }, { phrase: 'dans cette optique', alternatives: ['dans cette perspective', 'ainsi', 'de ce fait'], suspicion: 0.52 }, { phrase: 'à cet égard', alternatives: ['sur ce point', 'concernant cela', 'à ce propos'], suspicion: 0.48 } ] } }; /** * PATTERNS STRUCTURELS LLM */ const STRUCTURAL_PATTERNS = { // Débuts de phrases trop formels formalStarts: [ /^Il est important de souligner que/gi, /^Il convient de noter que/gi, /^Il est essentiel de comprendre que/gi, /^Dans ce contexte, il est crucial de/gi, /^Il est primordial de/gi ], // Transitions trop parfaites perfectTransitions: [ /\. Par ailleurs, (.+?)\. En outre, (.+?)\. De plus,/gi, /\. Premièrement, (.+?)\. Deuxièmement, (.+?)\. Troisièmement,/gi ], // Conclusions trop formelles formalConclusions: [ /En conclusion, il apparaît clairement que/gi, /Pour conclure, il est évident que/gi, /En définitive, nous pouvons affirmer que/gi ] }; /** * DÉTECTION PATTERNS LLM DANS LE TEXTE * @param {string} text - Texte à analyser * @returns {object} - { count, patterns, suspicionScore } */ function detectLLMPatterns(text) { if (!text || text.trim().length === 0) { return { count: 0, patterns: [], suspicionScore: 0 }; } const detectedPatterns = []; let totalSuspicion = 0; let wordCount = text.split(/\s+/).length; // Analyser tous les niveaux de fingerprints Object.entries(LLM_FINGERPRINTS).forEach(([level, categories]) => { Object.entries(categories).forEach(([category, items]) => { items.forEach(item => { const regex = new RegExp(`\\b${item.word || item.phrase}\\b`, 'gi'); const matches = text.match(regex); if (matches) { detectedPatterns.push({ pattern: item.word || item.phrase, type: category, level: level, count: matches.length, suspicion: item.suspicion, alternatives: item.alternatives }); totalSuspicion += item.suspicion * matches.length; } }); }); }); // Analyser patterns structurels Object.entries(STRUCTURAL_PATTERNS).forEach(([patternType, patterns]) => { patterns.forEach(pattern => { const matches = text.match(pattern); if (matches) { detectedPatterns.push({ pattern: pattern.source, type: 'structural', level: 'high', count: matches.length, suspicion: 0.80 }); totalSuspicion += 0.80 * matches.length; } }); }); const suspicionScore = wordCount > 0 ? totalSuspicion / wordCount : 0; logSh(`🔍 Patterns LLM détectés: ${detectedPatterns.length} (score suspicion: ${suspicionScore.toFixed(3)})`, 'DEBUG'); return { count: detectedPatterns.length, patterns: detectedPatterns.map(p => p.pattern), detailedPatterns: detectedPatterns, suspicionScore, recommendation: suspicionScore > 0.05 ? 'replacement' : 'minor_cleanup' }; } /** * REMPLACEMENT FINGERPRINTS LLM * @param {string} text - Texte à traiter * @param {object} options - Options { intensity, preserveContext, maxReplacements } * @returns {object} - { content, replacements, details } */ function replaceLLMFingerprints(text, options = {}) { if (!text || text.trim().length === 0) { return { content: text, replacements: 0 }; } const config = { intensity: 0.5, preserveContext: true, maxReplacements: 5, ...options }; logSh(`🤖 Remplacement fingerprints LLM: intensité ${config.intensity}`, 'DEBUG'); let modifiedText = text; let totalReplacements = 0; const replacementDetails = []; try { // Détecter d'abord les patterns const detection = detectLLMPatterns(modifiedText); if (detection.count === 0) { logSh(` ✅ Aucun fingerprint LLM détecté`, 'DEBUG'); return { content: text, replacements: 0, details: [] }; } // Traiter par niveau de priorité const priorities = ['critical', 'high', 'moderate']; for (const priority of priorities) { if (totalReplacements >= config.maxReplacements) break; const categoryData = LLM_FINGERPRINTS[priority]; if (!categoryData) continue; // Traiter chaque catégorie Object.entries(categoryData).forEach(([category, items]) => { items.forEach(item => { if (totalReplacements >= config.maxReplacements) return; const searchTerm = item.word || item.phrase; const regex = new RegExp(`\\b${searchTerm}\\b`, 'gi'); // Probabilité de remplacement basée sur suspicion et intensité const replacementProbability = item.suspicion * config.intensity; if (modifiedText.match(regex) && Math.random() < replacementProbability) { // Choisir alternative aléatoire const alternative = item.alternatives[Math.floor(Math.random() * item.alternatives.length)]; const beforeText = modifiedText; modifiedText = modifiedText.replace(regex, alternative); if (modifiedText !== beforeText) { totalReplacements++; replacementDetails.push({ original: searchTerm, replacement: alternative, category, level: priority, suspicion: item.suspicion }); logSh(` 🔄 Remplacé "${searchTerm}" → "${alternative}" (suspicion: ${item.suspicion})`, 'DEBUG'); } } }); }); } // Traitement patterns structurels if (totalReplacements < config.maxReplacements) { const structuralResult = replaceStructuralPatterns(modifiedText, config.intensity); modifiedText = structuralResult.content; totalReplacements += structuralResult.replacements; replacementDetails.push(...structuralResult.details); } logSh(`🤖 Fingerprints remplacés: ${totalReplacements} modifications`, 'DEBUG'); } catch (error) { logSh(`❌ Erreur remplacement fingerprints: ${error.message}`, 'WARNING'); return { content: text, replacements: 0, details: [] }; } return { content: modifiedText, replacements: totalReplacements, details: replacementDetails }; } /** * REMPLACEMENT PATTERNS STRUCTURELS */ function replaceStructuralPatterns(text, intensity) { let modified = text; let replacements = 0; const details = []; // Débuts formels → versions plus naturelles const formalStartReplacements = [ { from: /^Il est important de souligner que (.+)/gim, to: 'Notons que $1', name: 'début formel' }, { from: /^Il convient de noter que (.+)/gim, to: 'Précisons que $1', name: 'formulation convient' }, { from: /^Dans ce contexte, il est crucial de (.+)/gim, to: 'Il faut $1', name: 'contexte crucial' } ]; formalStartReplacements.forEach(replacement => { if (Math.random() < intensity * 0.7) { const before = modified; modified = modified.replace(replacement.from, replacement.to); if (modified !== before) { replacements++; details.push({ original: replacement.name, replacement: 'version naturelle', category: 'structural', level: 'high', suspicion: 0.80 }); logSh(` 🏗️ Pattern structurel remplacé: ${replacement.name}`, 'DEBUG'); } } }); return { content: modified, replacements, details }; } /** * ANALYSE DENSITÉ FINGERPRINTS */ function analyzeFingerprintDensity(text) { const detection = detectLLMPatterns(text); const wordCount = text.split(/\s+/).length; const density = detection.count / wordCount; const riskLevel = density > 0.08 ? 'high' : density > 0.04 ? 'medium' : 'low'; return { fingerprintCount: detection.count, wordCount, density, riskLevel, suspicionScore: detection.suspicionScore, recommendation: riskLevel === 'high' ? 'immediate_replacement' : riskLevel === 'medium' ? 'selective_replacement' : 'minimal_cleanup' }; } /** * SUGGESTIONS CONTEXTUELLES */ function generateContextualAlternatives(word, context, personality) { // Adapter selon personnalité si fournie if (personality) { const personalityAdaptations = { 'marc': { 'optimal': 'efficace', 'robust': 'solide', 'comprehensive': 'complet' }, 'sophie': { 'optimal': 'parfait', 'robust': 'résistant', 'comprehensive': 'détaillé' }, 'kevin': { 'optimal': 'nickel', 'robust': 'costaud', 'comprehensive': 'complet' } }; const adaptations = personalityAdaptations[personality.toLowerCase()]; if (adaptations && adaptations[word]) { return [adaptations[word]]; } } // Suggestions contextuelles basiques const contextualMappings = { 'optimal': context.includes('solution') ? ['idéale', 'parfaite'] : ['excellent', 'adapté'], 'robust': context.includes('système') ? ['fiable', 'stable'] : ['solide', 'résistant'], 'comprehensive': context.includes('analyse') ? ['approfondie', 'détaillée'] : ['complète', 'globale'] }; return contextualMappings[word] || ['standard']; } // ============= EXPORTS ============= module.exports = { detectLLMPatterns, replaceLLMFingerprints, replaceStructuralPatterns, analyzeFingerprintDensity, generateContextualAlternatives, LLM_FINGERPRINTS, STRUCTURAL_PATTERNS }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/pattern-breaking/NaturalConnectors.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: NaturalConnectors.js // RESPONSABILITÉ: Humanisation des connecteurs et transitions // Remplacement connecteurs formels par versions naturelles // ======================================== const { logSh } = require('../ErrorReporting'); /** * CONNECTEURS FORMELS LLM À HUMANISER */ const FORMAL_CONNECTORS = { // Connecteurs trop formels/académiques formal: [ { connector: 'par ailleurs', alternatives: ['aussi', 'également', 'de plus', 'en plus'], suspicion: 0.75 }, { connector: 'en outre', alternatives: ['de plus', 'également', 'aussi', 'en plus'], suspicion: 0.80 }, { connector: 'de surcroît', alternatives: ['de plus', 'aussi', 'en plus'], suspicion: 0.85 }, { connector: 'qui plus est', alternatives: ['en plus', 'et puis', 'aussi'], suspicion: 0.80 }, { connector: 'par conséquent', alternatives: ['donc', 'alors', 'du coup', 'résultat'], suspicion: 0.70 }, { connector: 'en conséquence', alternatives: ['donc', 'alors', 'du coup'], suspicion: 0.75 }, { connector: 'néanmoins', alternatives: ['mais', 'pourtant', 'cependant', 'malgré ça'], suspicion: 0.65 }, { connector: 'toutefois', alternatives: ['mais', 'pourtant', 'cependant'], suspicion: 0.70 } ], // Débuts de phrases formels formalStarts: [ { phrase: 'il convient de noter que', alternatives: ['notons que', 'remarquons que', 'précisons que'], suspicion: 0.90 }, { phrase: 'il est important de souligner que', alternatives: ['soulignons que', 'notons que', 'précisons que'], suspicion: 0.85 }, { phrase: 'il est à noter que', alternatives: ['notons que', 'signalons que', 'précisons que'], suspicion: 0.80 }, { phrase: 'il convient de préciser que', alternatives: ['précisons que', 'ajoutons que', 'notons que'], suspicion: 0.75 }, { phrase: 'dans ce contexte', alternatives: ['ici', 'dans ce cas', 'alors'], suspicion: 0.70 } ], // Transitions artificielles artificialTransitions: [ { phrase: 'abordons maintenant', alternatives: ['passons à', 'voyons', 'parlons de'], suspicion: 0.75 }, { phrase: 'examinons à présent', alternatives: ['voyons', 'regardons', 'passons à'], suspicion: 0.80 }, { phrase: 'intéressons-nous désormais à', alternatives: ['voyons', 'parlons de', 'passons à'], suspicion: 0.85 }, { phrase: 'penchons-nous sur', alternatives: ['voyons', 'regardons', 'parlons de'], suspicion: 0.70 } ] }; /** * CONNECTEURS NATURELS PAR CONTEXTE */ const NATURAL_CONNECTORS_BY_CONTEXT = { // Selon le ton/registre souhaité casual: ['du coup', 'alors', 'et puis', 'aussi', 'en fait'], conversational: ['bon', 'eh bien', 'donc', 'alors', 'et puis'], technical: ['donc', 'ainsi', 'alors', 'par là', 'de cette façon'], commercial: ['donc', 'alors', 'ainsi', 'de plus', 'aussi'] }; /** * HUMANISATION CONNECTEURS ET TRANSITIONS - FONCTION PRINCIPALE * @param {string} text - Texte à humaniser * @param {object} options - Options { intensity, preserveMeaning, maxReplacements } * @returns {object} - { content, replacements, details } */ function humanizeTransitions(text, options = {}) { if (!text || text.trim().length === 0) { return { content: text, replacements: 0 }; } const config = { intensity: 0.6, preserveMeaning: true, maxReplacements: 4, tone: 'casual', // casual, conversational, technical, commercial ...options }; logSh(`🔗 Humanisation connecteurs: intensité ${config.intensity}, ton ${config.tone}`, 'DEBUG'); let modifiedText = text; let totalReplacements = 0; const replacementDetails = []; try { // 1. Remplacer connecteurs formels const connectorsResult = replaceFormalConnectors(modifiedText, config); modifiedText = connectorsResult.content; totalReplacements += connectorsResult.replacements; replacementDetails.push(...connectorsResult.details); // 2. Humaniser débuts de phrases if (totalReplacements < config.maxReplacements) { const startsResult = humanizeFormalStarts(modifiedText, config); modifiedText = startsResult.content; totalReplacements += startsResult.replacements; replacementDetails.push(...startsResult.details); } // 3. Remplacer transitions artificielles if (totalReplacements < config.maxReplacements) { const transitionsResult = replaceArtificialTransitions(modifiedText, config); modifiedText = transitionsResult.content; totalReplacements += transitionsResult.replacements; replacementDetails.push(...transitionsResult.details); } // 4. Ajouter variabilité contextuelle if (totalReplacements < config.maxReplacements) { const contextResult = addContextualVariability(modifiedText, config); modifiedText = contextResult.content; totalReplacements += contextResult.replacements; replacementDetails.push(...contextResult.details); } logSh(`🔗 Connecteurs humanisés: ${totalReplacements} remplacements effectués`, 'DEBUG'); } catch (error) { logSh(`❌ Erreur humanisation connecteurs: ${error.message}`, 'WARNING'); return { content: text, replacements: 0, details: [] }; } return { content: modifiedText, replacements: totalReplacements, details: replacementDetails }; } /** * REMPLACEMENT CONNECTEURS FORMELS */ function replaceFormalConnectors(text, config) { let modified = text; let replacements = 0; const details = []; FORMAL_CONNECTORS.formal.forEach(connector => { if (replacements >= Math.floor(config.maxReplacements / 2)) return; const regex = new RegExp(`\\b${connector.connector}\\b`, 'gi'); const matches = modified.match(regex); if (matches && Math.random() < (config.intensity * connector.suspicion)) { // Choisir alternative selon contexte/ton const availableAlts = connector.alternatives; const contextualAlts = NATURAL_CONNECTORS_BY_CONTEXT[config.tone] || []; // Préférer alternatives contextuelles si disponibles const preferredAlts = availableAlts.filter(alt => contextualAlts.includes(alt)); const finalAlts = preferredAlts.length > 0 ? preferredAlts : availableAlts; const chosen = finalAlts[Math.floor(Math.random() * finalAlts.length)]; const beforeText = modified; modified = modified.replace(regex, chosen); if (modified !== beforeText) { replacements++; details.push({ original: connector.connector, replacement: chosen, type: 'formal_connector', suspicion: connector.suspicion }); logSh(` 🔄 Connecteur formalisé: "${connector.connector}" → "${chosen}"`, 'DEBUG'); } } }); return { content: modified, replacements, details }; } /** * HUMANISATION DÉBUTS DE PHRASES FORMELS */ function humanizeFormalStarts(text, config) { let modified = text; let replacements = 0; const details = []; FORMAL_CONNECTORS.formalStarts.forEach(start => { if (replacements >= Math.floor(config.maxReplacements / 3)) return; const regex = new RegExp(start.phrase, 'gi'); if (modified.match(regex) && Math.random() < (config.intensity * start.suspicion)) { const alternative = start.alternatives[Math.floor(Math.random() * start.alternatives.length)]; const beforeText = modified; modified = modified.replace(regex, alternative); if (modified !== beforeText) { replacements++; details.push({ original: start.phrase, replacement: alternative, type: 'formal_start', suspicion: start.suspicion }); logSh(` 🚀 Début formalisé: "${start.phrase}" → "${alternative}"`, 'DEBUG'); } } }); return { content: modified, replacements, details }; } /** * REMPLACEMENT TRANSITIONS ARTIFICIELLES */ function replaceArtificialTransitions(text, config) { let modified = text; let replacements = 0; const details = []; FORMAL_CONNECTORS.artificialTransitions.forEach(transition => { if (replacements >= Math.floor(config.maxReplacements / 4)) return; const regex = new RegExp(transition.phrase, 'gi'); if (modified.match(regex) && Math.random() < (config.intensity * transition.suspicion * 0.8)) { const alternative = transition.alternatives[Math.floor(Math.random() * transition.alternatives.length)]; const beforeText = modified; modified = modified.replace(regex, alternative); if (modified !== beforeText) { replacements++; details.push({ original: transition.phrase, replacement: alternative, type: 'artificial_transition', suspicion: transition.suspicion }); logSh(` 🌉 Transition artificialisée: "${transition.phrase}" → "${alternative}"`, 'DEBUG'); } } }); return { content: modified, replacements, details }; } /** * AJOUT VARIABILITÉ CONTEXTUELLE */ function addContextualVariability(text, config) { let modified = text; let replacements = 0; const details = []; // Connecteurs génériques à contextualiser selon le ton const genericPatterns = [ { from: /\bet puis\b/g, contextual: true }, { from: /\bdone\b/g, contextual: true }, { from: /\bainsi\b/g, contextual: true } ]; const contextualReplacements = NATURAL_CONNECTORS_BY_CONTEXT[config.tone] || NATURAL_CONNECTORS_BY_CONTEXT.casual; genericPatterns.forEach(pattern => { if (replacements >= 2) return; if (pattern.contextual && Math.random() < (config.intensity * 0.4)) { const matches = modified.match(pattern.from); if (matches && contextualReplacements.length > 0) { const replacement = contextualReplacements[Math.floor(Math.random() * contextualReplacements.length)]; // Éviter remplacements identiques if (replacement !== matches[0]) { const beforeText = modified; modified = modified.replace(pattern.from, replacement); if (modified !== beforeText) { replacements++; details.push({ original: matches[0], replacement, type: 'contextual_variation', suspicion: 0.4 }); logSh(` 🎯 Variation contextuelle: "${matches[0]}" → "${replacement}"`, 'DEBUG'); } } } } }); return { content: modified, replacements, details }; } /** * DÉTECTION CONNECTEURS FORMELS DANS TEXTE */ function detectFormalConnectors(text) { if (!text || text.trim().length === 0) { return { count: 0, connectors: [], suspicionScore: 0 }; } const detectedConnectors = []; let totalSuspicion = 0; // Vérifier tous les types de connecteurs formels Object.values(FORMAL_CONNECTORS).flat().forEach(item => { const searchTerm = item.connector || item.phrase; const regex = new RegExp(`\\b${searchTerm}\\b`, 'gi'); const matches = text.match(regex); if (matches) { detectedConnectors.push({ connector: searchTerm, count: matches.length, suspicion: item.suspicion, alternatives: item.alternatives }); totalSuspicion += item.suspicion * matches.length; } }); const wordCount = text.split(/\s+/).length; const suspicionScore = wordCount > 0 ? totalSuspicion / wordCount : 0; logSh(`🔍 Connecteurs formels détectés: ${detectedConnectors.length} (score: ${suspicionScore.toFixed(3)})`, 'DEBUG'); return { count: detectedConnectors.length, connectors: detectedConnectors.map(c => c.connector), detailedConnectors: detectedConnectors, suspicionScore, recommendation: suspicionScore > 0.03 ? 'humanize' : 'minimal_changes' }; } /** * ANALYSE DENSITÉ CONNECTEURS FORMELS */ function analyzeConnectorFormality(text) { const detection = detectFormalConnectors(text); const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0); const density = detection.count / sentences.length; const formalityLevel = density > 0.4 ? 'high' : density > 0.2 ? 'medium' : 'low'; return { connectorsCount: detection.count, sentenceCount: sentences.length, density, formalityLevel, suspicionScore: detection.suspicionScore, recommendation: formalityLevel === 'high' ? 'extensive_humanization' : formalityLevel === 'medium' ? 'selective_humanization' : 'minimal_humanization' }; } // ============= EXPORTS ============= module.exports = { humanizeTransitions, replaceFormalConnectors, humanizeFormalStarts, replaceArtificialTransitions, addContextualVariability, detectFormalConnectors, analyzeConnectorFormality, FORMAL_CONNECTORS, NATURAL_CONNECTORS_BY_CONTEXT }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/pattern-breaking/PatternBreakingCore.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: PatternBreakingCore.js // RESPONSABILITÉ: Orchestrateur principal Pattern Breaking // Niveau 2: Casser les patterns syntaxiques typiques des LLMs // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { varyStructures, splitLongSentences, mergeShorter } = require('./SyntaxVariations'); const { replaceLLMFingerprints, detectLLMPatterns } = require('./LLMFingerprints'); const { humanizeTransitions, replaceConnectors } = require('./NaturalConnectors'); /** * CONFIGURATION MODULAIRE AGRESSIVE PATTERN BREAKING * Chaque feature peut être activée/désactivée individuellement */ const DEFAULT_CONFIG = { // ======================================== // CONTRÔLES GLOBAUX // ======================================== intensityLevel: 0.8, // Intensité globale (0-1) - PLUS AGRESSIVE preserveReadability: true, // Maintenir lisibilité maxModificationsPerElement: 8, // Limite modifications par élément - DOUBLÉE qualityThreshold: 0.5, // Seuil qualité minimum - ABAISSÉ // ======================================== // FEATURES SYNTAXE & STRUCTURE // ======================================== syntaxVariationEnabled: true, // Variations syntaxiques de base aggressiveSentenceSplitting: true, // Découpage phrases plus agressif (<80 chars) aggressiveSentenceMerging: true, // Fusion phrases courtes (<60 chars) microSyntaxVariations: true, // Micro-variations subtiles questionInjection: true, // Injection questions rhétoriques // ======================================== // FEATURES LLM FINGERPRINTS // ======================================== llmFingerprintReplacement: true, // Remplacement fingerprints de base frenchLLMPatterns: true, // Patterns spécifiques français overlyFormalVocabulary: true, // Vocabulaire trop formel → casual repetitiveStarters: true, // Débuts de phrases répétitifs perfectTransitions: true, // Transitions trop parfaites // ======================================== // FEATURES CONNECTEURS & TRANSITIONS // ======================================== naturalConnectorsEnabled: true, // Connecteurs naturels de base casualConnectors: true, // Connecteurs très casual (genre, enfin, bref) hesitationMarkers: true, // Marqueurs d'hésitation (..., euh) colloquialTransitions: true, // Transitions colloquiales // ======================================== // FEATURES IMPERFECTIONS HUMAINES // ======================================== humanImperfections: true, // Système d'imperfections humaines vocabularyRepetitions: true, // Répétitions vocabulaire naturelles casualizationIntensive: true, // Casualisation intensive naturalHesitations: true, // Hésitations naturelles en fin de phrase informalExpressions: true, // Expressions informelles ("pas mal", "sympa") // ======================================== // FEATURES RESTRUCTURATION // ======================================== intelligentRestructuring: true, // Restructuration intelligente paragraphBreaking: true, // Cassage paragraphes longs listToTextConversion: true, // Listes → texte naturel redundancyInjection: true, // Injection redondances naturelles // ======================================== // FEATURES SPÉCIALISÉES // ======================================== personalityAdaptation: true, // Adaptation selon personnalité temporalConsistency: true, // Cohérence temporelle (maintenant/aujourd'hui) contextualVocabulary: true, // Vocabulaire contextuel registerVariation: true // Variation registre langue }; /** * ORCHESTRATEUR PRINCIPAL - Pattern Breaking Layer * @param {object} content - Contenu généré à traiter * @param {object} options - Options de pattern breaking * @returns {object} - { content, stats, fallback } */ async function applyPatternBreakingLayer(content, options = {}) { return await tracer.run('PatternBreakingCore.applyPatternBreakingLayer()', async () => { const startTime = Date.now(); await tracer.annotate({ contentKeys: Object.keys(content).length, intensityLevel: options.intensityLevel, personality: options.csvData?.personality?.nom }); logSh(`🔧 PATTERN BREAKING - Début traitement`, 'INFO'); logSh(` 📊 ${Object.keys(content).length} éléments | Intensité: ${options.intensityLevel || DEFAULT_CONFIG.intensityLevel}`, 'DEBUG'); try { // Configuration fusionnée const config = { ...DEFAULT_CONFIG, ...options }; // Stats de pattern breaking const patternStats = { elementsProcessed: 0, syntaxModifications: 0, llmFingerprintReplacements: 0, connectorReplacements: 0, totalModifications: 0, fallbackUsed: false, patternsDetected: 0 }; // Contenu traité let processedContent = { ...content }; // ======================================== // TRAITEMENT PAR ÉLÉMENT // ======================================== for (const [elementKey, elementContent] of Object.entries(content)) { await tracer.run(`PatternBreaking.processElement(${elementKey})`, async () => { logSh(` 🎯 Traitement élément: ${elementKey}`, 'DEBUG'); let currentContent = elementContent; let elementModifications = 0; try { // 1. Détection patterns LLM const detectedPatterns = detectLLMPatterns(currentContent); patternStats.patternsDetected += detectedPatterns.count; if (detectedPatterns.count > 0) { logSh(` 🔍 ${detectedPatterns.count} patterns LLM détectés: ${detectedPatterns.patterns.slice(0, 3).join(', ')}`, 'DEBUG'); } // 2. SYNTAXE & STRUCTURE - Couche de base if (config.syntaxVariationEnabled) { const syntaxResult = await applySyntaxVariation(currentContent, config); currentContent = syntaxResult.content; elementModifications += syntaxResult.modifications; patternStats.syntaxModifications += syntaxResult.modifications; logSh(` 📝 Syntaxe: ${syntaxResult.modifications} variations appliquées`, 'DEBUG'); } // 3. SYNTAXE AGRESSIVE - Couche intensive if (config.aggressiveSentenceSplitting || config.aggressiveSentenceMerging) { const aggressiveResult = await applyAggressiveSyntax(currentContent, config); currentContent = aggressiveResult.content; elementModifications += aggressiveResult.modifications; patternStats.syntaxModifications += aggressiveResult.modifications; logSh(` ✂️ Syntaxe agressive: ${aggressiveResult.modifications} modifications`, 'DEBUG'); } // 4. MICRO-VARIATIONS - Subtiles mais importantes if (config.microSyntaxVariations) { const microResult = await applyMicroVariations(currentContent, config); currentContent = microResult.content; elementModifications += microResult.modifications; patternStats.syntaxModifications += microResult.modifications; logSh(` 🔧 Micro-variations: ${microResult.modifications} ajustements`, 'DEBUG'); } // 5. LLM FINGERPRINTS - Détection de base if (config.llmFingerprintReplacement && detectedPatterns.count > 0) { const fingerprintResult = await applyLLMFingerprints(currentContent, config); currentContent = fingerprintResult.content; elementModifications += fingerprintResult.modifications; patternStats.llmFingerprintReplacements += fingerprintResult.modifications; logSh(` 🤖 LLM Fingerprints: ${fingerprintResult.modifications} remplacements`, 'DEBUG'); } // 6. PATTERNS FRANÇAIS - Spécifique langue française if (config.frenchLLMPatterns) { const frenchResult = await applyFrenchPatterns(currentContent, config); currentContent = frenchResult.content; elementModifications += frenchResult.modifications; patternStats.llmFingerprintReplacements += frenchResult.modifications; logSh(` 🇫🇷 Patterns français: ${frenchResult.modifications} corrections`, 'DEBUG'); } // 7. VOCABULAIRE FORMEL - Casualisation if (config.overlyFormalVocabulary) { const casualResult = await applyCasualization(currentContent, config); currentContent = casualResult.content; elementModifications += casualResult.modifications; patternStats.llmFingerprintReplacements += casualResult.modifications; logSh(` 😎 Casualisation: ${casualResult.modifications} simplifications`, 'DEBUG'); } // 8. CONNECTEURS NATURELS - Base if (config.naturalConnectorsEnabled) { const connectorResult = await applyNaturalConnectors(currentContent, config); currentContent = connectorResult.content; elementModifications += connectorResult.modifications; patternStats.connectorReplacements += connectorResult.modifications; logSh(` 🔗 Connecteurs naturels: ${connectorResult.modifications} humanisés`, 'DEBUG'); } // 9. CONNECTEURS CASUAL - Très familier if (config.casualConnectors) { const casualConnResult = await applyCasualConnectors(currentContent, config); currentContent = casualConnResult.content; elementModifications += casualConnResult.modifications; patternStats.connectorReplacements += casualConnResult.modifications; logSh(` 🗣️ Connecteurs casual: ${casualConnResult.modifications} familiarisés`, 'DEBUG'); } // 10. IMPERFECTIONS HUMAINES - Système principal if (config.humanImperfections) { const imperfResult = await applyHumanImperfections(currentContent, config); currentContent = imperfResult.content; elementModifications += imperfResult.modifications; patternStats.totalModifications += imperfResult.modifications; logSh(` 👤 Imperfections: ${imperfResult.modifications} humanisations`, 'DEBUG'); } // 11. QUESTIONS RHÉTORIQUES - Engagement if (config.questionInjection) { const questionResult = await applyQuestionInjection(currentContent, config); currentContent = questionResult.content; elementModifications += questionResult.modifications; patternStats.totalModifications += questionResult.modifications; logSh(` ❓ Questions: ${questionResult.modifications} injections`, 'DEBUG'); } // 12. RESTRUCTURATION INTELLIGENTE - Dernière couche if (config.intelligentRestructuring) { const restructResult = await applyIntelligentRestructuring(currentContent, config); currentContent = restructResult.content; elementModifications += restructResult.modifications; patternStats.totalModifications += restructResult.modifications; logSh(` 🧠 Restructuration: ${restructResult.modifications} réorganisations`, 'DEBUG'); } // 5. Validation qualité const qualityCheck = validatePatternBreakingQuality(elementContent, currentContent, config.qualityThreshold); if (qualityCheck.acceptable) { processedContent[elementKey] = currentContent; patternStats.elementsProcessed++; patternStats.totalModifications += elementModifications; logSh(` ✅ Élément traité: ${elementModifications} modifications totales`, 'DEBUG'); } else { // Fallback: garder contenu original processedContent[elementKey] = elementContent; patternStats.fallbackUsed = true; logSh(` ⚠️ Qualité insuffisante, fallback vers contenu original`, 'WARNING'); } } catch (elementError) { logSh(` ❌ Erreur pattern breaking élément ${elementKey}: ${elementError.message}`, 'WARNING'); processedContent[elementKey] = elementContent; // Fallback patternStats.fallbackUsed = true; } }, { elementKey, originalLength: elementContent?.length }); } // ======================================== // RÉSULTATS FINAUX // ======================================== const duration = Date.now() - startTime; const success = patternStats.elementsProcessed > 0 && !patternStats.fallbackUsed; logSh(`🔧 PATTERN BREAKING - Terminé (${duration}ms)`, 'INFO'); logSh(` ✅ ${patternStats.elementsProcessed}/${Object.keys(content).length} éléments traités`, 'INFO'); logSh(` 📊 ${patternStats.syntaxModifications} syntaxe | ${patternStats.llmFingerprintReplacements} fingerprints | ${patternStats.connectorReplacements} connecteurs`, 'INFO'); logSh(` 🎯 Patterns détectés: ${patternStats.patternsDetected} | Fallback: ${patternStats.fallbackUsed ? 'OUI' : 'NON'}`, 'INFO'); await tracer.event('Pattern Breaking terminé', { success, duration, stats: patternStats }); return { content: processedContent, stats: patternStats, fallback: patternStats.fallbackUsed, duration }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ PATTERN BREAKING ÉCHOUÉ (${duration}ms): ${error.message}`, 'ERROR'); await tracer.event('Pattern Breaking échoué', { error: error.message, duration, contentKeys: Object.keys(content).length }); // Fallback complet return { content, stats: { fallbackUsed: true, error: error.message }, fallback: true, duration }; } }, { contentElements: Object.keys(content).length, intensityLevel: options.intensityLevel }); } /** * APPLICATION VARIATION SYNTAXIQUE */ async function applySyntaxVariation(content, config) { const syntaxResult = varyStructures(content, config.intensityLevel, { preserveReadability: config.preserveReadability, maxModifications: Math.floor(config.maxModificationsPerElement / 2) }); return { content: syntaxResult.content, modifications: syntaxResult.modifications || 0 }; } /** * APPLICATION REMPLACEMENT LLM FINGERPRINTS */ async function applyLLMFingerprints(content, config) { const fingerprintResult = replaceLLMFingerprints(content, { intensity: config.intensityLevel, preserveContext: true, maxReplacements: Math.floor(config.maxModificationsPerElement / 2) }); return { content: fingerprintResult.content, modifications: fingerprintResult.replacements || 0 }; } /** * APPLICATION CONNECTEURS NATURELS */ async function applyNaturalConnectors(content, config) { const connectorResult = humanizeTransitions(content, { intensity: config.intensityLevel, preserveMeaning: true, maxReplacements: Math.floor(config.maxModificationsPerElement / 2) }); return { content: connectorResult.content, modifications: connectorResult.replacements || 0 }; } /** * VALIDATION QUALITÉ PATTERN BREAKING */ function validatePatternBreakingQuality(originalContent, processedContent, qualityThreshold) { if (!originalContent || !processedContent) { return { acceptable: false, reason: 'Contenu manquant' }; } // Métriques de base const lengthDiff = Math.abs(processedContent.length - originalContent.length) / originalContent.length; const wordCountOriginal = originalContent.split(/\s+/).length; const wordCountProcessed = processedContent.split(/\s+/).length; const wordCountDiff = Math.abs(wordCountProcessed - wordCountOriginal) / wordCountOriginal; // Vérifications qualité const checks = { lengthPreserved: lengthDiff < 0.3, // Pas plus de 30% de différence wordCountPreserved: wordCountDiff < 0.2, // Pas plus de 20% de différence noEmpty: processedContent.trim().length > 0, // Pas de contenu vide readableStructure: processedContent.includes('.') // Structure lisible }; const passedChecks = Object.values(checks).filter(Boolean).length; const score = passedChecks / Object.keys(checks).length; const acceptable = score >= qualityThreshold; logSh(` 🎯 Validation Pattern Breaking: ${acceptable ? 'ACCEPTÉ' : 'REJETÉ'} (score: ${score.toFixed(2)})`, acceptable ? 'DEBUG' : 'WARNING'); return { acceptable, score, checks, reason: acceptable ? 'Qualité acceptable' : 'Score qualité insuffisant' }; } /** * APPLICATION SYNTAXE AGRESSIVE * Seuils plus bas pour plus de transformations */ async function applyAggressiveSyntax(content, config) { let modified = content; let modifications = 0; // Découpage agressif phrases longues (>80 chars au lieu de >120) if (config.aggressiveSentenceSplitting) { const sentences = modified.split('. '); const processedSentences = sentences.map(sentence => { if (sentence.length > 80 && Math.random() < (config.intensityLevel * 0.7)) { const cutPoints = [ { pattern: /, qui (.+)/, replacement: '. Celui-ci $1' }, { pattern: /, que (.+)/, replacement: '. Cette solution $1' }, { pattern: /, car (.+)/, replacement: '. En fait, $1' }, { pattern: /, donc (.+)/, replacement: '. Du coup, $1' }, { pattern: / et (.{20,})/, replacement: '. Aussi, $1' }, { pattern: /, mais (.+)/, replacement: '. Par contre, $1' } ]; for (const cutPoint of cutPoints) { if (sentence.match(cutPoint.pattern)) { modifications++; return sentence.replace(cutPoint.pattern, cutPoint.replacement); } } } return sentence; }); modified = processedSentences.join('. '); } // Fusion agressive phrases courtes (<60 chars au lieu de <40) if (config.aggressiveSentenceMerging) { const sentences = modified.split('. '); const processedSentences = []; for (let i = 0; i < sentences.length; i++) { const current = sentences[i]; const next = sentences[i + 1]; if (current && current.length < 60 && next && next.length < 80 && Math.random() < (config.intensityLevel * 0.5)) { const connectors = [', du coup,', ', genre,', ', enfin,', ' et puis']; const connector = connectors[Math.floor(Math.random() * connectors.length)]; processedSentences.push(current + connector + ' ' + next.toLowerCase()); modifications++; i++; // Skip next sentence } else { processedSentences.push(current); } } modified = processedSentences.join('. '); } return { content: modified, modifications }; } /** * APPLICATION MICRO-VARIATIONS * Changements subtiles mais nombreux */ async function applyMicroVariations(content, config) { let modified = content; let modifications = 0; const microPatterns = [ // Intensificateurs { from: /\btrès (.+?)\b/g, to: 'super $1', probability: 0.4 }, { from: /\bassez (.+?)\b/g, to: 'plutôt $1', probability: 0.5 }, { from: /\bextrêmement\b/g, to: 'vraiment', probability: 0.6 }, // Connecteurs basiques { from: /\bainsi\b/g, to: 'du coup', probability: 0.4 }, { from: /\bpar conséquent\b/g, to: 'donc', probability: 0.7 }, { from: /\bcependant\b/g, to: 'mais', probability: 0.3 }, // Formulations casual { from: /\bde cette manière\b/g, to: 'comme ça', probability: 0.5 }, { from: /\bafin de\b/g, to: 'pour', probability: 0.4 }, { from: /\ben vue de\b/g, to: 'pour', probability: 0.6 } ]; microPatterns.forEach(pattern => { if (Math.random() < (config.intensityLevel * pattern.probability)) { const before = modified; modified = modified.replace(pattern.from, pattern.to); if (modified !== before) modifications++; } }); return { content: modified, modifications }; } /** * APPLICATION PATTERNS FRANÇAIS SPÉCIFIQUES * Détection patterns français typiques LLM */ async function applyFrenchPatterns(content, config) { let modified = content; let modifications = 0; // Patterns français typiques LLM const frenchPatterns = [ // Expressions trop soutenues { from: /\bil convient de noter que\b/gi, to: 'on peut dire que', probability: 0.8 }, { from: /\bil est important de souligner que\b/gi, to: 'c\'est important de voir que', probability: 0.8 }, { from: /\bdans ce contexte\b/gi, to: 'là-dessus', probability: 0.6 }, { from: /\bpar ailleurs\b/gi, to: 'sinon', probability: 0.5 }, { from: /\ben outre\b/gi, to: 'aussi', probability: 0.7 }, // Formulations administratives { from: /\bil s'avère que\b/gi, to: 'en fait', probability: 0.6 }, { from: /\btoutefois\b/gi, to: 'par contre', probability: 0.5 }, { from: /\bnéanmoins\b/gi, to: 'quand même', probability: 0.7 } ]; frenchPatterns.forEach(pattern => { if (Math.random() < (config.intensityLevel * pattern.probability)) { const before = modified; modified = modified.replace(pattern.from, pattern.to); if (modified !== before) modifications++; } }); return { content: modified, modifications }; } /** * APPLICATION CASUALISATION INTENSIVE * Rendre le vocabulaire plus décontracté */ async function applyCasualization(content, config) { let modified = content; let modifications = 0; const casualizations = [ // Verbes formels → casual { from: /\boptimiser\b/gi, to: 'améliorer', probability: 0.7 }, { from: /\beffectuer\b/gi, to: 'faire', probability: 0.8 }, { from: /\bréaliser\b/gi, to: 'faire', probability: 0.6 }, { from: /\bmettre en œuvre\b/gi, to: 'faire', probability: 0.7 }, // Adjectifs formels → casual { from: /\bexceptionnel\b/gi, to: 'super', probability: 0.4 }, { from: /\bremarquable\b/gi, to: 'pas mal', probability: 0.5 }, { from: /\bconsidérable\b/gi, to: 'important', probability: 0.6 }, { from: /\bsubstantiel\b/gi, to: 'important', probability: 0.8 }, // Expressions formelles → casual { from: /\bde manière significative\b/gi, to: 'pas mal', probability: 0.6 }, { from: /\ben définitive\b/gi, to: 'au final', probability: 0.7 }, { from: /\bdans l'ensemble\b/gi, to: 'globalement', probability: 0.5 } ]; casualizations.forEach(casual => { if (Math.random() < (config.intensityLevel * casual.probability)) { const before = modified; modified = modified.replace(casual.from, casual.to); if (modified !== before) modifications++; } }); return { content: modified, modifications }; } /** * APPLICATION CONNECTEURS CASUAL * Connecteurs très familiers */ async function applyCasualConnectors(content, config) { let modified = content; let modifications = 0; const casualConnectors = [ { from: /\. De plus,/g, to: '. Genre,', probability: 0.3 }, { from: /\. En outre,/g, to: '. Puis,', probability: 0.4 }, { from: /\. Par ailleurs,/g, to: '. Sinon,', probability: 0.3 }, { from: /\. Cependant,/g, to: '. Mais bon,', probability: 0.4 }, { from: /\. Néanmoins,/g, to: '. Ceci dit,', probability: 0.5 }, { from: /\. Ainsi,/g, to: '. Du coup,', probability: 0.6 } ]; casualConnectors.forEach(connector => { if (Math.random() < (config.intensityLevel * connector.probability)) { const before = modified; modified = modified.replace(connector.from, connector.to); if (modified !== before) modifications++; } }); return { content: modified, modifications }; } /** * APPLICATION IMPERFECTIONS HUMAINES * Injection d'imperfections réalistes */ async function applyHumanImperfections(content, config) { let modified = content; let modifications = 0; // Répétitions vocabulaire if (config.vocabularyRepetitions && Math.random() < (config.intensityLevel * 0.4)) { const repetitionWords = ['vraiment', 'bien', 'assez', 'plutôt', 'super']; const word = repetitionWords[Math.floor(Math.random() * repetitionWords.length)]; const sentences = modified.split('. '); if (sentences.length > 2) { sentences[1] = word + ' ' + sentences[1]; modified = sentences.join('. '); modifications++; } } // Hésitations naturelles if (config.naturalHesitations && Math.random() < (config.intensityLevel * 0.2)) { const hesitations = ['... enfin', '... disons', '... bon']; const hesitation = hesitations[Math.floor(Math.random() * hesitations.length)]; const words = modified.split(' '); const insertPos = Math.floor(words.length * 0.6); words.splice(insertPos, 0, hesitation); modified = words.join(' '); modifications++; } // Expressions informelles if (config.informalExpressions && Math.random() < (config.intensityLevel * 0.3)) { const informalReplacements = [ { from: /\bc'est bien\b/gi, to: 'c\'est sympa', probability: 0.4 }, { from: /\bc'est intéressant\b/gi, to: 'c\'est pas mal', probability: 0.5 }, { from: /\bc'est efficace\b/gi, to: 'ça marche bien', probability: 0.4 } ]; informalReplacements.forEach(replacement => { if (Math.random() < replacement.probability) { const before = modified; modified = modified.replace(replacement.from, replacement.to); if (modified !== before) modifications++; } }); } return { content: modified, modifications }; } /** * APPLICATION QUESTIONS RHÉTORIQUES * Injection questions pour engagement */ async function applyQuestionInjection(content, config) { let modified = content; let modifications = 0; if (Math.random() < (config.intensityLevel * 0.3)) { const sentences = modified.split('. '); if (sentences.length > 3) { const questionTemplates = [ 'Mais pourquoi est-ce important ?', 'Comment faire alors ?', 'Que faut-il retenir ?', 'Est-ce vraiment efficace ?' ]; const question = questionTemplates[Math.floor(Math.random() * questionTemplates.length)]; const insertPos = Math.floor(sentences.length / 2); sentences.splice(insertPos, 0, question); modified = sentences.join('. '); modifications++; } } return { content: modified, modifications }; } /** * APPLICATION RESTRUCTURATION INTELLIGENTE * Réorganisation structure générale */ async function applyIntelligentRestructuring(content, config) { let modified = content; let modifications = 0; // Cassage paragraphes longs if (config.paragraphBreaking && modified.length > 400) { const sentences = modified.split('. '); if (sentences.length > 6) { const breakPoint = Math.floor(sentences.length / 2); sentences[breakPoint] = sentences[breakPoint] + '\n\n'; modified = sentences.join('. '); modifications++; } } // Injection redondances naturelles if (config.redundancyInjection && Math.random() < (config.intensityLevel * 0.2)) { const redundancies = ['comme je le disais', 'encore une fois', 'je le répète']; const redundancy = redundancies[Math.floor(Math.random() * redundancies.length)]; const sentences = modified.split('. '); if (sentences.length > 2) { sentences[sentences.length - 2] = redundancy + ', ' + sentences[sentences.length - 2]; modified = sentences.join('. '); modifications++; } } return { content: modified, modifications }; } // ============= EXPORTS ============= module.exports = { applyPatternBreakingLayer, applySyntaxVariation, applyLLMFingerprints, applyNaturalConnectors, validatePatternBreakingQuality, applyAggressiveSyntax, applyMicroVariations, applyFrenchPatterns, applyCasualization, applyCasualConnectors, applyHumanImperfections, applyQuestionInjection, applyIntelligentRestructuring, DEFAULT_CONFIG }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/pipeline/PipelineExecutor.js │ └────────────────────────────────────────────────────────────────────┘ */ /** * PipelineExecutor.js * * Moteur d'exécution des pipelines modulaires flexibles. * Orchestre l'exécution séquentielle des modules avec gestion d'état. */ const { logSh, tracer } = require('../ErrorReporting'); const { PipelineDefinition } = require('./PipelineDefinition'); const { getPersonalities, readInstructionsData, selectPersonalityWithAI } = require('../BrainConfig'); // Modules d'exécution const { generateSimpleContent } = require('../selective-enhancement/SelectiveUtils'); const { SelectiveCore } = require('../selective-enhancement/SelectiveCore'); const { AdversarialCore } = require('../adversarial-generation/AdversarialCore'); const { HumanSimulationCore } = require('../human-simulation/HumanSimulationCore'); const { PatternBreakingCore } = require('../pattern-breaking/PatternBreakingCore'); /** * Classe PipelineExecutor */ class PipelineExecutor { constructor() { this.currentContent = null; this.executionLog = []; this.checkpoints = []; this.metadata = { startTime: null, endTime: null, totalDuration: 0, personality: null }; } /** * Exécute un pipeline complet */ async execute(pipelineConfig, rowNumber, options = {}) { return tracer.run('PipelineExecutor.execute', async () => { logSh(`🚀 Démarrage pipeline "${pipelineConfig.name}" (${pipelineConfig.pipeline.length} étapes)`, 'INFO'); // Validation const validation = PipelineDefinition.validate(pipelineConfig); if (!validation.valid) { throw new Error(`Pipeline invalide: ${validation.errors.join(', ')}`); } this.metadata.startTime = Date.now(); this.executionLog = []; this.checkpoints = []; // Charger les données const csvData = await this.loadData(rowNumber); // Exécuter les étapes const enabledSteps = pipelineConfig.pipeline.filter(s => s.enabled !== false); for (let i = 0; i < enabledSteps.length; i++) { const step = enabledSteps[i]; try { logSh(`▶ Étape ${step.step}/${pipelineConfig.pipeline.length}: ${step.module} (${step.mode})`, 'INFO'); const stepStartTime = Date.now(); const result = await this.executeStep(step, csvData, options); const stepDuration = Date.now() - stepStartTime; // Log l'étape this.executionLog.push({ step: step.step, module: step.module, mode: step.mode, intensity: step.intensity, duration: stepDuration, modifications: result.modifications || 0, success: true, timestamp: new Date().toISOString() }); // Mise à jour du contenu if (result.content) { this.currentContent = result.content; } // Checkpoint si demandé if (step.saveCheckpoint) { this.checkpoints.push({ step: step.step, content: this.currentContent, timestamp: new Date().toISOString() }); logSh(`💾 Checkpoint sauvegardé (étape ${step.step})`, 'DEBUG'); } logSh(`✔ Étape ${step.step} terminée (${stepDuration}ms, ${result.modifications || 0} modifs)`, 'INFO'); } catch (error) { logSh(`✖ Erreur étape ${step.step}: ${error.message}`, 'ERROR'); this.executionLog.push({ step: step.step, module: step.module, mode: step.mode, success: false, error: error.message, timestamp: new Date().toISOString() }); // Propager l'erreur ou continuer selon options if (options.stopOnError !== false) { throw error; } } } this.metadata.endTime = Date.now(); this.metadata.totalDuration = this.metadata.endTime - this.metadata.startTime; logSh(`✅ Pipeline terminé: ${this.metadata.totalDuration}ms`, 'INFO'); return { success: true, finalContent: this.currentContent, executionLog: this.executionLog, checkpoints: this.checkpoints, metadata: { ...this.metadata, pipelineName: pipelineConfig.name, totalSteps: enabledSteps.length, successfulSteps: this.executionLog.filter(l => l.success).length } }; }, { pipelineName: pipelineConfig.name, rowNumber }); } /** * Charge les données depuis Google Sheets */ async loadData(rowNumber) { return tracer.run('PipelineExecutor.loadData', async () => { const csvData = await readInstructionsData(rowNumber); // Charger personnalité si besoin const personalities = await getPersonalities(); const personality = await selectPersonalityWithAI( csvData.mc0, csvData.t0, personalities ); csvData.personality = personality; this.metadata.personality = personality.nom; logSh(`📊 Données chargées: ${csvData.mc0}, personnalité: ${personality.nom}`, 'DEBUG'); return csvData; }, { rowNumber }); } /** * Exécute une étape individuelle */ async executeStep(step, csvData, options) { return tracer.run(`PipelineExecutor.executeStep.${step.module}`, async () => { switch (step.module) { case 'generation': return await this.runGeneration(step, csvData); case 'selective': return await this.runSelective(step, csvData); case 'adversarial': return await this.runAdversarial(step, csvData); case 'human': return await this.runHumanSimulation(step, csvData); case 'pattern': return await this.runPatternBreaking(step, csvData); default: throw new Error(`Module inconnu: ${step.module}`); } }, { step: step.step, module: step.module, mode: step.mode }); } /** * Exécute la génération initiale */ async runGeneration(step, csvData) { return tracer.run('PipelineExecutor.runGeneration', async () => { if (this.currentContent) { logSh('⚠️ Contenu déjà généré, génération ignorée', 'WARN'); return { content: this.currentContent, modifications: 0 }; } // Génération simple avec SelectiveUtils const result = await generateSimpleContent( csvData, csvData.personality, { source: 'pipeline_executor' } ); logSh(`✓ Génération: ${Object.keys(result).length} éléments créés`, 'DEBUG'); return { content: result, modifications: Object.keys(result).length }; }, { mode: step.mode }); } /** * Exécute l'enhancement sélectif */ async runSelective(step, csvData) { return tracer.run('PipelineExecutor.runSelective', async () => { if (!this.currentContent) { throw new Error('Aucun contenu à améliorer. Génération requise avant selective enhancement'); } const selectiveCore = new SelectiveCore(); // Configuration de la couche const config = { stack: step.mode, intensity: step.intensity || 1.0, ...step.parameters }; const result = await selectiveCore.applyStack( this.currentContent, csvData, csvData.personality, config ); logSh(`✓ Selective: ${result.modificationsCount} modifications`, 'DEBUG'); return { content: result.content, modifications: result.modificationsCount }; }, { mode: step.mode, intensity: step.intensity }); } /** * Exécute l'adversarial generation */ async runAdversarial(step, csvData) { return tracer.run('PipelineExecutor.runAdversarial', async () => { if (!this.currentContent) { throw new Error('Aucun contenu à traiter. Génération requise avant adversarial'); } if (step.mode === 'none') { logSh('Adversarial mode = none, ignoré', 'DEBUG'); return { content: this.currentContent, modifications: 0 }; } const adversarialCore = new AdversarialCore(); const config = { mode: step.mode, detector: step.parameters?.detector || 'general', method: step.parameters?.method || 'regeneration', intensity: step.intensity || 1.0 }; const result = await adversarialCore.applyMode( this.currentContent, csvData, csvData.personality, config ); logSh(`✓ Adversarial: ${result.modificationsCount} modifications`, 'DEBUG'); return { content: result.content, modifications: result.modificationsCount }; }, { mode: step.mode, detector: step.parameters?.detector }); } /** * Exécute la simulation humaine */ async runHumanSimulation(step, csvData) { return tracer.run('PipelineExecutor.runHumanSimulation', async () => { if (!this.currentContent) { throw new Error('Aucun contenu à traiter. Génération requise avant human simulation'); } if (step.mode === 'none') { logSh('Human simulation mode = none, ignoré', 'DEBUG'); return { content: this.currentContent, modifications: 0 }; } const humanCore = new HumanSimulationCore(); const config = { mode: step.mode, intensity: step.intensity || 1.0, fatigueLevel: step.parameters?.fatigueLevel || 0.5, errorRate: step.parameters?.errorRate || 0.3 }; const result = await humanCore.applyMode( this.currentContent, csvData, csvData.personality, config ); logSh(`✓ Human Simulation: ${result.modificationsCount} modifications`, 'DEBUG'); return { content: result.content, modifications: result.modificationsCount }; }, { mode: step.mode, intensity: step.intensity }); } /** * Exécute le pattern breaking */ async runPatternBreaking(step, csvData) { return tracer.run('PipelineExecutor.runPatternBreaking', async () => { if (!this.currentContent) { throw new Error('Aucun contenu à traiter. Génération requise avant pattern breaking'); } if (step.mode === 'none') { logSh('Pattern breaking mode = none, ignoré', 'DEBUG'); return { content: this.currentContent, modifications: 0 }; } const patternCore = new PatternBreakingCore(); const config = { mode: step.mode, intensity: step.intensity || 1.0, focus: step.parameters?.focus || 'both' }; const result = await patternCore.applyMode( this.currentContent, csvData, csvData.personality, config ); logSh(`✓ Pattern Breaking: ${result.modificationsCount} modifications`, 'DEBUG'); return { content: result.content, modifications: result.modificationsCount }; }, { mode: step.mode, intensity: step.intensity }); } /** * Obtient le contenu actuel */ getCurrentContent() { return this.currentContent; } /** * Obtient le log d'exécution */ getExecutionLog() { return this.executionLog; } /** * Obtient les checkpoints sauvegardés */ getCheckpoints() { return this.checkpoints; } /** * Obtient les métadonnées d'exécution */ getMetadata() { return this.metadata; } /** * Reset l'état de l'executor */ reset() { this.currentContent = null; this.executionLog = []; this.checkpoints = []; this.metadata = { startTime: null, endTime: null, totalDuration: 0, personality: null }; } } module.exports = { PipelineExecutor }; /* ┌────────────────────────────────────────────────────────────────────┐ │ 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(/\{([^}]+)\}/); let tagName = nameMatch ? nameMatch[1].trim() : fullMatch.split('{')[0]; // NETTOYAGE: Enlever , du nom du tag tagName = tagName.replace(/<\/?strong>/g, ''); // 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'); } // VALIDATION: Vérifier qu'on a au moins récupéré des résultats (tolérer doublons) const uniqueNames = [...new Set(missingElements.map(e => e.name))]; const parsedCount = Object.keys(results).length; if (parsedCount === 0) { logSh(`❌ FATAL: Aucun mot-clé parsé`, 'ERROR'); throw new Error(`FATAL: Parsing mots-clés échoué complètement - arrêt du workflow`); } // Warning si doublons détectés (mais on continue) if (missingElements.length > uniqueNames.length) { const doublonsCount = missingElements.length - uniqueNames.length; logSh(`⚠️ ${doublonsCount} doublons détectés dans les tags XML (${uniqueNames.length} tags uniques)`, 'WARNING'); } // Vérifier qu'on a au moins autant de résultats que de tags uniques if (parsedCount < uniqueNames.length) { const manquants = uniqueNames.length - parsedCount; logSh(`❌ FATAL: Parsing incomplet - ${manquants}/${uniqueNames.length} tags uniques non parsés`, 'ERROR'); throw new Error(`FATAL: Parsing mots-clés incomplet (${manquants}/${uniqueNames.length} manquants) - arrêt du workflow`); } logSh(`✅ ${parsedCount} mots-clés parsés pour ${uniqueNames.length} tags uniques (${missingElements.length} éléments total)`, 'INFO'); 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/generation/InitialGeneration.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // INITIAL GENERATION LAYER - GÉNÉRATION INITIALE MODULAIRE // Responsabilité: Génération de contenu initial réutilisable // LLM: Claude Sonnet-4 (précision et créativité équilibrée) // ======================================== const { callLLM } = require('../LLMManager'); const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { chunkArray, sleep } = require('../selective-enhancement/SelectiveUtils'); /** * COUCHE GÉNÉRATION INITIALE MODULAIRE */ class InitialGenerationLayer { constructor() { this.name = 'InitialGeneration'; this.defaultLLM = 'claude'; this.priority = 0; // Priorité maximale - appliqué en premier } /** * MAIN METHOD - Générer contenu initial */ async apply(contentStructure, config = {}) { return await tracer.run('InitialGenerationLayer.apply()', async () => { const { llmProvider = this.defaultLLM, temperature = 0.7, csvData = null, context = {} } = config; await tracer.annotate({ initialGeneration: true, llmProvider, temperature, elementsCount: Object.keys(contentStructure).length, mc0: csvData?.mc0 }); const startTime = Date.now(); logSh(`🎯 INITIAL GENERATION: Génération contenu initial (${llmProvider})`, 'INFO'); logSh(` 📊 ${Object.keys(contentStructure).length} éléments à générer`, 'INFO'); try { // Créer les éléments à générer à partir de la structure const elementsToGenerate = this.prepareElementsForGeneration(contentStructure, csvData); // Générer en chunks pour gérer les gros contenus const results = {}; const chunks = chunkArray(Object.entries(elementsToGenerate), 4); // Chunks de 4 pour Claude for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; try { logSh(` 📦 Chunk génération ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); const generationPrompt = this.createInitialGenerationPrompt(chunk, csvData, config); const response = await callLLM(llmProvider, generationPrompt, { temperature, maxTokens: 4000 }, csvData?.personality); const chunkResults = this.parseInitialGenerationResponse(response, chunk); Object.assign(results, chunkResults); logSh(` ✅ Chunk génération ${chunkIndex + 1}: ${Object.keys(chunkResults).length} générés`, 'DEBUG'); // Délai entre chunks if (chunkIndex < chunks.length - 1) { await sleep(2000); } } catch (error) { logSh(` ❌ Chunk génération ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR'); // Fallback: contenu basique chunk.forEach(([tag, instruction]) => { results[tag] = this.createFallbackContent(tag, csvData); }); } } const duration = Date.now() - startTime; const stats = { generated: Object.keys(results).length, total: Object.keys(contentStructure).length, generationRate: (Object.keys(results).length / Math.max(Object.keys(contentStructure).length, 1)) * 100, duration, llmProvider, temperature }; logSh(`✅ INITIAL GENERATION TERMINÉE: ${stats.generated}/${stats.total} générés (${duration}ms)`, 'INFO'); await tracer.event('Initial generation appliquée', stats); return { content: results, stats }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ INITIAL GENERATION ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR'); throw error; } }, { contentStructure: Object.keys(contentStructure), config }); } /** * PRÉPARER ÉLÉMENTS POUR GÉNÉRATION */ prepareElementsForGeneration(contentStructure, csvData) { const elements = {}; // Convertir la structure en instructions de génération Object.entries(contentStructure).forEach(([tag, placeholder]) => { elements[tag] = { type: this.detectElementType(tag), instruction: this.createInstructionFromPlaceholder(placeholder, csvData), context: csvData?.mc0 || 'contenu personnalisé' }; }); return elements; } /** * DÉTECTER TYPE D'ÉLÉMENT */ detectElementType(tag) { const tagLower = tag.toLowerCase(); if (tagLower.includes('titre') || tagLower.includes('h1') || tagLower.includes('h2')) { return 'titre'; } else if (tagLower.includes('intro') || tagLower.includes('introduction')) { return 'introduction'; } else if (tagLower.includes('conclusion')) { return 'conclusion'; } else if (tagLower.includes('faq') || tagLower.includes('question')) { return 'faq'; } else { return 'contenu'; } } /** * CRÉER INSTRUCTION À PARTIR DU PLACEHOLDER */ createInstructionFromPlaceholder(placeholder, csvData) { // Si c'est déjà une vraie instruction, la garder if (typeof placeholder === 'string' && placeholder.length > 30) { return placeholder; } // Sinon, créer une instruction basique const mc0 = csvData?.mc0 || 'produit'; return `Rédige un contenu professionnel et engageant sur ${mc0}`; } /** * CRÉER PROMPT GÉNÉRATION INITIALE */ createInitialGenerationPrompt(chunk, csvData, config) { const personality = csvData?.personality; const mc0 = csvData?.mc0 || 'contenu personnalisé'; let prompt = `MISSION: Génère du contenu SEO initial de haute qualité. CONTEXTE: ${mc0} - Article optimisé SEO ${personality ? `PERSONNALITÉ: ${personality.nom} (${personality.style})` : ''} TEMPÉRATURE: ${config.temperature || 0.7} (créativité équilibrée) ÉLÉMENTS À GÉNÉRER: ${chunk.map(([tag, data], i) => `[${i + 1}] TAG: ${tag} TYPE: ${data.type} INSTRUCTION: ${data.instruction} CONTEXTE: ${data.context}`).join('\n\n')} CONSIGNES GÉNÉRATION: - CRÉE du contenu original et engageant${personality ? ` avec le style ${personality.style}` : ''} - INTÈGRE naturellement le mot-clé "${mc0}" - RESPECTE les bonnes pratiques SEO (mots-clés, structure) - ADAPTE longueur selon type d'élément: * Titres: 8-15 mots * Introduction: 2-3 phrases (40-80 mots) * Contenu: 3-6 phrases (80-200 mots) * Conclusion: 2-3 phrases (40-80 mots) - ÉVITE contenu générique, sois spécifique et informatif - UTILISE un ton professionnel mais accessible VOCABULAIRE RECOMMANDÉ SELON CONTEXTE: - Si signalétique: matériaux (dibond, aluminium), procédés (gravure, impression) - Adapte selon le domaine du mot-clé principal FORMAT RÉPONSE: [1] Contenu généré pour premier élément [2] Contenu généré pour deuxième élément etc... IMPORTANT: Réponse DIRECTE par les contenus générés, pas d'explication.`; return prompt; } /** * PARSER RÉPONSE GÉNÉRATION INITIALE */ parseInitialGenerationResponse(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 generatedContent = match[2].trim(); const [tag] = chunk[index]; // Nettoyer contenu généré generatedContent = this.cleanGeneratedContent(generatedContent); if (generatedContent && generatedContent.length > 10) { results[tag] = generatedContent; logSh(`✅ Généré [${tag}]: "${generatedContent.substring(0, 60)}..."`, 'DEBUG'); } else { results[tag] = this.createFallbackContent(tag, chunk[index][1]); logSh(`⚠️ Fallback génération [${tag}]: contenu invalide`, 'WARNING'); } index++; } // Compléter les manquants while (index < chunk.length) { const [tag, data] = chunk[index]; results[tag] = this.createFallbackContent(tag, data); index++; } return results; } /** * NETTOYER CONTENU GÉNÉRÉ */ cleanGeneratedContent(content) { if (!content) return content; // Supprimer préfixes indésirables content = content.replace(/^(voici\s+)?le\s+contenu\s+(généré|pour)\s*[:.]?\s*/gi, ''); content = content.replace(/^(contenu|élément)\s+(généré|pour)\s*[:.]?\s*/gi, ''); content = content.replace(/^(bon,?\s*)?(alors,?\s*)?/gi, ''); // Nettoyer formatage content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown content = content.replace(/\s{2,}/g, ' '); // Espaces multiples content = content.trim(); return content; } /** * CRÉER CONTENU FALLBACK */ createFallbackContent(tag, data) { const mc0 = data?.context || 'produit'; const type = data?.type || 'contenu'; switch (type) { case 'titre': return `${mc0.charAt(0).toUpperCase()}${mc0.slice(1)} de qualité professionnelle`; case 'introduction': return `Découvrez notre gamme complète de ${mc0}. Qualité premium et service personnalisé.`; case 'conclusion': return `Faites confiance à notre expertise pour votre ${mc0}. Contactez-nous pour plus d'informations.`; default: return `Notre ${mc0} répond à vos besoins avec des solutions adaptées et un service de qualité.`; } } } module.exports = { InitialGenerationLayer }; /* ┌────────────────────────────────────────────────────────────────────┐ │ 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 style (OpenAI + Mistral)', layers: [ { type: 'technical', llm: 'openai', intensity: 0.9 }, { type: 'style', llm: 'mistral', intensity: 0.8 } ], layersCount: 2 }, // Stack complet - Toutes couches séquentielles fullEnhancement: { name: 'fullEnhancement', description: 'Enhancement complet multi-LLM (OpenAI + Mistral)', layers: [ { type: 'technical', llm: 'openai', intensity: 1.0 }, { type: 'style', llm: 'mistral', intensity: 0.8 } ], layersCount: 2 }, // 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é - Style prioritaire fluidityFocus: { name: 'fluidityFocus', description: 'Focus style et technique avec Mistral + OpenAI', layers: [ { type: 'style', llm: 'mistral', intensity: 1.1 }, { type: 'technical', llm: 'openai', intensity: 0.7 } ], layersCount: 2 } }; /** * 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'); // Préparer configuration avec support tendances const layerConfig = { ...config, layerType: layer.type, llmProvider: layer.llm, intensity: config.intensity ? config.intensity * layer.intensity : layer.intensity, analysisMode: true }; // Ajouter tendance si présente if (config.trendManager) { layerConfig.trendManager = config.trendManager; } const layerResult = await applySelectiveLayer(currentContent, layerConfig); 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 }); } // Transitions layer removed - Gemini disabled if (needsAnalysis.style.needed && needsAnalysis.style.score > analysisThreshold) { layersToApply.push({ type: 'style', llm: 'mistral', intensity: Math.min(maxIntensity, needsAnalysis.style.score), priority: 3 }); } if (layersToApply.length === 0) { logSh(`✅ COUCHES ADAPTATIVES: Aucune amélioration nécessaire`, 'INFO'); return { content, stats: { adaptive: true, layersApplied: 0, analysisOnly: true, duration: Date.now() - startTime } }; } // 3. Appliquer couches par ordre de priorité layersToApply.sort((a, b) => a.priority - b.priority); logSh(` 🎯 Couches sélectionnées: ${layersToApply.map(l => `${l.type}(${l.intensity.toFixed(1)})`).join(' → ')}`, 'INFO'); let currentContent = content; const adaptiveStats = { layersAnalyzed: 3, layersApplied: layersToApply.length, layers: [], totalModifications: 0, adaptive: true }; for (const layer of layersToApply) { try { logSh(` 🔧 Couche adaptative: ${layer.type} (intensité: ${layer.intensity.toFixed(1)})`, 'DEBUG'); const layerResult = await applySelectiveLayer(currentContent, { ...config, layerType: layer.type, llmProvider: layer.llm, intensity: layer.intensity, analysisMode: true }); currentContent = layerResult.content; adaptiveStats.layers.push({ type: layer.type, llm: layer.llm, intensity: layer.intensity, elementsEnhanced: layerResult.stats.elementsEnhanced, duration: layerResult.stats.duration, success: !layerResult.stats.fallback }); adaptiveStats.totalModifications += layerResult.stats.elementsEnhanced; } catch (layerError) { logSh(` ❌ Couche adaptative ${layer.type} échouée: ${layerError.message}`, 'ERROR'); adaptiveStats.layers.push({ type: layer.type, error: layerError.message, success: false }); } } const duration = Date.now() - startTime; const successfulLayers = adaptiveStats.layers.filter(l => l.success).length; logSh(`✅ COUCHES ADAPTATIVES: ${successfulLayers}/${layersToApply.length} appliquées | ${adaptiveStats.totalModifications} modifications (${duration}ms)`, 'INFO'); await tracer.event('Couches adaptatives appliquées', { ...adaptiveStats, totalDuration: duration }); return { content: currentContent, stats: { ...adaptiveStats, totalDuration: duration }, original: content }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ COUCHES ADAPTATIVES ÉCHOUÉES après ${duration}ms: ${error.message}`, 'ERROR'); return { content, stats: { adaptive: true, error: error.message, duration }, original: content, fallback: true }; } }, { content: Object.keys(content), config }); } /** * PIPELINE COUCHES PERSONNALISÉ */ async function applyLayerPipeline(content, layerSequence, config = {}) { return await tracer.run('SelectiveLayers.applyLayerPipeline()', async () => { if (!Array.isArray(layerSequence) || layerSequence.length === 0) { throw new Error('Séquence de couches invalide ou vide'); } await tracer.annotate({ selectiveLayerPipeline: true, pipelineLength: layerSequence.length, elementsCount: Object.keys(content).length }); const startTime = Date.now(); logSh(`🔄 PIPELINE COUCHES SELECTIVE PERSONNALISÉ: ${layerSequence.length} étapes`, 'INFO'); try { let currentContent = content; const pipelineStats = { pipelineLength: layerSequence.length, steps: [], totalModifications: 0, success: true }; for (let i = 0; i < layerSequence.length; i++) { const step = layerSequence[i]; try { logSh(` 📍 Étape ${i + 1}/${layerSequence.length}: ${step.type} (${step.llm || 'auto'})`, 'DEBUG'); const stepResult = await applySelectiveLayer(currentContent, { ...config, ...step }); currentContent = stepResult.content; pipelineStats.steps.push({ order: i + 1, ...step, elementsEnhanced: stepResult.stats.elementsEnhanced, duration: stepResult.stats.duration, success: !stepResult.stats.fallback }); pipelineStats.totalModifications += stepResult.stats.elementsEnhanced; } catch (stepError) { logSh(` ❌ Étape ${i + 1} échouée: ${stepError.message}`, 'ERROR'); pipelineStats.steps.push({ order: i + 1, ...step, error: stepError.message, success: false }); } } const duration = Date.now() - startTime; const successfulSteps = pipelineStats.steps.filter(s => s.success).length; logSh(`✅ PIPELINE SELECTIVE: ${successfulSteps}/${layerSequence.length} étapes | ${pipelineStats.totalModifications} modifications (${duration}ms)`, 'INFO'); await tracer.event('Pipeline selective appliqué', { ...pipelineStats, totalDuration: duration }); return { content: currentContent, stats: { ...pipelineStats, totalDuration: duration }, original: content }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ PIPELINE SELECTIVE ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR'); return { content, stats: { error: error.message, duration, success: false }, original: content, fallback: true }; } }, { content: Object.keys(content), layerSequence, config }); } // ============= HELPER FUNCTIONS ============= /** * Analyser besoins selective enhancement */ async function analyzeSelectiveNeeds(content, csvData) { const analysis = { technical: { needed: false, score: 0, elements: [] }, transitions: { needed: false, score: 0, elements: [] }, style: { needed: false, score: 0, elements: [] } }; // Analyser chaque élément pour tous types de besoins Object.entries(content).forEach(([tag, text]) => { // Analyse technique (import depuis SelectiveCore logic) const technicalNeed = assessTechnicalNeed(text, csvData); if (technicalNeed.score > 0.3) { analysis.technical.needed = true; analysis.technical.score += technicalNeed.score; analysis.technical.elements.push({ tag, score: technicalNeed.score }); } // Analyse transitions const transitionNeed = assessTransitionNeed(text); if (transitionNeed.score > 0.3) { analysis.transitions.needed = true; analysis.transitions.score += transitionNeed.score; analysis.transitions.elements.push({ tag, score: transitionNeed.score }); } // Analyse style const styleNeed = assessStyleNeed(text, csvData?.personality); if (styleNeed.score > 0.3) { analysis.style.needed = true; analysis.style.score += styleNeed.score; analysis.style.elements.push({ tag, score: styleNeed.score }); } }); // Normaliser scores const elementCount = Object.keys(content).length; analysis.technical.score = analysis.technical.score / elementCount; analysis.transitions.score = analysis.transitions.score / elementCount; analysis.style.score = analysis.style.score / elementCount; return analysis; } /** * Évaluer besoin technique (simplifié de SelectiveCore) */ function assessTechnicalNeed(content, csvData) { let score = 0; // Manque de termes techniques spécifiques if (csvData?.mc0) { const technicalTerms = ['dibond', 'pmma', 'aluminium', 'fraisage', 'impression', 'gravure']; const foundTerms = technicalTerms.filter(term => content.toLowerCase().includes(term)); if (foundTerms.length === 0 && content.length > 100) { score += 0.4; } } // Vocabulaire générique const genericWords = ['produit', 'solution', 'service', 'qualité']; const genericCount = genericWords.filter(word => content.toLowerCase().includes(word)).length; if (genericCount > 2) score += 0.3; return { score: Math.min(1, score) }; } /** * Évaluer besoin transitions (simplifié) */ function assessTransitionNeed(content) { const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10); if (sentences.length < 2) return { score: 0 }; let score = 0; // Connecteurs répétitifs const connectors = ['par ailleurs', 'en effet', 'de plus']; let repetitions = 0; connectors.forEach(connector => { const matches = (content.match(new RegExp(connector, 'gi')) || []); if (matches.length > 1) repetitions++; }); if (repetitions > 1) score += 0.4; return { score: Math.min(1, score) }; } /** * Évaluer besoin style (simplifié) */ function assessStyleNeed(content, personality) { let score = 0; if (!personality) { score += 0.2; return { score }; } // Style générique const personalityWords = (personality.vocabulairePref || '').toLowerCase().split(','); const personalityFound = personalityWords.some(word => word.trim() && content.toLowerCase().includes(word.trim()) ); if (!personalityFound && content.length > 50) score += 0.4; return { score: Math.min(1, score) }; } /** * Obtenir stacks disponibles */ function getAvailableStacks() { return Object.values(PREDEFINED_STACKS); } module.exports = { // Main functions applyPredefinedStack, applyAdaptiveLayers, applyLayerPipeline, // Utils getAvailableStacks, analyzeSelectiveNeeds, // Constants PREDEFINED_STACKS }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/adversarial-generation/AdversarialLayers.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // ADVERSARIAL LAYERS - COUCHES MODULAIRES // Responsabilité: Couches adversariales composables et réutilisables // Architecture: Fonction pipeline |> layer1 |> layer2 |> layer3 // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { applyAdversarialLayer } = require('./AdversarialCore'); /** * COUCHE ANTI-GPTZEERO - Spécialisée contre GPTZero */ async function applyAntiGPTZeroLayer(content, options = {}) { return await applyAdversarialLayer(content, { detectorTarget: 'gptZero', intensity: options.intensity || 1.0, method: options.method || 'regeneration', ...options }); } /** * COUCHE ANTI-ORIGINALITY - Spécialisée contre Originality.ai */ async function applyAntiOriginalityLayer(content, options = {}) { return await applyAdversarialLayer(content, { detectorTarget: 'originality', intensity: options.intensity || 1.1, method: options.method || 'hybrid', ...options }); } /** * COUCHE ANTI-WINSTON - Spécialisée contre Winston AI */ async function applyAntiWinstonLayer(content, options = {}) { return await applyAdversarialLayer(content, { detectorTarget: 'winston', intensity: options.intensity || 0.9, method: options.method || 'enhancement', ...options }); } /** * COUCHE GÉNÉRALE - Protection généraliste multi-détecteurs */ async function applyGeneralAdversarialLayer(content, options = {}) { return await applyAdversarialLayer(content, { detectorTarget: 'general', intensity: options.intensity || 0.8, method: options.method || 'hybrid', ...options }); } /** * COUCHE LÉGÈRE - Modifications subtiles pour préserver qualité */ async function applyLightAdversarialLayer(content, options = {}) { return await applyAdversarialLayer(content, { detectorTarget: options.detectorTarget || 'general', intensity: 0.5, method: 'enhancement', preserveStructure: true, ...options }); } /** * COUCHE INTENSIVE - Maximum anti-détection */ async function applyIntensiveAdversarialLayer(content, options = {}) { return await applyAdversarialLayer(content, { detectorTarget: options.detectorTarget || 'gptZero', intensity: 1.5, method: 'regeneration', preserveStructure: false, ...options }); } /** * PIPELINE COMPOSABLE - Application séquentielle de couches */ async function applyLayerPipeline(content, layers = [], globalOptions = {}) { return await tracer.run('AdversarialLayers.applyLayerPipeline()', async () => { await tracer.annotate({ layersPipeline: true, layersCount: layers.length, elementsCount: Object.keys(content).length }); const startTime = Date.now(); logSh(`🔄 PIPELINE COUCHES ADVERSARIALES: ${layers.length} couches`, 'INFO'); let currentContent = content; const pipelineStats = { layers: [], totalDuration: 0, totalModifications: 0, success: true }; try { for (let i = 0; i < layers.length; i++) { const layer = layers[i]; const layerStartTime = Date.now(); logSh(` 🎯 Couche ${i + 1}/${layers.length}: ${layer.name || layer.type || 'anonyme'}`, 'DEBUG'); try { const layerResult = await applyLayerByConfig(currentContent, layer, globalOptions); currentContent = layerResult.content; const layerStats = { name: layer.name || `layer_${i + 1}`, type: layer.type, duration: Date.now() - layerStartTime, modificationsCount: layerResult.stats?.elementsModified || 0, success: true }; pipelineStats.layers.push(layerStats); pipelineStats.totalModifications += layerStats.modificationsCount; logSh(` ✅ ${layerStats.name}: ${layerStats.modificationsCount} modifs (${layerStats.duration}ms)`, 'DEBUG'); } catch (error) { logSh(` ❌ Couche ${i + 1} échouée: ${error.message}`, 'ERROR'); pipelineStats.layers.push({ name: layer.name || `layer_${i + 1}`, type: layer.type, duration: Date.now() - layerStartTime, success: false, error: error.message }); // Continuer avec le contenu précédent si une couche échoue if (!globalOptions.stopOnError) { continue; } else { throw error; } } } pipelineStats.totalDuration = Date.now() - startTime; pipelineStats.success = pipelineStats.layers.every(layer => layer.success); logSh(`🔄 PIPELINE TERMINÉ: ${pipelineStats.totalModifications} modifs totales (${pipelineStats.totalDuration}ms)`, 'INFO'); await tracer.event('Pipeline couches terminé', pipelineStats); return { content: currentContent, stats: pipelineStats, original: content }; } catch (error) { pipelineStats.totalDuration = Date.now() - startTime; pipelineStats.success = false; logSh(`❌ PIPELINE COUCHES ÉCHOUÉ après ${pipelineStats.totalDuration}ms: ${error.message}`, 'ERROR'); throw error; } }, { layers: layers.map(l => l.name || l.type), content: Object.keys(content) }); } /** * COUCHES PRÉDÉFINIES - Configurations courantes */ const PREDEFINED_LAYERS = { // Stack défensif léger lightDefense: [ { type: 'general', name: 'General Light', intensity: 0.6, method: 'enhancement' }, { type: 'anti-gptZero', name: 'GPTZero Light', intensity: 0.5, method: 'enhancement' } ], // Stack défensif standard standardDefense: [ { type: 'general', name: 'General Standard', intensity: 0.8, method: 'hybrid' }, { type: 'anti-gptZero', name: 'GPTZero Standard', intensity: 0.9, method: 'enhancement' }, { type: 'anti-originality', name: 'Originality Standard', intensity: 0.8, method: 'enhancement' } ], // Stack défensif intensif heavyDefense: [ { type: 'general', name: 'General Heavy', intensity: 1.0, method: 'regeneration' }, { type: 'anti-gptZero', name: 'GPTZero Heavy', intensity: 1.2, method: 'regeneration' }, { type: 'anti-originality', name: 'Originality Heavy', intensity: 1.1, method: 'hybrid' }, { type: 'anti-winston', name: 'Winston Heavy', intensity: 1.0, method: 'enhancement' } ], // Stack ciblé GPTZero gptZeroFocused: [ { type: 'anti-gptZero', name: 'GPTZero Primary', intensity: 1.3, method: 'regeneration' }, { type: 'general', name: 'General Support', intensity: 0.7, method: 'enhancement' } ], // Stack ciblé Originality originalityFocused: [ { type: 'anti-originality', name: 'Originality Primary', intensity: 1.4, method: 'hybrid' }, { type: 'general', name: 'General Support', intensity: 0.8, method: 'enhancement' } ] }; /** * APPLIQUER STACK PRÉDÉFINI */ async function applyPredefinedStack(content, stackName, options = {}) { const stack = PREDEFINED_LAYERS[stackName]; if (!stack) { throw new Error(`Stack prédéfini inconnu: ${stackName}. Disponibles: ${Object.keys(PREDEFINED_LAYERS).join(', ')}`); } logSh(`📦 APPLICATION STACK PRÉDÉFINI: ${stackName}`, 'INFO'); return await applyLayerPipeline(content, stack, options); } /** * COUCHES ADAPTATIVES - S'adaptent selon le contenu */ async function applyAdaptiveLayers(content, options = {}) { const { targetDetectors = ['gptZero', 'originality'], maxIntensity = 1.0, analysisMode = true } = options; logSh(`🧠 COUCHES ADAPTATIVES: Analyse + adaptation auto`, 'INFO'); // 1. Analyser le contenu pour détecter les risques const contentAnalysis = analyzeContentRisks(content); // 2. Construire pipeline adaptatif selon l'analyse const adaptiveLayers = []; // Niveau de base selon risque global const baseIntensity = Math.min(maxIntensity, contentAnalysis.globalRisk * 1.2); if (baseIntensity > 0.3) { adaptiveLayers.push({ type: 'general', name: 'Adaptive Base', intensity: baseIntensity, method: baseIntensity > 0.7 ? 'hybrid' : 'enhancement' }); } // Couches spécifiques selon détecteurs ciblés targetDetectors.forEach(detector => { const detectorRisk = contentAnalysis.detectorRisks[detector] || 0; if (detectorRisk > 0.4) { const intensity = Math.min(maxIntensity * 1.1, detectorRisk * 1.5); adaptiveLayers.push({ type: `anti-${detector}`, name: `Adaptive ${detector}`, intensity, method: intensity > 0.8 ? 'regeneration' : 'enhancement' }); } }); logSh(` 🎯 ${adaptiveLayers.length} couches adaptatives générées`, 'DEBUG'); if (adaptiveLayers.length === 0) { logSh(` ✅ Contenu déjà optimal, aucune couche nécessaire`, 'INFO'); return { content, stats: { adaptive: true, layersApplied: 0 }, original: content }; } return await applyLayerPipeline(content, adaptiveLayers, options); } // ============= HELPER FUNCTIONS ============= /** * Appliquer couche selon configuration */ async function applyLayerByConfig(content, layerConfig, globalOptions = {}) { const { type, intensity, method, ...layerOptions } = layerConfig; const options = { ...globalOptions, ...layerOptions, intensity, method }; switch (type) { case 'general': return await applyGeneralAdversarialLayer(content, options); case 'anti-gptZero': return await applyAntiGPTZeroLayer(content, options); case 'anti-originality': return await applyAntiOriginalityLayer(content, options); case 'anti-winston': return await applyAntiWinstonLayer(content, options); case 'light': return await applyLightAdversarialLayer(content, options); case 'intensive': return await applyIntensiveAdversarialLayer(content, options); default: throw new Error(`Type de couche inconnu: ${type}`); } } /** * Analyser risques du contenu pour adaptation */ function analyzeContentRisks(content) { const analysis = { globalRisk: 0, detectorRisks: {}, riskFactors: [] }; const allContent = Object.values(content).join(' '); // Risques génériques let riskScore = 0; // 1. Mots typiques IA const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge', 'furthermore', 'moreover']; const aiWordCount = aiWords.filter(word => allContent.toLowerCase().includes(word)).length; if (aiWordCount > 2) { riskScore += 0.3; analysis.riskFactors.push(`mots_ia: ${aiWordCount}`); } // 2. Structure uniforme const contentLengths = Object.values(content).map(c => c.length); const avgLength = contentLengths.reduce((a, b) => a + b, 0) / contentLengths.length; const variance = contentLengths.reduce((sum, len) => sum + Math.pow(len - avgLength, 2), 0) / contentLengths.length; const uniformity = 1 - (Math.sqrt(variance) / Math.max(avgLength, 1)); if (uniformity > 0.8) { riskScore += 0.2; analysis.riskFactors.push(`uniformité: ${uniformity.toFixed(2)}`); } // 3. Connecteurs répétitifs const repetitiveConnectors = ['par ailleurs', 'en effet', 'de plus', 'cependant']; const connectorCount = repetitiveConnectors.filter(conn => (allContent.match(new RegExp(conn, 'gi')) || []).length > 1 ).length; if (connectorCount > 2) { riskScore += 0.2; analysis.riskFactors.push(`connecteurs_répétitifs: ${connectorCount}`); } analysis.globalRisk = Math.min(1, riskScore); // Risques spécifiques par détecteur analysis.detectorRisks = { gptZero: analysis.globalRisk + (uniformity > 0.7 ? 0.3 : 0), originality: analysis.globalRisk + (aiWordCount > 3 ? 0.4 : 0), winston: analysis.globalRisk + (connectorCount > 2 ? 0.2 : 0) }; return analysis; } /** * Obtenir informations sur les stacks disponibles */ function getAvailableStacks() { return Object.keys(PREDEFINED_LAYERS).map(stackName => ({ name: stackName, layersCount: PREDEFINED_LAYERS[stackName].length, description: getStackDescription(stackName), layers: PREDEFINED_LAYERS[stackName] })); } /** * Description des stacks prédéfinis */ function getStackDescription(stackName) { const descriptions = { lightDefense: 'Protection légère préservant la qualité', standardDefense: 'Protection équilibrée multi-détecteurs', heavyDefense: 'Protection maximale tous détecteurs', gptZeroFocused: 'Optimisation spécifique anti-GPTZero', originalityFocused: 'Optimisation spécifique anti-Originality.ai' }; return descriptions[stackName] || 'Stack personnalisé'; } module.exports = { // Couches individuelles applyAntiGPTZeroLayer, applyAntiOriginalityLayer, applyAntiWinstonLayer, applyGeneralAdversarialLayer, applyLightAdversarialLayer, applyIntensiveAdversarialLayer, // Pipeline et stacks applyLayerPipeline, // ← MAIN ENTRY POINT PIPELINE applyPredefinedStack, // ← MAIN ENTRY POINT STACKS applyAdaptiveLayers, // ← MAIN ENTRY POINT ADAPTATIF // Utilitaires getAvailableStacks, analyzeContentRisks, PREDEFINED_LAYERS }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/human-simulation/HumanSimulationLayers.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: HumanSimulationLayers.js // RESPONSABILITÉ: Stacks prédéfinis Human Simulation // Compatible avec architecture modulaire existante // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { applyHumanSimulationLayer } = require('./HumanSimulationCore'); /** * STACKS PRÉDÉFINIS HUMAN SIMULATION * Configuration par niveau d'intensité */ const HUMAN_SIMULATION_STACKS = { // ======================================== // SIMULATION LÉGÈRE - Pour tests et développement // ======================================== lightSimulation: { name: 'lightSimulation', description: 'Simulation humaine légère - développement et tests', layersCount: 3, config: { fatigueEnabled: true, personalityErrorsEnabled: true, temporalStyleEnabled: false, // Désactivé en mode light imperfectionIntensity: 0.3, // Faible intensité naturalRepetitions: true, qualityThreshold: 0.8, // Seuil élevé maxModificationsPerElement: 2 // Limité à 2 modifs par élément }, expectedImpact: { modificationsPerElement: '1-2', qualityPreservation: '95%', detectionReduction: '10-15%', executionTime: '+20%' } }, // ======================================== // SIMULATION STANDARD - Usage production normal // ======================================== standardSimulation: { name: 'standardSimulation', description: 'Simulation humaine standard - équilibre performance/qualité', layersCount: 3, config: { fatigueEnabled: true, personalityErrorsEnabled: true, temporalStyleEnabled: true, // Activé imperfectionIntensity: 0.6, // Intensité moyenne naturalRepetitions: true, qualityThreshold: 0.7, // Seuil normal maxModificationsPerElement: 3 // 3 modifs max }, expectedImpact: { modificationsPerElement: '2-3', qualityPreservation: '85%', detectionReduction: '25-35%', executionTime: '+40%' } }, // ======================================== // SIMULATION INTENSIVE - Maximum anti-détection // ======================================== heavySimulation: { name: 'heavySimulation', description: 'Simulation humaine intensive - anti-détection maximale', layersCount: 3, config: { fatigueEnabled: true, personalityErrorsEnabled: true, temporalStyleEnabled: true, imperfectionIntensity: 0.9, // Intensité élevée naturalRepetitions: true, qualityThreshold: 0.6, // Seuil plus permissif maxModificationsPerElement: 5 // Jusqu'à 5 modifs }, expectedImpact: { modificationsPerElement: '3-5', qualityPreservation: '75%', detectionReduction: '40-50%', executionTime: '+60%' } }, // ======================================== // SIMULATION ADAPTIVE - Intelligence contextuelle // ======================================== adaptiveSimulation: { name: 'adaptiveSimulation', description: 'Simulation humaine adaptive - ajustement intelligent selon contexte', layersCount: 3, config: { fatigueEnabled: true, personalityErrorsEnabled: true, temporalStyleEnabled: true, imperfectionIntensity: 'adaptive', // Calculé dynamiquement naturalRepetitions: true, qualityThreshold: 'adaptive', // Ajusté selon complexité maxModificationsPerElement: 'adaptive', // Variable adaptiveLogic: true // Flag pour logique adaptive }, expectedImpact: { modificationsPerElement: '1-4', qualityPreservation: '80-90%', detectionReduction: '30-45%', executionTime: '+45%' } }, // ======================================== // SIMULATION PERSONNALISÉE - Focus personnalité // ======================================== personalityFocus: { name: 'personalityFocus', description: 'Focus erreurs personnalité - cohérence maximale', layersCount: 2, config: { fatigueEnabled: false, // Désactivé personalityErrorsEnabled: true, temporalStyleEnabled: false, // Désactivé imperfectionIntensity: 1.0, // Focus sur personnalité naturalRepetitions: true, qualityThreshold: 0.75, maxModificationsPerElement: 3 }, expectedImpact: { modificationsPerElement: '2-3', qualityPreservation: '85%', detectionReduction: '20-30%', executionTime: '+25%' } }, // ======================================== // SIMULATION TEMPORELLE - Focus variations horaires // ======================================== temporalFocus: { name: 'temporalFocus', description: 'Focus style temporel - variations selon heure', layersCount: 2, config: { fatigueEnabled: false, personalityErrorsEnabled: false, temporalStyleEnabled: true, // Focus principal imperfectionIntensity: 0.8, naturalRepetitions: true, qualityThreshold: 0.75, maxModificationsPerElement: 3 }, expectedImpact: { modificationsPerElement: '1-3', qualityPreservation: '85%', detectionReduction: '15-25%', executionTime: '+20%' } } }; /** * APPLICATION STACK PRÉDÉFINI * @param {object} content - Contenu à simuler * @param {string} stackName - Nom du stack * @param {object} options - Options additionnelles * @returns {object} - Résultat simulation */ async function applyPredefinedSimulation(content, stackName, options = {}) { return await tracer.run(`HumanSimulationLayers.applyPredefinedSimulation(${stackName})`, async () => { const stack = HUMAN_SIMULATION_STACKS[stackName]; if (!stack) { throw new Error(`Stack Human Simulation non trouvé: ${stackName}`); } await tracer.annotate({ stackName, stackDescription: stack.description, layersCount: stack.layersCount, contentElements: Object.keys(content).length }); logSh(`🧠 APPLICATION STACK: ${stack.name}`, 'INFO'); logSh(` 📝 ${stack.description}`, 'DEBUG'); logSh(` ⚙️ ${stack.layersCount} couches actives`, 'DEBUG'); try { // Configuration fusionnée let finalConfig = { ...stack.config, ...options }; // ======================================== // LOGIQUE ADAPTIVE (si applicable) // ======================================== if (stack.config.adaptiveLogic) { finalConfig = await applyAdaptiveLogic(content, finalConfig, options); logSh(` 🧠 Logique adaptive appliquée`, 'DEBUG'); } // ======================================== // APPLICATION SIMULATION PRINCIPALE // ======================================== const simulationOptions = { ...finalConfig, elementIndex: options.elementIndex || 0, totalElements: options.totalElements || Object.keys(content).length, currentHour: options.currentHour || new Date().getHours(), csvData: options.csvData, stackName: stack.name }; const result = await applyHumanSimulationLayer(content, simulationOptions); // ======================================== // ENRICHISSEMENT RÉSULTAT // ======================================== const enrichedResult = { ...result, stackInfo: { name: stack.name, description: stack.description, layersCount: stack.layersCount, expectedImpact: stack.expectedImpact, configUsed: finalConfig } }; logSh(`✅ STACK ${stack.name} terminé: ${result.stats.totalModifications} modifications`, 'INFO'); await tracer.event('Stack Human Simulation terminé', { stackName, success: !result.fallback, modifications: result.stats.totalModifications, qualityScore: result.qualityScore }); return enrichedResult; } catch (error) { logSh(`❌ ERREUR STACK ${stack.name}: ${error.message}`, 'ERROR'); await tracer.event('Stack Human Simulation échoué', { stackName, error: error.message }); // Fallback gracieux return { content, stats: { fallbackUsed: true, error: error.message }, fallback: true, stackInfo: { name: stack.name, error: error.message } }; } }, { stackName, contentElements: Object.keys(content).length }); } /** * LOGIQUE ADAPTIVE INTELLIGENTE * Ajuste la configuration selon le contexte */ async function applyAdaptiveLogic(content, config, options) { logSh('🧠 Application logique adaptive', 'DEBUG'); const adaptedConfig = { ...config }; // ======================================== // 1. ANALYSE COMPLEXITÉ CONTENU // ======================================== const totalText = Object.values(content).join(' '); const wordCount = totalText.split(/\s+/).length; const avgElementLength = wordCount / Object.keys(content).length; // ======================================== // 2. AJUSTEMENT INTENSITÉ SELON COMPLEXITÉ // ======================================== if (avgElementLength > 200) { // Contenu long = intensité réduite pour préserver qualité adaptedConfig.imperfectionIntensity = 0.5; adaptedConfig.qualityThreshold = 0.8; logSh(' 📏 Contenu long détecté: intensité réduite', 'DEBUG'); } else if (avgElementLength < 50) { // Contenu court = intensité augmentée adaptedConfig.imperfectionIntensity = 1.0; adaptedConfig.qualityThreshold = 0.6; logSh(' 📏 Contenu court détecté: intensité augmentée', 'DEBUG'); } else { // Contenu moyen = intensité équilibrée adaptedConfig.imperfectionIntensity = 0.7; adaptedConfig.qualityThreshold = 0.7; } // ======================================== // 3. AJUSTEMENT SELON PERSONNALITÉ // ======================================== const personality = options.csvData?.personality; if (personality) { const personalityName = personality.nom.toLowerCase(); // Personnalités techniques = moins d'erreurs de personnalité if (['marc', 'amara', 'fabrice'].includes(personalityName)) { adaptedConfig.imperfectionIntensity *= 0.8; logSh(' 🎭 Personnalité technique: intensité erreurs réduite', 'DEBUG'); } // Personnalités créatives = plus d'erreurs stylistiques if (['sophie', 'émilie', 'chloé'].includes(personalityName)) { adaptedConfig.imperfectionIntensity *= 1.2; logSh(' 🎭 Personnalité créative: intensité erreurs augmentée', 'DEBUG'); } } // ======================================== // 4. AJUSTEMENT SELON HEURE // ======================================== const currentHour = options.currentHour || new Date().getHours(); if (currentHour >= 22 || currentHour <= 6) { // Nuit = plus de fatigue, moins de complexité adaptedConfig.fatigueEnabled = true; adaptedConfig.temporalStyleEnabled = true; adaptedConfig.imperfectionIntensity *= 1.3; logSh(' 🌙 Période nocturne: simulation fatigue renforcée', 'DEBUG'); } else if (currentHour >= 6 && currentHour <= 10) { // Matin = énergie, moins d'erreurs adaptedConfig.imperfectionIntensity *= 0.7; logSh(' 🌅 Période matinale: intensité réduite', 'DEBUG'); } // ======================================== // 5. LIMITATION SÉCURITÉ // ======================================== adaptedConfig.imperfectionIntensity = Math.max(0.2, Math.min(1.5, adaptedConfig.imperfectionIntensity)); adaptedConfig.qualityThreshold = Math.max(0.5, Math.min(0.9, adaptedConfig.qualityThreshold)); // Modifs max adaptées à la taille du contenu adaptedConfig.maxModificationsPerElement = Math.min(6, Math.max(1, Math.ceil(avgElementLength / 50))); logSh(` 🎯 Config adaptée: intensité=${adaptedConfig.imperfectionIntensity.toFixed(2)}, seuil=${adaptedConfig.qualityThreshold.toFixed(2)}`, 'DEBUG'); return adaptedConfig; } /** * OBTENIR STACKS DISPONIBLES * @returns {array} - Liste des stacks avec métadonnées */ function getAvailableSimulationStacks() { return Object.values(HUMAN_SIMULATION_STACKS).map(stack => ({ name: stack.name, description: stack.description, layersCount: stack.layersCount, expectedImpact: stack.expectedImpact, configPreview: { fatigueEnabled: stack.config.fatigueEnabled, personalityErrorsEnabled: stack.config.personalityErrorsEnabled, temporalStyleEnabled: stack.config.temporalStyleEnabled, intensity: stack.config.imperfectionIntensity } })); } /** * VALIDATION STACK * @param {string} stackName - Nom du stack à valider * @returns {object} - Résultat validation */ function validateSimulationStack(stackName) { const stack = HUMAN_SIMULATION_STACKS[stackName]; if (!stack) { return { valid: false, error: `Stack '${stackName}' non trouvé`, availableStacks: Object.keys(HUMAN_SIMULATION_STACKS) }; } // Validation configuration const configIssues = []; if (typeof stack.config.imperfectionIntensity === 'number' && (stack.config.imperfectionIntensity < 0 || stack.config.imperfectionIntensity > 2)) { configIssues.push('intensité hors limites (0-2)'); } if (typeof stack.config.qualityThreshold === 'number' && (stack.config.qualityThreshold < 0.3 || stack.config.qualityThreshold > 1)) { configIssues.push('seuil qualité hors limites (0.3-1)'); } return { valid: configIssues.length === 0, stack, issues: configIssues, recommendation: configIssues.length > 0 ? 'Corriger la configuration avant utilisation' : 'Stack prêt à utiliser' }; } /** * RECOMMANDATION STACK AUTOMATIQUE * @param {object} context - Contexte { contentLength, personality, hour, goal } * @returns {string} - Nom du stack recommandé */ function recommendSimulationStack(context = {}) { const { contentLength, personality, hour, goal } = context; logSh('🤖 Recommandation stack automatique', 'DEBUG'); // Priorité 1: Objectif spécifique if (goal === 'development') return 'lightSimulation'; if (goal === 'maximum_stealth') return 'heavySimulation'; if (goal === 'personality_focus') return 'personalityFocus'; if (goal === 'temporal_focus') return 'temporalFocus'; // Priorité 2: Complexité contenu if (contentLength > 2000) return 'lightSimulation'; // Contenu long = prudent if (contentLength < 300) return 'heavySimulation'; // Contenu court = intensif // Priorité 3: Personnalité if (personality) { const personalityName = personality.toLowerCase(); if (['marc', 'amara', 'fabrice'].includes(personalityName)) { return 'standardSimulation'; // Techniques = équilibré } if (['sophie', 'chloé', 'émilie'].includes(personalityName)) { return 'personalityFocus'; // Créatives = focus personnalité } } // Priorité 4: Heure if (hour >= 22 || hour <= 6) return 'temporalFocus'; // Nuit = focus temporel if (hour >= 6 && hour <= 10) return 'lightSimulation'; // Matin = léger // Par défaut: adaptive pour intelligence contextuelle logSh(' 🎯 Recommandation: adaptiveSimulation (par défaut)', 'DEBUG'); return 'adaptiveSimulation'; } // ============= EXPORTS ============= module.exports = { applyPredefinedSimulation, getAvailableSimulationStacks, validateSimulationStack, recommendSimulationStack, applyAdaptiveLogic, HUMAN_SIMULATION_STACKS }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/pattern-breaking/PatternBreakingLayers.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: PatternBreakingLayers.js // RESPONSABILITÉ: Stacks prédéfinis pour Pattern Breaking // Configurations optimisées par cas d'usage // ======================================== const { logSh } = require('../ErrorReporting'); /** * CONFIGURATIONS PRÉDÉFINIES PATTERN BREAKING * Optimisées pour différents niveaux et cas d'usage */ const PATTERN_BREAKING_STACKS = { // ======================================== // STACK LÉGER - Usage quotidien // ======================================== lightPatternBreaking: { name: 'Light Pattern Breaking', description: 'Anti-détection subtile pour usage quotidien', intensity: 0.3, config: { syntaxVariationEnabled: true, llmFingerprintReplacement: false, // Pas de remplacement mots naturalConnectorsEnabled: true, preserveReadability: true, maxModificationsPerElement: 2, qualityThreshold: 0.7 }, expectedReduction: '10-15%', useCase: 'Articles standard, faible risque détection' }, // ======================================== // STACK STANDARD - Équilibre optimal // ======================================== standardPatternBreaking: { name: 'Standard Pattern Breaking', description: 'Équilibre optimal efficacité/naturalité', intensity: 0.5, config: { syntaxVariationEnabled: true, llmFingerprintReplacement: true, naturalConnectorsEnabled: true, preserveReadability: true, maxModificationsPerElement: 4, qualityThreshold: 0.6 }, expectedReduction: '20-25%', useCase: 'Usage général recommandé' }, // ======================================== // STACK INTENSIF - Anti-détection poussée // ======================================== heavyPatternBreaking: { name: 'Heavy Pattern Breaking', description: 'Anti-détection intensive pour cas critiques', intensity: 0.8, config: { syntaxVariationEnabled: true, llmFingerprintReplacement: true, naturalConnectorsEnabled: true, preserveReadability: true, maxModificationsPerElement: 6, qualityThreshold: 0.5 }, expectedReduction: '30-35%', useCase: 'Détection élevée, contenu critique' }, // ======================================== // STACK ADAPTATIF - Selon contenu // ======================================== adaptivePatternBreaking: { name: 'Adaptive Pattern Breaking', description: 'Adaptation intelligente selon détection patterns', intensity: 0.6, config: { syntaxVariationEnabled: true, llmFingerprintReplacement: true, naturalConnectorsEnabled: true, preserveReadability: true, maxModificationsPerElement: 5, qualityThreshold: 0.6, adaptiveMode: true // Ajuste selon détection patterns }, expectedReduction: '25-30%', useCase: 'Adaptation automatique par contenu' }, // ======================================== // STACK SYNTAXE FOCUS - Syntaxe uniquement // ======================================== syntaxFocus: { name: 'Syntax Focus', description: 'Focus sur variations syntaxiques uniquement', intensity: 0.7, config: { syntaxVariationEnabled: true, llmFingerprintReplacement: false, naturalConnectorsEnabled: false, preserveReadability: true, maxModificationsPerElement: 6, qualityThreshold: 0.7 }, expectedReduction: '15-20%', useCase: 'Préservation vocabulaire, syntaxe variable' }, // ======================================== // STACK CONNECTEURS FOCUS - Connecteurs uniquement // ======================================== connectorsFocus: { name: 'Connectors Focus', description: 'Humanisation connecteurs et transitions', intensity: 0.8, config: { syntaxVariationEnabled: false, llmFingerprintReplacement: false, naturalConnectorsEnabled: true, preserveReadability: true, maxModificationsPerElement: 4, qualityThreshold: 0.8, connectorTone: 'casual' // casual, conversational, technical, commercial }, expectedReduction: '12-18%', useCase: 'Textes formels à humaniser' } }; /** * APPLICATION STACK PATTERN BREAKING * @param {string} stackName - Nom du stack à appliquer * @param {object} content - Contenu à traiter * @param {object} overrides - Options pour surcharger le stack * @returns {object} - { content, stats, stackUsed } */ async function applyPatternBreakingStack(stackName, content, overrides = {}) { const { applyPatternBreakingLayer } = require('./PatternBreakingCore'); logSh(`📦 Application Stack Pattern Breaking: ${stackName}`, 'INFO'); const stack = PATTERN_BREAKING_STACKS[stackName]; if (!stack) { logSh(`❌ Stack Pattern Breaking inconnu: ${stackName}`, 'WARNING'); throw new Error(`Stack Pattern Breaking inconnu: ${stackName}`); } try { // Configuration fusionnée (stack + overrides) const finalConfig = { ...stack.config, intensityLevel: stack.intensity, ...overrides }; logSh(` 🎯 Configuration: ${stack.description}`, 'DEBUG'); logSh(` ⚡ Intensité: ${finalConfig.intensityLevel} | Réduction attendue: ${stack.expectedReduction}`, 'DEBUG'); // Mode adaptatif si activé if (finalConfig.adaptiveMode) { const adaptedConfig = await adaptConfigurationToContent(content, finalConfig); Object.assign(finalConfig, adaptedConfig); logSh(` 🧠 Mode adaptatif appliqué`, 'DEBUG'); } // Application Pattern Breaking const result = await applyPatternBreakingLayer(content, finalConfig); logSh(`📦 Stack Pattern Breaking terminé: ${result.stats?.totalModifications || 0} modifications`, 'INFO'); return { content: result.content, stats: { ...result.stats, stackUsed: stackName, stackDescription: stack.description, expectedReduction: stack.expectedReduction }, fallback: result.fallback, stackUsed: stackName }; } catch (error) { logSh(`❌ Erreur application Stack Pattern Breaking ${stackName}: ${error.message}`, 'ERROR'); throw error; } } /** * ADAPTATION CONFIGURATION SELON CONTENU */ async function adaptConfigurationToContent(content, baseConfig) { const { detectLLMPatterns } = require('./LLMFingerprints'); const { detectFormalConnectors } = require('./NaturalConnectors'); logSh(`🧠 Adaptation configuration selon contenu...`, 'DEBUG'); const adaptations = { ...baseConfig }; try { // Analyser patterns LLM const llmDetection = detectLLMPatterns(content); const formalDetection = detectFormalConnectors(content); logSh(` 📊 Patterns LLM: ${llmDetection.count} (score: ${llmDetection.suspicionScore.toFixed(3)})`, 'DEBUG'); logSh(` 📊 Connecteurs formels: ${formalDetection.count} (score: ${formalDetection.suspicionScore.toFixed(3)})`, 'DEBUG'); // Adapter selon détection patterns LLM if (llmDetection.suspicionScore > 0.06) { adaptations.llmFingerprintReplacement = true; adaptations.intensityLevel = Math.min(1.0, baseConfig.intensityLevel + 0.2); logSh(` 🔧 Intensité augmentée pour patterns LLM élevés: ${adaptations.intensityLevel}`, 'DEBUG'); } else if (llmDetection.suspicionScore < 0.02) { adaptations.llmFingerprintReplacement = false; logSh(` 🔧 Remplacement LLM désactivé (faible détection)`, 'DEBUG'); } // Adapter selon connecteurs formels if (formalDetection.suspicionScore > 0.04) { adaptations.naturalConnectorsEnabled = true; adaptations.maxModificationsPerElement = Math.min(8, baseConfig.maxModificationsPerElement + 2); logSh(` 🔧 Focus connecteurs activé: max ${adaptations.maxModificationsPerElement} modifications`, 'DEBUG'); } // Adapter selon longueur texte const wordCount = content.split(/\s+/).length; if (wordCount > 500) { adaptations.maxModificationsPerElement = Math.min(10, baseConfig.maxModificationsPerElement + 3); logSh(` 🔧 Texte long détecté: max ${adaptations.maxModificationsPerElement} modifications`, 'DEBUG'); } } catch (error) { logSh(`⚠️ Erreur adaptation configuration: ${error.message}`, 'WARNING'); } return adaptations; } /** * RECOMMANDATION STACK AUTOMATIQUE */ function recommendPatternBreakingStack(content, context = {}) { const { detectLLMPatterns } = require('./LLMFingerprints'); const { detectFormalConnectors } = require('./NaturalConnectors'); try { const llmDetection = detectLLMPatterns(content); const formalDetection = detectFormalConnectors(content); const wordCount = content.split(/\s+/).length; logSh(`🤖 Recommandation Stack Pattern Breaking...`, 'DEBUG'); // Critères de recommandation const criteria = { llmPatternsHigh: llmDetection.suspicionScore > 0.05, formalConnectorsHigh: formalDetection.suspicionScore > 0.03, longContent: wordCount > 300, criticalContext: context.critical === true, preserveQuality: context.preserveQuality === true }; // Logique de recommandation let recommendedStack = 'standardPatternBreaking'; let reason = 'Configuration équilibrée par défaut'; if (criteria.criticalContext) { recommendedStack = 'heavyPatternBreaking'; reason = 'Contexte critique détecté'; } else if (criteria.llmPatternsHigh && criteria.formalConnectorsHigh) { recommendedStack = 'heavyPatternBreaking'; reason = 'Patterns LLM et connecteurs formels élevés'; } else if (criteria.llmPatternsHigh) { recommendedStack = 'adaptivePatternBreaking'; reason = 'Patterns LLM élevés détectés'; } else if (criteria.formalConnectorsHigh) { recommendedStack = 'connectorsFocus'; reason = 'Connecteurs formels prédominants'; } else if (criteria.preserveQuality) { recommendedStack = 'lightPatternBreaking'; reason = 'Préservation qualité prioritaire'; } else if (!criteria.llmPatternsHigh && !criteria.formalConnectorsHigh) { recommendedStack = 'syntaxFocus'; reason = 'Faible détection patterns, focus syntaxe'; } logSh(`🎯 Stack recommandé: ${recommendedStack} (${reason})`, 'DEBUG'); return { recommendedStack, reason, criteria, confidence: calculateRecommendationConfidence(criteria) }; } catch (error) { logSh(`⚠️ Erreur recommandation Stack: ${error.message}`, 'WARNING'); return { recommendedStack: 'standardPatternBreaking', reason: 'Fallback suite erreur analyse', criteria: {}, confidence: 0.5 }; } } /** * CALCUL CONFIANCE RECOMMANDATION */ function calculateRecommendationConfidence(criteria) { let confidence = 0.5; // Base // Augmenter confiance selon critères détectés if (criteria.llmPatternsHigh) confidence += 0.2; if (criteria.formalConnectorsHigh) confidence += 0.2; if (criteria.criticalContext) confidence += 0.3; if (criteria.longContent) confidence += 0.1; return Math.min(1.0, confidence); } /** * LISTE STACKS DISPONIBLES */ function listAvailableStacks() { return Object.entries(PATTERN_BREAKING_STACKS).map(([key, stack]) => ({ name: key, displayName: stack.name, description: stack.description, intensity: stack.intensity, expectedReduction: stack.expectedReduction, useCase: stack.useCase })); } /** * VALIDATION STACK */ function validateStack(stackName) { const stack = PATTERN_BREAKING_STACKS[stackName]; if (!stack) { return { valid: false, error: `Stack inconnu: ${stackName}` }; } // Vérifications configuration const config = stack.config; const checks = { hasIntensity: typeof stack.intensity === 'number', hasConfig: typeof config === 'object', hasValidThreshold: config.qualityThreshold >= 0 && config.qualityThreshold <= 1, hasValidMaxMods: config.maxModificationsPerElement > 0 }; const valid = Object.values(checks).every(Boolean); return { valid, checks, error: valid ? null : 'Configuration stack invalide' }; } // ============= EXPORTS ============= module.exports = { applyPatternBreakingStack, recommendPatternBreakingStack, adaptConfigurationToContent, listAvailableStacks, validateStack, PATTERN_BREAKING_STACKS }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/StepExecutor.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: StepExecutor.js // RESPONSABILITÉ: Exécution des étapes modulaires // ======================================== const { logSh } = require('./ErrorReporting'); /** * EXECUTEUR D'ÉTAPES MODULAIRES * Execute les différents systèmes étape par étape avec stats détaillées */ class StepExecutor { constructor() { // Mapping des systèmes vers leurs exécuteurs this.systems = { 'initial-generation': this.executeInitialGeneration.bind(this), 'selective': this.executeSelective.bind(this), 'adversarial': this.executeAdversarial.bind(this), 'human-simulation': this.executeHumanSimulation.bind(this), 'pattern-breaking': this.executePatternBreaking.bind(this) }; logSh('🎯 StepExecutor initialisé', 'DEBUG'); } // ======================================== // INTERFACE PRINCIPALE // ======================================== /** * Execute une étape spécifique */ async executeStep(system, inputData, options = {}) { const startTime = Date.now(); logSh(`🚀 Exécution étape: ${system}`, 'INFO'); try { // Vérifier que le système existe if (!this.systems[system]) { throw new Error(`Système inconnu: ${system}`); } // Préparer les données d'entrée const processedInput = this.preprocessInputData(inputData); // Executer le système const rawResult = await this.systems[system](processedInput, options); // Traiter le résultat const processedResult = await this.postprocessResult(rawResult, system); const duration = Date.now() - startTime; logSh(`✅ Étape ${system} terminée en ${duration}ms`, 'INFO'); return { success: true, system, result: processedResult.content, formatted: this.formatOutput(processedResult.content, 'tag'), xmlFormatted: this.formatOutput(processedResult.content, 'xml'), stats: { duration, tokensUsed: processedResult.tokensUsed || 0, cost: processedResult.cost || 0, llmCalls: processedResult.llmCalls || [], system: system, timestamp: Date.now() } }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ Erreur étape ${system}: ${error.message}`, 'ERROR'); return { success: false, system, error: error.message, stats: { duration, system: system, timestamp: Date.now(), error: true } }; } } // ======================================== // EXÉCUTEURS SPÉCIFIQUES // ======================================== /** * Construire la structure de contenu depuis la hiérarchie réelle */ buildContentStructureFromHierarchy(inputData, hierarchy) { const contentStructure = {}; // Si hiérarchie disponible, l'utiliser if (hierarchy && Object.keys(hierarchy).length > 0) { logSh(`🔍 Hiérarchie debug: ${Object.keys(hierarchy).length} sections`, 'DEBUG'); logSh(`🔍 Première section sample: ${JSON.stringify(Object.values(hierarchy)[0]).substring(0, 200)}`, 'DEBUG'); Object.entries(hierarchy).forEach(([path, section]) => { // Générer pour le titre si présent if (section.title && section.title.originalElement) { const tag = section.title.originalElement.name; const instruction = section.title.instructions || section.title.originalElement.instructions || `Rédige un titre pour ${inputData.mc0}`; contentStructure[tag] = instruction; } // Générer pour le texte si présent if (section.text && section.text.originalElement) { const tag = section.text.originalElement.name; const instruction = section.text.instructions || section.text.originalElement.instructions || `Rédige du contenu sur ${inputData.mc0}`; contentStructure[tag] = instruction; } // Générer pour les questions FAQ si présentes if (section.questions && section.questions.length > 0) { section.questions.forEach(q => { if (q.originalElement) { const tag = q.originalElement.name; const instruction = q.instructions || q.originalElement.instructions || `Rédige une question/réponse FAQ sur ${inputData.mc0}`; contentStructure[tag] = instruction; } }); } }); logSh(`🏗️ Structure depuis hiérarchie: ${Object.keys(contentStructure).length} éléments`, 'DEBUG'); } else { // Fallback: structure générique si pas de hiérarchie logSh(`⚠️ Pas de hiérarchie, utilisation structure générique`, 'WARNING'); contentStructure['Titre_H1'] = `Rédige un titre H1 accrocheur et optimisé SEO sur ${inputData.mc0}`; contentStructure['Introduction'] = `Rédige une introduction engageante qui présente ${inputData.mc0}`; contentStructure['Contenu_Principal'] = `Développe le contenu principal détaillé sur ${inputData.mc0} avec des informations utiles et techniques`; contentStructure['Conclusion'] = `Rédige une conclusion percutante qui encourage à l'action pour ${inputData.mc0}`; } return contentStructure; } /** * Execute Initial Generation */ async executeInitialGeneration(inputData, options = {}) { try { const { InitialGenerationLayer } = require('./generation/InitialGeneration'); logSh('🎯 Démarrage Génération Initiale', 'DEBUG'); const config = { temperature: options.temperature || 0.7, maxTokens: options.maxTokens || 4000 }; // Créer la structure de contenu à générer depuis la hiérarchie réelle // La hiérarchie peut être dans inputData.hierarchy OU options.hierarchy const hierarchy = options.hierarchy || inputData.hierarchy; const contentStructure = this.buildContentStructureFromHierarchy(inputData, hierarchy); logSh(`📊 Structure construite: ${Object.keys(contentStructure).length} éléments depuis hiérarchie`, 'DEBUG'); const initialGenerator = new InitialGenerationLayer(); const result = await initialGenerator.apply(contentStructure, { ...config, csvData: inputData, llmProvider: 'claude' }); return { content: result.content || result, tokensUsed: result.stats?.tokensUsed || 200, cost: (result.stats?.tokensUsed || 200) * 0.00002, llmCalls: [ { provider: 'claude', tokens: result.stats?.tokensUsed || 200, cost: 0.004, phase: 'initial_generation' } ], phases: { initialGeneration: result.stats }, beforeAfter: { before: contentStructure, after: result.content } }; } catch (error) { logSh(`❌ Erreur Initial Generation: ${error.message}`, 'ERROR'); return this.createFallbackContent('initial-generation', inputData, error); } } /** * Execute Selective Enhancement */ async executeSelective(inputData, options = {}) { try { // Import dynamique pour éviter les dépendances circulaires const { applyPredefinedStack } = require('./selective-enhancement/SelectiveLayers'); logSh('🎯 Démarrage Selective Enhancement seulement', 'DEBUG'); const config = { selectiveStack: options.selectiveStack || 'standardEnhancement', temperature: options.temperature || 0.7, maxTokens: options.maxTokens || 3000 }; // Vérifier si on a du contenu à améliorer let contentToEnhance = null; if (options.inputContent && Object.keys(options.inputContent).length > 0) { // Utiliser le contenu fourni contentToEnhance = options.inputContent; } else { // Fallback: créer un contenu basique pour le test logSh('⚠️ Pas de contenu d\'entrée, création d\'un contenu basique pour test', 'WARNING'); contentToEnhance = { 'Titre_H1': inputData.t0 || 'Titre principal', 'Introduction': `Contenu sur ${inputData.mc0}`, 'Contenu_Principal': `Développement du sujet ${inputData.mc0}`, 'Conclusion': `Conclusion sur ${inputData.mc0}` }; } const beforeContent = JSON.parse(JSON.stringify(contentToEnhance)); // Deep copy // ÉTAPE ENHANCEMENT - Améliorer le contenu fourni avec la stack spécifiée logSh(`🎯 Enhancement sélectif du contenu avec stack: ${config.selectiveStack}`, 'DEBUG'); const result = await applyPredefinedStack(contentToEnhance, config.selectiveStack, { csvData: inputData, analysisMode: false }); return { content: result.content || result, tokensUsed: result.tokensUsed || 300, cost: (result.tokensUsed || 300) * 0.00002, llmCalls: result.llmCalls || [ { provider: 'gpt4', tokens: 100, cost: 0.002, phase: 'technical_enhancement' }, { provider: 'gemini', tokens: 100, cost: 0.001, phase: 'transition_enhancement' }, { provider: 'mistral', tokens: 100, cost: 0.0005, phase: 'style_enhancement' } ], phases: { selectiveEnhancement: result.stats }, beforeAfter: { before: beforeContent, after: result.content || result } }; } catch (error) { logSh(`❌ Erreur Selective: ${error.message}`, 'ERROR'); // Fallback avec contenu simulé pour le développement return this.createFallbackContent('selective', inputData, error); } } /** * Execute Adversarial Generation */ async executeAdversarial(inputData, options = {}) { try { const { applyPredefinedStack: applyAdversarialStack } = require('./adversarial-generation/AdversarialLayers'); logSh('🎯 Démarrage Adversarial Generation', 'DEBUG'); const config = { adversarialMode: options.adversarialMode || 'standard', temperature: options.temperature || 1.0, antiDetectionLevel: options.antiDetectionLevel || 'medium' }; // Vérifier si on a du contenu à transformer let contentToTransform = null; if (options.inputContent && Object.keys(options.inputContent).length > 0) { contentToTransform = options.inputContent; } else { // Fallback: créer un contenu basique pour le test logSh('⚠️ Pas de contenu d\'entrée, création d\'un contenu basique pour test', 'WARNING'); contentToTransform = { 'Titre_H1': inputData.t0 || 'Titre principal', 'Introduction': `Contenu sur ${inputData.mc0}`, 'Contenu_Principal': `Développement du sujet ${inputData.mc0}`, 'Conclusion': `Conclusion sur ${inputData.mc0}` }; } const beforeContent = JSON.parse(JSON.stringify(contentToTransform)); // Deep copy // Mapping des modes vers les stacks prédéfinies const modeToStack = { 'light': 'lightDefense', 'standard': 'standardDefense', 'heavy': 'heavyDefense', 'none': 'none', 'adaptive': 'adaptive' }; const stackName = modeToStack[config.adversarialMode] || 'standardDefense'; logSh(`🎯 Adversarial avec stack: ${stackName} (mode: ${config.adversarialMode})`, 'DEBUG'); const result = await applyAdversarialStack(contentToTransform, stackName, { csvData: inputData, detectorTarget: config.detectorTarget || 'general', intensity: config.intensity || 1.0 }); return { content: result.content || result, tokensUsed: result.tokensUsed || 200, cost: (result.tokensUsed || 200) * 0.00002, llmCalls: result.llmCalls || [ { provider: 'claude', tokens: 100, cost: 0.002, phase: 'adversarial_generation' }, { provider: 'mistral', tokens: 100, cost: 0.0005, phase: 'adversarial_enhancement' } ], phases: { adversarialGeneration: result.stats }, beforeAfter: { before: beforeContent, after: result.content || result } }; } catch (error) { logSh(`❌ Erreur Adversarial: ${error.message}`, 'ERROR'); return this.createFallbackContent('adversarial', inputData, error); } } /** * Execute Human Simulation */ async executeHumanSimulation(inputData, options = {}) { try { const { applyPredefinedSimulation } = require('./human-simulation/HumanSimulationLayers'); logSh('🎯 Démarrage Human Simulation', 'DEBUG'); const config = { humanSimulationMode: options.humanSimulationMode || 'standardSimulation', personalityFactor: options.personalityFactor || 0.7, fatigueLevel: options.fatigueLevel || 'medium' }; // Vérifier si on a du contenu à humaniser let contentToHumanize = null; if (options.inputContent && Object.keys(options.inputContent).length > 0) { contentToHumanize = options.inputContent; } else { // Fallback: créer un contenu basique pour le test logSh('⚠️ Pas de contenu d\'entrée, création d\'un contenu basique pour test', 'WARNING'); contentToHumanize = { 'Titre_H1': inputData.t0 || 'Titre principal', 'Introduction': `Contenu sur ${inputData.mc0}`, 'Contenu_Principal': `Développement du sujet ${inputData.mc0}`, 'Conclusion': `Conclusion sur ${inputData.mc0}` }; } const beforeContent = JSON.parse(JSON.stringify(contentToHumanize)); // Deep copy const simulationMode = config.humanSimulationMode || 'standardSimulation'; logSh(`🎯 Human Simulation avec mode: ${simulationMode}`, 'DEBUG'); const result = await applyPredefinedSimulation(contentToHumanize, simulationMode, { csvData: inputData, ...config }); return { content: result.content || result, tokensUsed: result.tokensUsed || 180, cost: (result.tokensUsed || 180) * 0.00002, llmCalls: result.llmCalls || [ { provider: 'gemini', tokens: 90, cost: 0.0009, phase: 'human_simulation' }, { provider: 'claude', tokens: 90, cost: 0.0018, phase: 'personality_application' } ], phases: { humanSimulation: result.stats }, beforeAfter: { before: beforeContent, after: result.content || result } }; } catch (error) { logSh(`❌ Erreur Human Simulation: ${error.message}`, 'ERROR'); return this.createFallbackContent('human-simulation', inputData, error); } } /** * Execute Pattern Breaking */ async executePatternBreaking(inputData, options = {}) { try { const { applyPatternBreakingStack } = require('./pattern-breaking/PatternBreakingLayers'); logSh('🎯 Démarrage Pattern Breaking', 'DEBUG'); const config = { patternBreakingMode: options.patternBreakingMode || 'standardPatternBreaking', syntaxVariation: options.syntaxVariation || 0.6, connectorDiversity: options.connectorDiversity || 0.8 }; // Vérifier si on a du contenu à transformer let contentToTransform = null; if (options.inputContent && Object.keys(options.inputContent).length > 0) { contentToTransform = options.inputContent; } else { // Fallback: créer un contenu basique pour le test logSh('⚠️ Pas de contenu d\'entrée, création d\'un contenu basique pour test', 'WARNING'); contentToTransform = { 'Titre_H1': inputData.t0 || 'Titre principal', 'Introduction': `Contenu sur ${inputData.mc0}`, 'Contenu_Principal': `Développement du sujet ${inputData.mc0}`, 'Conclusion': `Conclusion sur ${inputData.mc0}` }; } const beforeContent = JSON.parse(JSON.stringify(contentToTransform)); // Deep copy const patternMode = config.patternBreakingMode || 'standardPatternBreaking'; logSh(`🎯 Pattern Breaking avec mode: ${patternMode}`, 'DEBUG'); const result = await applyPatternBreakingStack(contentToTransform, patternMode, { csvData: inputData, ...config }); return { content: result.content || result, tokensUsed: result.tokensUsed || 120, cost: (result.tokensUsed || 120) * 0.00002, llmCalls: result.llmCalls || [ { provider: 'gpt4', tokens: 60, cost: 0.0012, phase: 'pattern_analysis' }, { provider: 'mistral', tokens: 60, cost: 0.0003, phase: 'pattern_breaking' } ], phases: { patternBreaking: result.stats }, beforeAfter: { before: beforeContent, after: result.content || result } }; } catch (error) { logSh(`❌ Erreur Pattern Breaking: ${error.message}`, 'ERROR'); return this.createFallbackContent('pattern-breaking', inputData, error); } } // ======================================== // HELPERS ET FORMATAGE // ======================================== /** * Préprocesse les données d'entrée */ preprocessInputData(inputData) { return { mc0: inputData.mc0 || 'mot-clé principal', t0: inputData.t0 || 'titre principal', mcPlus1: inputData.mcPlus1 || '', tPlus1: inputData.tPlus1 || '', personality: inputData.personality || { nom: 'Test', style: 'neutre' }, xmlTemplate: inputData.xmlTemplate || this.getDefaultTemplate(), // Ajout d'un contexte pour les modules context: { timestamp: Date.now(), source: 'step-by-step', debug: true } }; } /** * Post-traite le résultat */ async postprocessResult(rawResult, system) { // Si le résultat est juste une chaîne, la transformer en objet if (typeof rawResult === 'string') { return { content: { 'Contenu': rawResult }, tokensUsed: Math.floor(rawResult.length / 4), // Estimation cost: 0.001, llmCalls: [{ provider: 'unknown', tokens: 50, cost: 0.001 }] }; } // Si c'est déjà un objet structuré, le retourner tel quel if (rawResult && typeof rawResult === 'object') { return rawResult; } // Fallback return { content: { 'Résultat': String(rawResult) }, tokensUsed: 50, cost: 0.001, llmCalls: [] }; } /** * Formate la sortie selon le format demandé */ formatOutput(content, format = 'tag') { if (!content || typeof content !== 'object') { return String(content || 'Pas de contenu'); } switch (format) { case 'tag': return Object.entries(content) .map(([tag, text]) => `[${tag}]\n${text}`) .join('\n\n'); case 'xml': return Object.entries(content) .map(([tag, text]) => `<${tag.toLowerCase()}>${text}`) .join('\n'); case 'json': return JSON.stringify(content, null, 2); default: return this.formatOutput(content, 'tag'); } } /** * Crée un contenu de fallback pour les erreurs */ createFallbackContent(system, inputData, error) { const fallbackContent = { 'Titre_H1': `${inputData.t0} - Traité par ${system}`, 'Introduction': `Contenu généré en mode ${system} pour "${inputData.mc0}".`, 'Contenu_Principal': `Ceci est un contenu de démonstration pour le système ${system}. En production, ce contenu serait généré par l'IA avec les paramètres spécifiés.`, 'Note_Technique': `⚠️ Mode fallback activé - Erreur: ${error.message}` }; return { content: fallbackContent, tokensUsed: 100, cost: 0.002, llmCalls: [ { provider: 'fallback', tokens: 100, cost: 0.002, error: error.message } ], fallback: true }; } /** * Template XML par défaut */ getDefaultTemplate() { return `

|Titre_H1{{T0}}{Titre principal optimisé}|

|Introduction{{MC0}}{Introduction engageante}| |Contenu_Principal{{MC0,T0}}{Contenu principal détaillé}| |Conclusion{{T0}}{Conclusion percutante}|
`; } } module.exports = { StepExecutor }; /* ┌────────────────────────────────────────────────────────────────────┐ │ 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 ? elements.length : 'undefined'} éléments`, 'DEBUG'); // Fix: s'assurer que elements est un array if (!Array.isArray(elements)) { logSh(`⚠ Elements n'est pas un array, type: ${typeof elements}`, 'WARN'); elements = []; } // Debug: montrer le XML logSh(`🔍 XML début: ${cleanXML}`, 'DEBUG'); // Debug: montrer le contenu généré Object.keys(generatedContent).forEach(key => { logSh(`🔍 Généré [${key}]: "${generatedContent[key]}"`, 'DEBUG'); }); // Debug: montrer les éléments elements.forEach((element, i) => { logSh(`🔍 Element ${i+1}: originalTag="${element.originalTag}", originalFullMatch="${element.originalFullMatch}"`, 'DEBUG'); }); let finalXML = cleanXML; // Créer un mapping tag pur → tag original complet const tagMapping = {}; elements.forEach(element => { tagMapping[element.originalTag] = element.originalFullMatch || element.originalTag; }); logSh(`🔍 TagMapping créé: ${JSON.stringify(tagMapping, null, 2)}`, 'DEBUG'); // Remplacer en utilisant les tags originaux complets Object.keys(generatedContent).forEach(pureTag => { const content = generatedContent[pureTag]; logSh(`🔍 === TRAITEMENT TAG: ${pureTag} ===`, 'DEBUG'); logSh(`🔍 Contenu à injecter: "${content}"`, 'DEBUG'); // Trouver le tag original complet dans le XML const originalTag = findOriginalTagInXML(finalXML, pureTag); logSh(`🔍 Tag original trouvé: ${originalTag ? originalTag : 'AUCUN'}`, 'DEBUG'); if (originalTag) { const beforeLength = finalXML.length; finalXML = finalXML.replace(originalTag, content); const afterLength = finalXML.length; if (beforeLength !== afterLength) { logSh(`✅ SUCCÈS: Remplacé ${originalTag} par contenu (${afterLength - beforeLength + originalTag.length} chars)`, 'DEBUG'); } else { logSh(`❌ ÉCHEC: Replace n'a pas fonctionné pour ${originalTag}`, 'DEBUG'); } } else { // Fallback : essayer avec le tag pur const beforeLength = finalXML.length; finalXML = finalXML.replace(pureTag, content); const afterLength = finalXML.length; logSh(`⚠ FALLBACK ${pureTag}: remplacement ${beforeLength !== afterLength ? 'RÉUSSI' : 'ÉCHOUÉ'}`, 'DEBUG'); logSh(`⚠ Contenu fallback: "${content}"`, 'DEBUG'); } }); // Vérifier les tags restants const remainingTags = (finalXML.match(/\|[^|]*\|/g) || []); if (remainingTags.length > 0) { logSh(`ATTENTION: ${remainingTags.length} tags non remplacés: ${remainingTags.slice(0, 3).join(', ')}...`, 'WARNING'); } logSh('Injection terminée', 'INFO'); return finalXML; } /** * Helper pour trouver le tag original complet dans le XML * @param {string} xmlString - Contenu XML * @param {string} pureTag - Tag pur à rechercher * @returns {string|null} - Tag original trouvé ou null */ function findOriginalTagInXML(xmlString, pureTag) { logSh(`🔍 === RECHERCHE TAG DANS XML ===`, 'DEBUG'); logSh(`🔍 Tag pur recherché: "${pureTag}"`, 'DEBUG'); // Extraire le nom du tag pur : |Titre_H1_1| → Titre_H1_1 const tagName = pureTag.replace(/\|/g, ''); logSh(`🔍 Nom tag extrait: "${tagName}"`, 'DEBUG'); // Chercher tous les tags qui commencent par ce nom (avec espaces optionnels) const regex = new RegExp(`\\|\\s*${tagName}[^|]*\\|`, 'g'); logSh(`🔍 Regex utilisée: ${regex}`, 'DEBUG'); // Debug: montrer tous les tags présents dans le XML const allTags = xmlString.match(/\|[^|]*\|/g) || []; logSh(`🔍 Tags présents dans XML: ${allTags.length}`, 'DEBUG'); allTags.forEach((tag, i) => { logSh(`🔍 ${i+1}. "${tag}"`, 'DEBUG'); }); const matches = xmlString.match(regex); logSh(`🔍 Matches trouvés: ${matches ? matches.length : 0}`, 'DEBUG'); if (matches && matches.length > 0) { logSh(`🔍 Premier match: "${matches[0]}"`, 'DEBUG'); logSh(`✅ Tag original trouvé pour ${pureTag}: ${matches[0]}`, 'DEBUG'); return matches[0]; } logSh(`❌ Aucun tag original trouvé pour ${pureTag}`, 'DEBUG'); return null; } // ============= EXPORTS ============= module.exports = { cleanStrongTags, replaceAllCSVVariables, injectGeneratedContent, findOriginalTagInXML }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/ArticleStorage.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: ArticleStorage.js // Description: Système de sauvegarde articles avec texte compilé uniquement // ======================================== require('dotenv').config(); const { google } = require('googleapis'); const { logSh } = require('./ErrorReporting'); // Configuration Google Sheets const SHEET_CONFIG = { sheetId: '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c' }; /** * NOUVELLE FONCTION : Compiler le contenu de manière organique * Respecte la hiérarchie et les associations naturelles */ async function compileGeneratedTextsOrganic(generatedTexts, elements) { if (!generatedTexts || Object.keys(generatedTexts).length === 0) { return ''; } logSh(`🌱 Compilation ORGANIQUE de ${Object.keys(generatedTexts).length} éléments...`, 'DEBUG'); let compiledParts = []; // 1. DÉTECTER et GROUPER les sections organiques const organicSections = buildOrganicSections(generatedTexts, elements); // 2. COMPILER dans l'ordre naturel organicSections.forEach(section => { if (section.type === 'header_with_content') { // H1, H2, H3 avec leur contenu associé if (section.title) { compiledParts.push(cleanIndividualContent(section.title)); } if (section.content) { compiledParts.push(cleanIndividualContent(section.content)); } } else if (section.type === 'standalone_content') { // Contenu sans titre associé compiledParts.push(cleanIndividualContent(section.content)); } else if (section.type === 'faq_pair') { // Paire question + réponse if (section.question && section.answer) { compiledParts.push(cleanIndividualContent(section.question)); compiledParts.push(cleanIndividualContent(section.answer)); } } }); // 3. Joindre avec espacement naturel const finalText = compiledParts.join('\n\n'); logSh(`✅ Compilation organique terminée: ${finalText.length} caractères`, 'INFO'); return finalText.trim(); } /** * Construire les sections organiques en analysant les associations */ function buildOrganicSections(generatedTexts, elements) { const sections = []; const usedTags = new Set(); // 🔧 FIX: Gérer le cas où elements est null/undefined if (!elements) { logSh('⚠️ Elements null, utilisation compilation simple', 'DEBUG'); // Compilation simple : tout le contenu dans l'ordre des clés Object.keys(generatedTexts).forEach(tag => { sections.push({ type: 'standalone_content', content: generatedTexts[tag], tag: tag }); }); return sections; } // 1. ANALYSER l'ordre original des éléments const originalOrder = elements.map(el => el.originalTag); logSh(`📋 Analyse de ${originalOrder.length} éléments dans l'ordre original...`, 'DEBUG'); // 2. DÉTECTER les associations naturelles for (let i = 0; i < originalOrder.length; i++) { const currentTag = originalOrder[i]; const currentContent = generatedTexts[currentTag]; if (!currentContent || usedTags.has(currentTag)) continue; const currentType = identifyElementType(currentTag); if (currentType === 'titre_h1' || currentType === 'titre_h2' || currentType === 'titre_h3') { // CHERCHER le contenu associé qui suit const associatedContent = findAssociatedContent(originalOrder, i, generatedTexts, usedTags); sections.push({ type: 'header_with_content', title: currentContent, content: associatedContent.content, titleTag: currentTag, contentTag: associatedContent.tag }); usedTags.add(currentTag); if (associatedContent.tag) { usedTags.add(associatedContent.tag); } logSh(` ✓ Section: ${currentType} + contenu associé`, 'DEBUG'); } else if (currentType === 'faq_question') { // CHERCHER la réponse correspondante const matchingAnswer = findMatchingFAQAnswer(currentTag, generatedTexts); if (matchingAnswer) { sections.push({ type: 'faq_pair', question: currentContent, answer: matchingAnswer.content, questionTag: currentTag, answerTag: matchingAnswer.tag }); usedTags.add(currentTag); usedTags.add(matchingAnswer.tag); logSh(` ✓ Paire FAQ: ${currentTag} + ${matchingAnswer.tag}`, 'DEBUG'); } } else if (currentType !== 'faq_reponse') { // CONTENU STANDALONE (pas une réponse FAQ déjà traitée) sections.push({ type: 'standalone_content', content: currentContent, contentTag: currentTag }); usedTags.add(currentTag); logSh(` ✓ Contenu standalone: ${currentType}`, 'DEBUG'); } } logSh(`🏗️ ${sections.length} sections organiques construites`, 'INFO'); return sections; } /** * Trouver le contenu associé à un titre (paragraphe qui suit) */ function findAssociatedContent(originalOrder, titleIndex, generatedTexts, usedTags) { // Chercher dans les éléments suivants for (let j = titleIndex + 1; j < originalOrder.length; j++) { const nextTag = originalOrder[j]; const nextContent = generatedTexts[nextTag]; if (!nextContent || usedTags.has(nextTag)) continue; const nextType = identifyElementType(nextTag); // Si on trouve un autre titre, on s'arrête if (nextType === 'titre_h1' || nextType === 'titre_h2' || nextType === 'titre_h3') { break; } // Si on trouve du contenu (texte, intro), c'est probablement associé if (nextType === 'texte' || nextType === 'intro') { return { content: nextContent, tag: nextTag }; } } return { content: null, tag: null }; } /** * Extraire le numéro d'une FAQ : |Faq_q_1| ou |Faq_a_2| → "1" ou "2" */ function extractFAQNumber(tag) { const match = tag.match(/(\d+)/); return match ? match[1] : null; } /** * Trouver la réponse FAQ correspondant à une question */ function findMatchingFAQAnswer(questionTag, generatedTexts) { // Extraire le numéro : |Faq_q_1| → 1 const questionNumber = extractFAQNumber(questionTag); if (!questionNumber) return null; // Chercher la réponse correspondante for (const tag in generatedTexts) { const tagType = identifyElementType(tag); if (tagType === 'faq_reponse') { const answerNumber = extractFAQNumber(tag); if (answerNumber === questionNumber) { return { content: generatedTexts[tag], tag: tag }; } } } return null; } /** * Nouvelle fonction de sauvegarde avec compilation organique */ async function saveGeneratedArticleOrganic(articleData, csvData, config = {}) { try { logSh('💾 Sauvegarde article avec compilation organique...', 'INFO'); const sheets = await getSheetsClient(); // 🆕 Choisir la sheet selon le flag useVersionedSheet const targetSheetName = config.useVersionedSheet ? 'Generated_Articles_Versioned' : 'Generated_Articles'; let articlesSheet = await getOrCreateSheet(sheets, targetSheetName); // ===== COMPILATION ORGANIQUE ===== const compiledText = await compileGeneratedTextsOrganic( articleData.generatedTexts, articleData.originalElements // Passer les éléments originaux si disponibles ); logSh(`📝 Texte compilé organiquement: ${compiledText.length} caractères`, 'INFO'); // Métadonnées avec format français const now = new Date(); const frenchTimestamp = formatDateToFrench(now); // UTILISER le slug du CSV (colonne A du Google Sheet source) // Le slug doit venir de csvData.slug (récupéré via getBrainConfig) const slug = csvData.slug || generateSlugFromContent(csvData.mc0, csvData.t0); const metadata = { timestamp: frenchTimestamp, slug: slug, mc0: csvData.mc0, t0: csvData.t0, personality: csvData.personality?.nom || 'Unknown', antiDetectionLevel: config.antiDetectionLevel || config.adversarialMode || 'none', elementsCount: Object.keys(articleData.generatedTexts || {}).length, textLength: compiledText.length, wordCount: countWords(compiledText), llmUsed: config.llmUsed || 'openai', validationStatus: articleData.validationReport?.status || 'unknown', // 🆕 Métadonnées de versioning version: config.version || '1.0', stage: config.stage || 'final_version', stageDescription: config.stageDescription || 'Version finale', parentArticleId: config.parentArticleId || null, versionHistory: config.versionHistory || null }; // Préparer la ligne de données selon le format de la sheet let row; if (config.useVersionedSheet) { // Format VERSIONED (21 colonnes) : avec version, stage, stageDescription, parentArticleId row = [ metadata.timestamp, metadata.slug, metadata.mc0, metadata.t0, metadata.personality, metadata.antiDetectionLevel, compiledText, metadata.textLength, metadata.wordCount, metadata.elementsCount, metadata.llmUsed, metadata.validationStatus, metadata.version, // Colonne M metadata.stage, // Colonne N metadata.stageDescription, // Colonne O metadata.parentArticleId || '', // Colonne P '', '', '', '', // Colonnes Q,R,S,T : scores détecteurs (réservées) JSON.stringify({ // Colonne U csvData: { ...csvData, xmlTemplate: undefined, xmlFileName: csvData.xmlFileName }, config: config, stats: metadata, versionHistory: metadata.versionHistory }) ]; } else { // Format LEGACY (17 colonnes) : sans version/stage, scores détecteurs à la place row = [ metadata.timestamp, metadata.slug, metadata.mc0, metadata.t0, metadata.personality, metadata.antiDetectionLevel, compiledText, metadata.textLength, metadata.wordCount, metadata.elementsCount, metadata.llmUsed, metadata.validationStatus, '', '', '', '', // Colonnes M,N,O,P : scores détecteurs (GPTZero, Originality, CopyLeaks, HumanQuality) JSON.stringify({ // Colonne Q csvData: { ...csvData, xmlTemplate: undefined, xmlFileName: csvData.xmlFileName }, 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 dans la bonne sheet // Forcer le range à A1 pour éviter le décalage horizontal const targetRange = config.useVersionedSheet ? 'Generated_Articles_Versioned!A1' : 'Generated_Articles!A1'; logSh(`🔍 DEBUG APPEND: sheetId=${SHEET_CONFIG.sheetId}, range=${targetRange}, rowLength=${row.length}`, 'INFO'); logSh(`🔍 DEBUG ROW PREVIEW: [${row.slice(0, 5).map(c => typeof c === 'string' ? c.substring(0, 50) : c).join(', ')}...]`, 'INFO'); const appendResult = await sheets.spreadsheets.values.append({ spreadsheetId: SHEET_CONFIG.sheetId, range: targetRange, valueInputOption: 'USER_ENTERED', insertDataOption: 'INSERT_ROWS', // Force l'insertion d'une nouvelle ligne resource: { values: [row] } }); logSh(`✅ APPEND SUCCESS: ${appendResult.status} - Updated ${appendResult.data.updates?.updatedCells || 0} cells`, 'INFO'); // Récupérer le numéro de ligne pour l'ID article const targetRangeForId = config.useVersionedSheet ? 'Generated_Articles_Versioned!A:A' : 'Generated_Articles!A:A'; const response = await sheets.spreadsheets.values.get({ spreadsheetId: SHEET_CONFIG.sheetId, range: targetRangeForId }); const articleId = response.data.values ? response.data.values.length : 1; logSh(`✅ Article organique sauvé: ID ${articleId}, ${metadata.wordCount} mots`, 'INFO'); return { articleId: articleId, textLength: metadata.textLength, wordCount: metadata.wordCount, sheetRow: response.data.values ? response.data.values.length : 2 }; } catch (error) { logSh(`❌ Erreur sauvegarde organique: ${error.toString()}`, 'ERROR'); throw error; } } /** * Générer un slug à partir du contenu MC0 et T0 */ function generateSlugFromContent(mc0, t0) { if (!mc0 && !t0) return 'article-generated'; const source = mc0 || t0; return source .toString() .toLowerCase() .replace(/[àáâäã]/g, 'a') .replace(/[èéêë]/g, 'e') .replace(/[ìíîï]/g, 'i') .replace(/[òóôöõ]/g, 'o') .replace(/[ùúûü]/g, 'u') .replace(/[ç]/g, 'c') .replace(/[ñ]/g, 'n') .replace(/[^a-z0-9\s-]/g, '') // Enlever caractères spéciaux .replace(/\s+/g, '-') // Espaces -> tirets .replace(/-+/g, '-') // Éviter doubles tirets .replace(/^-+|-+$/g, '') // Enlever tirets début/fin .substring(0, 50); // Limiter longueur } /** * Identifier le type d'élément par son tag */ function identifyElementType(tag) { const cleanTag = tag.toLowerCase().replace(/[|{}]/g, ''); if (cleanTag.includes('titre_h1') || cleanTag.includes('h1')) return 'titre_h1'; if (cleanTag.includes('titre_h2') || cleanTag.includes('h2')) return 'titre_h2'; if (cleanTag.includes('titre_h3') || cleanTag.includes('h3')) return 'titre_h3'; if (cleanTag.includes('intro')) return 'intro'; if (cleanTag.includes('faq_q') || cleanTag.includes('faq_question')) return 'faq_question'; if (cleanTag.includes('faq_a') || cleanTag.includes('faq_reponse')) return 'faq_reponse'; return 'texte'; // Par défaut } /** * Nettoyer un contenu individuel */ function cleanIndividualContent(content) { if (!content) return ''; let cleaned = content.toString(); // 1. Supprimer les balises HTML cleaned = cleaned.replace(/<[^>]*>/g, ''); // 2. Décoder les entités HTML cleaned = cleaned.replace(/</g, '<'); cleaned = cleaned.replace(/>/g, '>'); cleaned = cleaned.replace(/&/g, '&'); cleaned = cleaned.replace(/"/g, '"'); cleaned = cleaned.replace(/'/g, "'"); cleaned = cleaned.replace(/ /g, ' '); // 3. Nettoyer les espaces cleaned = cleaned.replace(/\s+/g, ' '); cleaned = cleaned.replace(/\n\s+/g, '\n'); // 4. Supprimer les caractères de contrôle étranges cleaned = cleaned.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); return cleaned.trim(); } /** * Créer la sheet de stockage avec headers appropriés */ async function createArticlesStorageSheet(sheets, sheetName = 'Generated_Articles') { logSh(`🗄️ Création sheet ${sheetName}...`, 'INFO'); try { // Créer la nouvelle sheet await sheets.spreadsheets.batchUpdate({ spreadsheetId: SHEET_CONFIG.sheetId, resource: { requests: [{ addSheet: { properties: { title: sheetName } } }] } }); // Headers avec versioning const headers = [ 'Timestamp', 'Slug', 'MC0', 'T0', 'Personality', 'AntiDetection_Level', 'Compiled_Text', // ← COLONNE PRINCIPALE 'Text_Length', 'Word_Count', 'Elements_Count', 'LLM_Used', 'Validation_Status', // 🆕 Colonnes de versioning 'Version', // v1.0, v1.1, v1.2, v2.0 'Stage', // initial_generation, selective_enhancement, etc. 'Stage_Description', // Description détaillée de l'étape 'Parent_Article_ID', // ID de l'article parent (pour linkage) 'GPTZero_Score', // Scores détecteurs (à remplir) 'Originality_Score', 'CopyLeaks_Score', 'Human_Quality_Score', 'Full_Metadata_JSON' // Backup complet avec historique ]; // Ajouter les headers await sheets.spreadsheets.values.update({ spreadsheetId: SHEET_CONFIG.sheetId, range: `${sheetName}!A1:U1`, valueInputOption: 'USER_ENTERED', resource: { values: [headers] } }); // Formatter les headers await sheets.spreadsheets.batchUpdate({ spreadsheetId: SHEET_CONFIG.sheetId, resource: { requests: [{ repeatCell: { range: { sheetId: await getSheetIdByName(sheets, sheetName), startRowIndex: 0, endRowIndex: 1, startColumnIndex: 0, endColumnIndex: headers.length }, cell: { userEnteredFormat: { textFormat: { bold: true }, backgroundColor: { red: 0.878, green: 0.878, blue: 0.878 }, horizontalAlignment: 'CENTER' } }, fields: 'userEnteredFormat(textFormat,backgroundColor,horizontalAlignment)' } }] } }); logSh(`✅ Sheet ${sheetName} créée avec succès`, 'INFO'); return true; } catch (error) { logSh(`❌ Erreur création sheet: ${error.toString()}`, 'ERROR'); throw error; } } /** * Formater date au format français DD/MM/YYYY HH:mm:ss */ function formatDateToFrench(date) { // Utiliser toLocaleString avec le format français return date.toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZone: 'Europe/Paris' }).replace(',', ''); } /** * Compter les mots dans un texte */ function countWords(text) { if (!text || text.trim() === '') return 0; return text.trim().split(/\s+/).length; } /** * Récupérer un article sauvé par ID */ async function getStoredArticle(articleId) { try { const sheets = await getSheetsClient(); const rowNumber = articleId + 2; // +2 car header + 0-indexing const response = await sheets.spreadsheets.values.get({ spreadsheetId: SHEET_CONFIG.sheetId, range: `Generated_Articles!A${rowNumber}:Q${rowNumber}` }); if (!response.data.values || response.data.values.length === 0) { throw new Error(`Article ${articleId} non trouvé`); } const data = response.data.values[0]; return { articleId: articleId, timestamp: data[0], slug: data[1], mc0: data[2], t0: data[3], personality: data[4], antiDetectionLevel: data[5], compiledText: data[6], // ← TEXTE PUR textLength: data[7], wordCount: data[8], elementsCount: data[9], llmUsed: data[10], validationStatus: data[11], gptZeroScore: data[12], originalityScore: data[13], copyLeaksScore: data[14], humanScore: data[15], fullMetadata: data[16] ? JSON.parse(data[16]) : null }; } catch (error) { logSh(`❌ Erreur récupération article ${articleId}: ${error.toString()}`, 'ERROR'); throw error; } } /** * Lister les derniers articles générés */ async function getRecentArticles(limit = 10) { try { const sheets = await getSheetsClient(); const response = await sheets.spreadsheets.values.get({ spreadsheetId: SHEET_CONFIG.sheetId, range: 'Generated_Articles!A:L' }); if (!response.data.values || response.data.values.length <= 1) { return []; // Pas de données ou seulement headers } const data = response.data.values.slice(1); // Exclure headers const startIndex = Math.max(0, data.length - limit); const recentData = data.slice(startIndex); return recentData.map((row, index) => ({ articleId: startIndex + index, timestamp: row[0], slug: row[1], mc0: row[2], personality: row[4], antiDetectionLevel: row[5], wordCount: row[8], validationStatus: row[11] })).reverse(); // Plus récents en premier } catch (error) { logSh(`❌ Erreur liste articles récents: ${error.toString()}`, 'ERROR'); return []; } } /** * Mettre à jour les scores de détection d'un article */ async function updateDetectionScores(articleId, scores) { try { const sheets = await getSheetsClient(); const rowNumber = articleId + 2; const updates = []; // Colonnes des scores : M, N, O (GPTZero, Originality, CopyLeaks) if (scores.gptzero !== undefined) { updates.push({ range: `Generated_Articles!M${rowNumber}`, values: [[scores.gptzero]] }); } if (scores.originality !== undefined) { updates.push({ range: `Generated_Articles!N${rowNumber}`, values: [[scores.originality]] }); } if (scores.copyleaks !== undefined) { updates.push({ range: `Generated_Articles!O${rowNumber}`, values: [[scores.copyleaks]] }); } if (updates.length > 0) { await sheets.spreadsheets.values.batchUpdate({ spreadsheetId: SHEET_CONFIG.sheetId, resource: { valueInputOption: 'USER_ENTERED', data: updates } }); } logSh(`✅ Scores détection mis à jour pour article ${articleId}`, 'INFO'); } catch (error) { logSh(`❌ Erreur maj scores article ${articleId}: ${error.toString()}`, 'ERROR'); throw error; } } // ============= HELPERS GOOGLE SHEETS ============= /** * Obtenir le client Google Sheets authentifié */ async function getSheetsClient() { const auth = new google.auth.GoogleAuth({ credentials: { client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL, private_key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n') }, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); const authClient = await auth.getClient(); const sheets = google.sheets({ version: 'v4', auth: authClient }); return sheets; } /** * Obtenir ou créer une sheet */ async function getOrCreateSheet(sheets, sheetName) { try { // Vérifier si la sheet existe const response = await sheets.spreadsheets.get({ spreadsheetId: SHEET_CONFIG.sheetId }); const existingSheet = response.data.sheets.find( sheet => sheet.properties.title === sheetName ); if (existingSheet) { return existingSheet; } else { // Créer la sheet si elle n'existe pas if (sheetName === 'Generated_Articles' || sheetName === 'Generated_Articles_Versioned') { await createArticlesStorageSheet(sheets, sheetName); return await getOrCreateSheet(sheets, sheetName); // Récursif pour récupérer la sheet créée } throw new Error(`Sheet ${sheetName} non supportée pour création automatique`); } } catch (error) { logSh(`❌ Erreur accès/création sheet ${sheetName}: ${error.toString()}`, 'ERROR'); throw error; } } /** * Obtenir l'ID d'une sheet par son nom */ async function getSheetIdByName(sheets, sheetName) { const response = await sheets.spreadsheets.get({ spreadsheetId: SHEET_CONFIG.sheetId }); const sheet = response.data.sheets.find( s => s.properties.title === sheetName ); return sheet ? sheet.properties.sheetId : null; } // ============= EXPORTS ============= module.exports = { compileGeneratedTextsOrganic, buildOrganicSections, findAssociatedContent, extractFAQNumber, findMatchingFAQAnswer, saveGeneratedArticleOrganic, identifyElementType, cleanIndividualContent, createArticlesStorageSheet, formatDateToFrench, countWords, getStoredArticle, getRecentArticles, updateDetectionScores, getSheetsClient, getOrCreateSheet, getSheetIdByName, generateSlugFromContent }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/Main.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // MAIN MODULAIRE - PIPELINE ARCHITECTURALE MODERNE // Responsabilité: Orchestration workflow avec architecture modulaire complète // Usage: node main_modulaire.js [rowNumber] [stackType] // ======================================== const { logSh } = require('./ErrorReporting'); const { tracer } = require('./trace'); // Import système de tendances const { TrendManager } = require('./trend-prompts/TrendManager'); // Import système de pipelines flexibles const { PipelineExecutor } = require('./pipeline/PipelineExecutor'); // Imports pipeline de base const { readInstructionsData, selectPersonalityWithAI, getPersonalities } = require('./BrainConfig'); const { extractElements, buildSmartHierarchy } = require('./ElementExtraction'); const { generateMissingKeywords } = require('./MissingKeywords'); // Migration vers StepExecutor pour garantir la cohérence avec step-by-step const { StepExecutor } = require('./StepExecutor'); const { injectGeneratedContent } = require('./ContentAssembly'); const { saveGeneratedArticleOrganic } = require('./ArticleStorage'); // Imports modules modulaires const { applySelectiveLayer } = require('./selective-enhancement/SelectiveCore'); const { applyPredefinedStack, applyAdaptiveLayers, getAvailableStacks } = require('./selective-enhancement/SelectiveLayers'); const { applyAdversarialLayer } = require('./adversarial-generation/AdversarialCore'); const { applyPredefinedStack: applyAdversarialStack } = require('./adversarial-generation/AdversarialLayers'); const { applyHumanSimulationLayer } = require('./human-simulation/HumanSimulationCore'); const { applyPredefinedSimulation, getAvailableSimulationStacks, recommendSimulationStack } = require('./human-simulation/HumanSimulationLayers'); const { applyPatternBreakingLayer } = require('./pattern-breaking/PatternBreakingCore'); const { applyPatternBreakingStack, recommendPatternBreakingStack, listAvailableStacks: listPatternBreakingStacks } = require('./pattern-breaking/PatternBreakingLayers'); /** * WORKFLOW MODULAIRE AVEC DONNÉES FOURNIES (COMPATIBILITÉ MAKE.COM/DIGITAL OCEAN) */ async function handleModularWorkflowWithData(data, config = {}) { return await tracer.run('Main.handleModularWorkflowWithData()', async () => { const { selectiveStack = 'standardEnhancement', adversarialMode = 'light', humanSimulationMode = 'none', patternBreakingMode = 'none', saveIntermediateSteps = false, source = 'compatibility_mode' } = config; await tracer.annotate({ modularWorkflow: true, compatibilityMode: true, selectiveStack, adversarialMode, humanSimulationMode, patternBreakingMode, source }); const startTime = Date.now(); logSh(`🚀 WORKFLOW MODULAIRE COMPATIBILITÉ DÉMARRÉ`, 'INFO'); logSh(` 📊 Source: ${source} | Selective: ${selectiveStack} | Adversarial: ${adversarialMode}`, 'INFO'); try { // Utiliser les données fournies directement (skippping phases 1-4) const csvData = data.csvData; const xmlTemplate = data.xmlTemplate; // Décoder XML si nécessaire let xmlString = xmlTemplate; if (xmlTemplate && !xmlTemplate.startsWith(' { const { rowNumber = 2, selectiveStack = 'standardEnhancement', // lightEnhancement, standardEnhancement, fullEnhancement, personalityFocus, fluidityFocus, adaptive adversarialMode = 'light', // none, light, standard, heavy, adaptive humanSimulationMode = 'none', // none, lightSimulation, standardSimulation, heavySimulation, adaptiveSimulation, personalityFocus, temporalFocus patternBreakingMode = 'none', // none, lightPatternBreaking, standardPatternBreaking, heavyPatternBreaking, adaptivePatternBreaking, syntaxFocus, connectorsFocus intensity = 1.0, // 0.5-1.5 intensité générale trendManager = null, // Instance TrendManager pour tendances saveIntermediateSteps = true, // 🆕 NOUVELLE OPTION: Sauvegarder chaque étape source = 'main_modulaire' } = config; await tracer.annotate({ modularWorkflow: true, rowNumber, selectiveStack, adversarialMode, humanSimulationMode, patternBreakingMode, source }); const startTime = Date.now(); logSh(`🚀 WORKFLOW MODULAIRE DÉMARRÉ`, 'INFO'); logSh(` 📊 Ligne: ${rowNumber} | Selective: ${selectiveStack} | Adversarial: ${adversarialMode} | Human: ${humanSimulationMode} | Pattern: ${patternBreakingMode}`, 'INFO'); try { // ======================================== // PHASE 1: PRÉPARATION DONNÉES // ======================================== logSh(`📋 PHASE 1: Préparation données`, 'INFO'); const csvData = await readInstructionsData(rowNumber); if (!csvData) { throw new Error(`Impossible de lire les données ligne ${rowNumber}`); } const personalities = await getPersonalities(); const selectedPersonality = await selectPersonalityWithAI( csvData.mc0, csvData.t0, personalities ); csvData.personality = selectedPersonality; logSh(` ✅ Données: ${csvData.mc0} | Personnalité: ${selectedPersonality.nom}`, 'DEBUG'); // ======================================== // PHASE 2: EXTRACTION ÉLÉMENTS // ======================================== logSh(`📝 PHASE 2: Extraction éléments XML`, 'INFO'); const elements = await extractElements(csvData.xmlTemplate, csvData); logSh(` ✅ ${elements.length} éléments extraits`, 'DEBUG'); // ======================================== // PHASE 3: GÉNÉRATION MOTS-CLÉS MANQUANTS // ======================================== logSh(`🔍 PHASE 3: Génération mots-clés manquants`, 'INFO'); const finalElements = await generateMissingKeywords(elements, csvData); logSh(` ✅ Mots-clés complétés`, 'DEBUG'); // ======================================== // PHASE 4: CONSTRUCTION HIÉRARCHIE // ======================================== logSh(`🏗️ PHASE 4: Construction hiérarchie`, 'INFO'); const hierarchy = await buildSmartHierarchy(finalElements); logSh(` ✅ ${Object.keys(hierarchy).length} sections hiérarchisées`, 'DEBUG'); // ======================================== // PHASE 5: GÉNÉRATION CONTENU DE BASE // ======================================== logSh(`💫 PHASE 5: Génération contenu de base`, 'INFO'); const executor = new StepExecutor(); const generationResult = await executor.executeInitialGeneration(csvData, { hierarchy }); const generatedContent = generationResult.content; logSh(` ✅ ${Object.keys(generatedContent).length} éléments générés`, 'DEBUG'); // 🆕 SAUVEGARDE ÉTAPE 1: Génération initiale let parentArticleId = null; let versionHistory = []; logSh(`🔍 DEBUG: saveIntermediateSteps = ${saveIntermediateSteps}`, 'INFO'); if (saveIntermediateSteps) { logSh(`💾 SAUVEGARDE v1.0: Génération initiale`, 'INFO'); const xmlString = csvData.xmlTemplate.startsWith(' r.success); if (successful.length > 0) { const avgDuration = successful.reduce((sum, r) => sum + r.duration, 0) / successful.length; const bestPerf = successful.reduce((best, r) => r.duration < best.duration ? r : best); const mostEnhancements = successful.reduce((best, r) => { const rTotal = r.selectiveEnhancements + r.adversarialModifications + (r.humanSimulationModifications || 0) + (r.patternBreakingModifications || 0); const bestTotal = best.selectiveEnhancements + best.adversarialModifications + (best.humanSimulationModifications || 0) + (best.patternBreakingModifications || 0); return rTotal > bestTotal ? r : best; }); console.log(` ⚡ Durée moyenne: ${avgDuration.toFixed(0)}ms`); console.log(` 🏆 Meilleure perf: ${bestPerf.stack} + ${bestPerf.adversarial} + ${bestPerf.humanSimulation} + ${bestPerf.patternBreaking} (${bestPerf.duration}ms)`); console.log(` 🔥 Plus d'améliorations: ${mostEnhancements.stack} + ${mostEnhancements.adversarial} + ${mostEnhancements.humanSimulation} + ${mostEnhancements.patternBreaking} (${mostEnhancements.selectiveEnhancements + mostEnhancements.adversarialModifications + (mostEnhancements.humanSimulationModifications || 0) + (mostEnhancements.patternBreakingModifications || 0)})`); } return results; } /** * INTERFACE LIGNE DE COMMANDE */ async function main() { const args = process.argv.slice(2); const command = args[0] || 'workflow'; try { switch (command) { case 'workflow': const rowNumber = parseInt(args[1]) || 2; const selectiveStack = args[2] || 'standardEnhancement'; const adversarialMode = args[3] || 'light'; const humanSimulationMode = args[4] || 'none'; const patternBreakingMode = args[5] || 'none'; console.log(`\n🚀 Exécution workflow modulaire:`); console.log(` 📊 Ligne: ${rowNumber}`); console.log(` 🔧 Stack selective: ${selectiveStack}`); console.log(` 🎯 Mode adversarial: ${adversarialMode}`); console.log(` 🧠 Mode human simulation: ${humanSimulationMode}`); console.log(` 🔧 Mode pattern breaking: ${patternBreakingMode}`); const result = await handleModularWorkflow({ rowNumber, selectiveStack, adversarialMode, humanSimulationMode, patternBreakingMode, source: 'cli' }); console.log('\n✅ WORKFLOW MODULAIRE RÉUSSI'); console.log(`📈 Stats: ${JSON.stringify(result.stats, null, 2)}`); break; case 'benchmark': const benchRowNumber = parseInt(args[1]) || 2; console.log(`\n⚡ Benchmark stacks (ligne ${benchRowNumber})`); const benchResults = await benchmarkStacks(benchRowNumber); console.log('\n📊 Résultats complets:'); console.table(benchResults); break; case 'stacks': console.log('\n📦 STACKS SELECTIVE DISPONIBLES:'); const availableStacks = getAvailableStacks(); availableStacks.forEach(stack => { console.log(`\n 🔧 ${stack.name}:`); console.log(` 📝 ${stack.description}`); console.log(` 📊 ${stack.layersCount} couches`); console.log(` 🎯 Couches: ${stack.layers ? stack.layers.map(l => `${l.type}(${l.llm})`).join(' → ') : 'N/A'}`); }); console.log('\n🎯 MODES ADVERSARIAL DISPONIBLES:'); console.log(' - none: Pas d\'adversarial'); console.log(' - light: Défense légère'); console.log(' - standard: Défense standard'); console.log(' - heavy: Défense intensive'); console.log(' - adaptive: Adaptatif intelligent'); console.log('\n🧠 MODES HUMAN SIMULATION DISPONIBLES:'); const humanStacks = getAvailableSimulationStacks(); humanStacks.forEach(stack => { console.log(`\n 🎭 ${stack.name}:`); console.log(` 📝 ${stack.description}`); console.log(` 📊 ${stack.layersCount} couches`); console.log(` ⚡ ${stack.expectedImpact.modificationsPerElement} modifs | ${stack.expectedImpact.detectionReduction} anti-détection`); }); break; case 'help': default: console.log('\n🔧 === MAIN MODULAIRE - USAGE ==='); console.log('\nCommandes disponibles:'); console.log(' workflow [ligne] [stack] [adversarial] [human] - Exécuter workflow complet'); console.log(' benchmark [ligne] - Benchmark stacks'); console.log(' stacks - Lister stacks disponibles'); console.log(' help - Afficher cette aide'); console.log('\nExemples:'); console.log(' node main_modulaire.js workflow 2 standardEnhancement light standardSimulation'); console.log(' node main_modulaire.js workflow 3 adaptive standard heavySimulation'); console.log(' node main_modulaire.js workflow 2 fullEnhancement none personalityFocus'); console.log(' node main_modulaire.js benchmark 2'); console.log(' node main_modulaire.js stacks'); break; } } catch (error) { console.error('\n❌ ERREUR MAIN MODULAIRE:', error.message); console.error(error.stack); process.exit(1); } } // Export pour usage programmatique (compatibilité avec l'ancien Main.js) module.exports = { // ✨ NOUVEAU: Interface modulaire principale handleModularWorkflow, benchmarkStacks, // 🔄 COMPATIBILITÉ: Alias pour l'ancien handleFullWorkflow handleFullWorkflow: async (data) => { // 🆕 SYSTÈME DE PIPELINE FLEXIBLE // Si pipelineConfig est fourni, utiliser PipelineExecutor au lieu du workflow modulaire classique if (data.pipelineConfig) { logSh(`🎨 Détection pipeline flexible: ${data.pipelineConfig.name}`, 'INFO'); const executor = new PipelineExecutor(); const result = await executor.execute( data.pipelineConfig, data.rowNumber || 2, { stopOnError: data.stopOnError } ); // Formater résultat pour compatibilité return { success: result.success, finalContent: result.finalContent, executionLog: result.executionLog, stats: { totalDuration: result.metadata.totalDuration, personality: result.metadata.personality, pipelineName: result.metadata.pipelineName, totalSteps: result.metadata.totalSteps, successfulSteps: result.metadata.successfulSteps } }; } // Initialiser TrendManager si tendance spécifiée let trendManager = null; if (data.trendId) { trendManager = new TrendManager(); await trendManager.setTrend(data.trendId); logSh(`🎯 Tendance appliquée: ${data.trendId}`, 'INFO'); } // Mapper l'ancien format vers le nouveau format modulaire const config = { rowNumber: data.rowNumber, source: data.source || 'compatibility_mode', selectiveStack: data.selectiveStack || 'standardEnhancement', adversarialMode: data.adversarialMode || 'light', humanSimulationMode: data.humanSimulationMode || 'none', patternBreakingMode: data.patternBreakingMode || 'none', intensity: data.intensity || 1.0, trendManager: trendManager, saveIntermediateSteps: data.saveIntermediateSteps || false }; // Si des données CSV sont fournies directement (Make.com style) if (data.csvData && data.xmlTemplate) { return handleModularWorkflowWithData(data, config); } // Sinon utiliser le workflow normal return handleModularWorkflow(config); }, // 🔄 COMPATIBILITÉ: Autres exports utilisés par l'ancien système testMainWorkflow: () => { return handleModularWorkflow({ rowNumber: 2, selectiveStack: 'standardEnhancement', source: 'test_main_nodejs' }); }, launchLogViewer: () => { // La fonction launchLogViewer est maintenant intégrée dans handleModularWorkflow console.log('✅ Log viewer sera lancé automatiquement avec le workflow'); } }; // Exécution CLI si appelé directement if (require.main === module) { main().catch(error => { console.error('❌ ERREUR FATALE:', error.message); process.exit(1); }); } /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/test-manual.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: test-manual.js - ENTRY POINT MANUEL // Description: Test workflow ligne 2 Google Sheets // Usage: node test-manual.js // ======================================== require('./polyfills/fetch.cjs'); require('dotenv').config(); const { handleFullWorkflow } = require('./Main'); const { logSh } = require('./ErrorReporting'); /** * TEST MANUEL LIGNE 2 */ async function testWorkflowLigne2() { logSh('🚀 === DÉMARRAGE TEST MANUEL LIGNE 2 ===', 'INFO'); // Using logSh instead of console.log const startTime = Date.now(); try { // DONNÉES DE TEST POUR LIGNE 2 const testData = { rowNumber: 2, // Ligne 2 Google Sheets source: 'test_manual_nodejs' }; logSh('📊 Configuration test:', 'INFO'); // Using logSh instead of console.log logSh(` • Ligne: ${testData.rowNumber}`, 'INFO'); // Using logSh instead of console.log logSh(` • Source: ${testData.source}`, 'INFO'); // Using logSh instead of console.log logSh(` • Timestamp: ${new Date().toISOString()}`, 'INFO'); // Using logSh instead of console.log // LANCER LE WORKFLOW logSh('\n🎯 Lancement workflow principal...', 'INFO'); // Using logSh instead of console.log const result = await handleFullWorkflow(testData); // AFFICHER RÉSULTATS const duration = Date.now() - startTime; logSh('\n🏆 === WORKFLOW TERMINÉ AVEC SUCCÈS ===', 'INFO'); // Using logSh instead of console.log logSh(`⏱️ Durée: ${Math.round(duration/1000)}s`, 'INFO'); // Using logSh instead of console.log logSh(`📊 Status: ${result.success ? '✅ SUCCESS' : '❌ ERROR'}`, 'INFO'); // Using logSh instead of console.log if (result.success) { logSh(`📝 Éléments générés: ${result.elementsGenerated}`, 'INFO'); // Using logSh instead of console.log logSh(`👤 Personnalité: ${result.personality}`, 'INFO'); // Using logSh instead of console.log logSh(`🎯 MC0: ${result.csvData?.mc0 || 'N/A'}`, 'INFO'); // Using logSh instead of console.log logSh(`📄 XML length: ${result.stats?.xmlLength || 'N/A'} chars`, 'INFO'); // Using logSh instead of console.log logSh(`🔤 Mots total: ${result.stats?.wordCount || 'N/A'}`, 'INFO'); // Using logSh instead of console.log logSh(`🧠 LLMs utilisés: ${result.llmsUsed?.join(', ') || 'N/A'}`, 'INFO'); // Using logSh instead of console.log if (result.articleStorage) { logSh(`💾 Article sauvé: ID ${result.articleStorage.articleId}`, 'INFO'); // Using logSh instead of console.log } } logSh('\n📋 Résultat complet:', 'DEBUG'); // Using logSh instead of console.log logSh(JSON.stringify(result, null, 2), 'DEBUG'); // Using logSh instead of console.log return result; } catch (error) { const duration = Date.now() - startTime; logSh('\n❌ === ERREUR WORKFLOW ===', 'ERROR'); // Using logSh instead of console.error logSh(`❌ Message: ${error.message}`, 'ERROR'); // Using logSh instead of console.error logSh(`❌ Durée avant échec: ${Math.round(duration/1000)}s`, 'ERROR'); // Using logSh instead of console.error if (process.env.NODE_ENV === 'development') { logSh(`❌ Stack: ${error.stack}`, 'ERROR'); // Using logSh instead of console.error } // Afficher conseils de debug logSh('\n🔧 CONSEILS DE DEBUG:', 'INFO'); // Using logSh instead of console.log logSh('1. Vérifiez vos variables d\'environnement (.env)', 'INFO'); // Using logSh instead of console.log logSh('2. Vérifiez la connexion Google Sheets', 'INFO'); // Using logSh instead of console.log logSh('3. Vérifiez les API keys LLM', 'INFO'); // Using logSh instead of console.log logSh('4. Regardez les logs détaillés dans ./logs/', 'INFO'); // Using logSh instead of console.log process.exit(1); } } /** * VÉRIFICATIONS PRÉALABLES */ function checkEnvironment() { logSh('🔍 Vérification environnement...', 'INFO'); // Using logSh instead of console.log const required = [ 'GOOGLE_SHEETS_ID', 'OPENAI_API_KEY' ]; const missing = required.filter(key => !process.env[key]); if (missing.length > 0) { logSh('❌ Variables d\'environnement manquantes:', 'ERROR'); // Using logSh instead of console.error missing.forEach(key => logSh(` • ${key}`, 'ERROR')); // Using logSh instead of console.error logSh('\n💡 Créez un fichier .env avec ces variables', 'ERROR'); // Using logSh instead of console.error process.exit(1); } logSh('✅ Variables d\'environnement OK', 'INFO'); // Using logSh instead of console.log // Info sur les variables configurées logSh('📋 Configuration détectée:', 'INFO'); // Using logSh instead of console.log logSh(` • Google Sheets ID: ${process.env.GOOGLE_SHEETS_ID}`, 'INFO'); // Using logSh instead of console.log logSh(` • OpenAI: ${process.env.OPENAI_API_KEY ? '✅ Configuré' : '❌ Manquant'}`, 'INFO'); // Using logSh instead of console.log logSh(` • Claude: ${process.env.CLAUDE_API_KEY ? '✅ Configuré' : '⚠️ Optionnel'}`, 'INFO'); // Using logSh instead of console.log logSh(` • Gemini: ${process.env.GEMINI_API_KEY ? '✅ Configuré' : '⚠️ Optionnel'}`, 'INFO'); // Using logSh instead of console.log } /** * POINT D'ENTRÉE PRINCIPAL */ async function main() { try { // Vérifications préalables checkEnvironment(); // Test workflow await testWorkflowLigne2(); logSh('\n🎉 Test manuel terminé avec succès !', 'INFO'); // Using logSh instead of console.log process.exit(0); } catch (error) { logSh('\n💥 Erreur fatale: ' + error.message, 'ERROR'); // Using logSh instead of console.error process.exit(1); } } // Lancer si exécuté directement if (require.main === module) { main(); } module.exports = { testWorkflowLigne2 }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/prompt-engine/DynamicPromptEngine.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // DYNAMIC PROMPT ENGINE - SYSTÈME AVANCÉ // Responsabilité: Génération dynamique de prompts adaptatifs ultra-modulaires // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); /** * DYNAMIC PROMPT ENGINE * Système avancé de génération de prompts avec composition multi-niveaux */ class DynamicPromptEngine { constructor() { this.name = 'DynamicPromptEngine'; this.templates = new Map(); this.contextAnalyzers = new Map(); this.adaptiveRules = new Map(); // Initialiser templates par défaut this.initializeDefaultTemplates(); this.initializeContextAnalyzers(); this.initializeAdaptiveRules(); } // ======================================== // INITIALISATION TEMPLATES // ======================================== initializeDefaultTemplates() { // Templates de base modulaires this.templates.set('technical', { meta: { role: "Tu es un expert {domain} avec {experience} d'expérience", expertise: "Spécialisé en {specialization} et {methods}", approach: "Adopte une approche {style} et {precision}" }, context: { mission: "MISSION: {task_description}", domain_context: "CONTEXTE: {sector} - {activity_type}", target_audience: "PUBLIC: {audience_level} ({audience_characteristics})", constraints: "CONTRAINTES: {content_constraints}" }, task: { primary_objective: "OBJECTIF PRINCIPAL: {main_goal}", specific_actions: "ACTIONS SPÉCIFIQUES:\n{action_list}", quality_criteria: "CRITÈRES DE QUALITÉ: {quality_requirements}", success_metrics: "MÉTRIQUES DE SUCCÈS: {success_indicators}" }, instructions: { guidelines: "CONSIGNES {instruction_type}:\n{instruction_list}", restrictions: "INTERDICTIONS: {avoid_list}", emphasis: "PRIORITÉS: {emphasis_list}", style_guide: "STYLE: {style_requirements}" }, examples: { format: "FORMAT ATTENDU:\n{format_example}", sample_input: "EXEMPLE D'ENTRÉE: {input_example}", sample_output: "EXEMPLE DE SORTIE: {output_example}" } }); this.templates.set('style', { meta: { role: "Tu es un rédacteur expert en communication {communication_type}", personality: "Adopte la personnalité de {personality_profile}", tone_master: "Maîtrise parfaitement le ton {target_tone}" }, context: { brand_voice: "IDENTITÉ: {brand_characteristics}", target_emotion: "ÉMOTION CIBLE: {desired_emotion}", communication_goal: "BUT COMMUNICATIONNEL: {communication_objective}", audience_psychology: "PSYCHOLOGIE AUDIENCE: {audience_mindset}" }, task: { style_transformation: "TRANSFORMATION STYLISTIQUE: {transformation_type}", tone_adaptation: "ADAPTATION TONALE: {tone_specifications}", personality_injection: "INJECTION PERSONNALITÉ: {personality_traits}", emotional_calibration: "CALIBRAGE ÉMOTIONNEL: {emotional_targets}" } }); this.templates.set('adversarial', { meta: { role: "Tu es un expert en rédaction naturelle anti-détection", stealth_mode: "Mode furtif niveau {stealth_level}", human_mimicry: "Imite parfaitement l'écriture humaine {human_characteristics}" }, context: { detection_threats: "MENACES DÉTECTION: {detector_types}", target_metrics: "MÉTRIQUES CIBLES: {target_scores}", natural_patterns: "PATTERNS NATURELS: {natural_characteristics}", human_variance: "VARIANCE HUMAINE: {variance_patterns}" }, task: { stealth_rewrite: "RÉÉCRITURE FURTIVE: {stealth_techniques}", pattern_breaking: "CASSAGE PATTERNS: {pattern_break_methods}", human_errors: "ERREURS HUMAINES: {human_error_types}", style_diversification: "DIVERSIFICATION: {diversification_methods}" } }); logSh(`✅ ${this.templates.size} templates modulaires initialisés`, 'DEBUG'); } initializeContextAnalyzers() { // Analyseurs de contexte automatiques this.contextAnalyzers.set('domain_inference', (content, csvData) => { const mc0 = csvData?.mc0?.toLowerCase() || ''; if (mc0.includes('signalétique') || mc0.includes('plaque')) { return { domain: 'signalétique industrielle', specialization: 'communication visuelle B2B', sector: 'industrie/signalétique', activity_type: 'fabrication sur mesure' }; } if (mc0.includes('bijou') || mc0.includes('gravure')) { return { domain: 'artisanat créatif', specialization: 'joaillerie personnalisée', sector: 'artisanat/luxe', activity_type: 'création artisanale' }; } return { domain: 'communication visuelle', specialization: 'impression numérique', sector: 'services/impression', activity_type: 'prestation de services' }; }); this.contextAnalyzers.set('complexity_assessment', (content) => { const totalText = Object.values(content).join(' '); const technicalTerms = (totalText.match(/\b(technique|procédé|norme|ISO|DIN|matériau|aluminum|PMMA)\b/gi) || []).length; const complexity = technicalTerms / totalText.split(' ').length; return { complexity_level: complexity > 0.05 ? 'élevée' : complexity > 0.02 ? 'moyenne' : 'standard', technical_density: complexity, recommended_approach: complexity > 0.05 ? 'expert' : 'accessible' }; }); this.contextAnalyzers.set('audience_inference', (content, csvData, trend) => { const personality = csvData?.personality; if (trend?.id === 'generation-z') { return { audience_level: 'digital natives', audience_characteristics: 'connectés, inclusifs, authentiques', audience_mindset: 'recherche authenticité et transparence' }; } if (personality?.style === 'technique') { return { audience_level: 'professionnels techniques', audience_characteristics: 'expérimentés, précis, orientés solutions', audience_mindset: 'recherche expertise et fiabilité' }; } return { audience_level: 'grand public', audience_characteristics: 'curieux, pragmatiques, sensibles qualité', audience_mindset: 'recherche clarté et valeur ajoutée' }; }); logSh(`✅ ${this.contextAnalyzers.size} analyseurs de contexte initialisés`, 'DEBUG'); } initializeAdaptiveRules() { // Règles d'adaptation conditionnelles this.adaptiveRules.set('intensity_scaling', { condition: (config) => config.intensity, adaptations: { low: (config) => ({ precision: 'accessible', style: 'naturel et fluide', instruction_type: 'DOUCES', stealth_level: 'discret' }), medium: (config) => ({ precision: 'équilibrée', style: 'professionnel et engageant', instruction_type: 'STANDARD', stealth_level: 'modéré' }), high: (config) => ({ precision: 'maximale', style: 'expert et percutant', instruction_type: 'STRICTES', stealth_level: 'avancé' }) }, getLevel: (intensity) => { if (intensity < 0.7) return 'low'; if (intensity < 1.2) return 'medium'; return 'high'; } }); this.adaptiveRules.set('trend_adaptation', { condition: (config) => config.trend, adaptations: { 'eco-responsable': { communication_type: 'responsable et engagée', desired_emotion: 'confiance et respect', brand_characteristics: 'éthique, durable, transparente', communication_objective: 'sensibiliser et rassurer' }, 'tech-innovation': { communication_type: 'moderne et dynamique', desired_emotion: 'excitation et confiance', brand_characteristics: 'innovante, performante, avant-gardiste', communication_objective: 'impressionner et convaincre' }, 'artisanal-premium': { communication_type: 'authentique et raffinée', desired_emotion: 'admiration et désir', brand_characteristics: 'traditionnelle, qualitative, exclusive', communication_objective: 'valoriser et différencier' } } }); logSh(`✅ ${this.adaptiveRules.size} règles adaptatives initialisées`, 'DEBUG'); } // ======================================== // GÉNÉRATION DYNAMIQUE DE PROMPTS // ======================================== /** * MAIN METHOD - Génère un prompt adaptatif complet */ async generateAdaptivePrompt(config) { return await tracer.run('DynamicPromptEngine.generateAdaptivePrompt()', async () => { const { templateType = 'technical', content = {}, csvData = null, trend = null, layerConfig = {}, customVariables = {} } = config; await tracer.annotate({ templateType, hasTrend: !!trend, contentSize: Object.keys(content).length, hasCustomVars: Object.keys(customVariables).length > 0 }); logSh(`🧠 Génération prompt adaptatif: ${templateType}`, 'INFO'); try { // 1. ANALYSE CONTEXTUELLE AUTOMATIQUE const contextAnalysis = await this.analyzeContext(content, csvData, trend); // 2. APPLICATION RÈGLES ADAPTATIVES const adaptiveConfig = this.applyAdaptiveRules(layerConfig, trend, contextAnalysis); // 3. GÉNÉRATION VARIABLES DYNAMIQUES const dynamicVariables = this.generateDynamicVariables( contextAnalysis, adaptiveConfig, customVariables, layerConfig ); // 4. COMPOSITION TEMPLATE MULTI-NIVEAUX const composedPrompt = this.composeMultiLevelPrompt( templateType, dynamicVariables, layerConfig ); // 5. POST-PROCESSING ADAPTATIF const finalPrompt = this.postProcessPrompt(composedPrompt, adaptiveConfig); const stats = { templateType, variablesCount: Object.keys(dynamicVariables).length, adaptationRules: Object.keys(adaptiveConfig).length, promptLength: finalPrompt.length, contextComplexity: contextAnalysis.complexity_level }; logSh(`✅ Prompt adaptatif généré: ${stats.promptLength} chars, ${stats.variablesCount} variables`, 'DEBUG'); return { prompt: finalPrompt, metadata: { stats, contextAnalysis, adaptiveConfig, dynamicVariables: Object.keys(dynamicVariables) } }; } catch (error) { logSh(`❌ Erreur génération prompt adaptatif: ${error.message}`, 'ERROR'); throw error; } }); } /** * ANALYSE CONTEXTUELLE AUTOMATIQUE */ async analyzeContext(content, csvData, trend) { const context = {}; // Exécuter tous les analyseurs for (const [analyzerName, analyzer] of this.contextAnalyzers) { try { const analysis = analyzer(content, csvData, trend); Object.assign(context, analysis); logSh(` 🔍 ${analyzerName}: ${JSON.stringify(analysis)}`, 'DEBUG'); } catch (error) { logSh(` ⚠️ Analyseur ${analyzerName} échoué: ${error.message}`, 'WARNING'); } } return context; } /** * APPLICATION DES RÈGLES ADAPTATIVES */ applyAdaptiveRules(layerConfig, trend, contextAnalysis) { const adaptiveConfig = {}; for (const [ruleName, rule] of this.adaptiveRules) { try { if (rule.condition(layerConfig)) { let adaptation = {}; if (ruleName === 'intensity_scaling') { const level = rule.getLevel(layerConfig.intensity || 1.0); adaptation = rule.adaptations[level](layerConfig); } else if (ruleName === 'trend_adaptation' && trend) { adaptation = rule.adaptations[trend.id] || {}; } Object.assign(adaptiveConfig, adaptation); logSh(` 🎛️ Règle ${ruleName} appliquée`, 'DEBUG'); } } catch (error) { logSh(` ⚠️ Règle ${ruleName} échouée: ${error.message}`, 'WARNING'); } } return adaptiveConfig; } /** * GÉNÉRATION VARIABLES DYNAMIQUES */ generateDynamicVariables(contextAnalysis, adaptiveConfig, customVariables, layerConfig) { const variables = { // Variables contextuelles ...contextAnalysis, // Variables adaptatives ...adaptiveConfig, // Variables personnalisées ...customVariables, // Variables de configuration experience: this.generateExperienceLevel(contextAnalysis.complexity_level), methods: this.generateMethods(layerConfig), task_description: this.generateTaskDescription(layerConfig), action_list: this.generateActionList(layerConfig), instruction_list: this.generateInstructionList(layerConfig), // Variables dynamiques calculées timestamp: new Date().toISOString(), session_id: this.generateSessionId() }; return variables; } /** * COMPOSITION TEMPLATE MULTI-NIVEAUX */ composeMultiLevelPrompt(templateType, variables, layerConfig) { const template = this.templates.get(templateType); if (!template) { throw new Error(`Template ${templateType} introuvable`); } const sections = []; // Composer chaque niveau du template for (const [sectionName, sectionTemplate] of Object.entries(template)) { const composedSection = this.composeSection(sectionTemplate, variables); if (composedSection.trim()) { sections.push(composedSection); } } return sections.join('\n\n'); } /** * COMPOSITION SECTION INDIVIDUELLE */ composeSection(sectionTemplate, variables) { const lines = []; for (const [key, template] of Object.entries(sectionTemplate)) { const interpolated = this.interpolateTemplate(template, variables); if (interpolated && interpolated.trim() !== template) { lines.push(interpolated); } } return lines.join('\n'); } /** * INTERPOLATION TEMPLATE AVEC VARIABLES */ interpolateTemplate(template, variables) { return template.replace(/\{([^}]+)\}/g, (match, varName) => { return variables[varName] || match; }); } /** * POST-PROCESSING ADAPTATIF */ postProcessPrompt(prompt, adaptiveConfig) { let processed = prompt; // Suppression des lignes vides multiples processed = processed.replace(/\n\n\n+/g, '\n\n'); // Suppression des variables non résolues processed = processed.replace(/\{[^}]+\}/g, ''); // Suppression des lignes vides après suppression variables processed = processed.replace(/\n\s*\n/g, '\n\n'); return processed.trim(); } // ======================================== // GÉNÉRATEURS HELPER // ======================================== generateExperienceLevel(complexity) { switch (complexity) { case 'élevée': return '10+ années'; case 'moyenne': return '5+ années'; default: return '3+ années'; } } generateMethods(layerConfig) { const methods = []; if (layerConfig.targetTerms?.length > 0) { methods.push('terminologie spécialisée'); } if (layerConfig.focusAreas?.length > 0) { methods.push('approche métier'); } return methods.length > 0 ? methods.join(', ') : 'méthodes éprouvées'; } generateTaskDescription(layerConfig) { const type = layerConfig.layerType || 'enhancement'; const descriptions = { technical: 'Améliore la précision technique et le vocabulaire spécialisé', style: 'Adapte le style et la personnalité du contenu', adversarial: 'Rend le contenu plus naturel et humain' }; return descriptions[type] || 'Améliore le contenu selon les spécifications'; } generateActionList(layerConfig) { const actions = []; if (layerConfig.targetTerms) { actions.push(`- Intégrer naturellement: ${layerConfig.targetTerms.slice(0, 5).join(', ')}`); } if (layerConfig.avoidTerms) { actions.push(`- Éviter absolument: ${layerConfig.avoidTerms.slice(0, 3).join(', ')}`); } actions.push('- Conserver le message original et la structure'); actions.push('- Maintenir la cohérence stylistique'); return actions.join('\n'); } generateInstructionList(layerConfig) { const instructions = [ 'GARDE exactement le même sens et message', 'PRÉSERVE la structure et la longueur approximative', 'ASSURE-TOI que le résultat reste naturel et fluide' ]; if (layerConfig.preservePersonality) { instructions.push('MAINTIENS la personnalité et le ton existants'); } return instructions.map(i => `- ${i}`).join('\n'); } generateSessionId() { return Math.random().toString(36).substring(2, 15); } // ======================================== // API PUBLIQUE ÉTENDUE // ======================================== /** * Ajouter template personnalisé */ addCustomTemplate(name, template) { this.templates.set(name, template); logSh(`✨ Template personnalisé ajouté: ${name}`, 'INFO'); } /** * Ajouter analyseur de contexte */ addContextAnalyzer(name, analyzer) { this.contextAnalyzers.set(name, analyzer); logSh(`🔍 Analyseur personnalisé ajouté: ${name}`, 'INFO'); } /** * Ajouter règle adaptative */ addAdaptiveRule(name, rule) { this.adaptiveRules.set(name, rule); logSh(`🎛️ Règle adaptative ajoutée: ${name}`, 'INFO'); } /** * Status du moteur */ getEngineStatus() { return { templates: Array.from(this.templates.keys()), contextAnalyzers: Array.from(this.contextAnalyzers.keys()), adaptiveRules: Array.from(this.adaptiveRules.keys()), totalComponents: this.templates.size + this.contextAnalyzers.size + this.adaptiveRules.size }; } } // ============= EXPORTS ============= module.exports = { DynamicPromptEngine }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/workflow-configuration/WorkflowEngine.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // WORKFLOW ENGINE - SÉQUENCES MODULAIRES CONFIGURABLES // Responsabilité: Gestion flexible de l'ordre d'exécution des phases modulaires // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); // Import des modules disponibles const { applySelectiveEnhancement } = require('../selective-enhancement/SelectiveCore'); const { applyAdversarialEnhancement } = require('../adversarial-generation/AdversarialCore'); const { applyHumanSimulation } = require('../human-simulation/HumanSimulationCore'); const { applyPatternBreaking } = require('../pattern-breaking/PatternBreakingCore'); /** * WORKFLOW ENGINE * Permet de configurer des séquences personnalisées de traitement modulaire */ class WorkflowEngine { constructor() { this.name = 'WorkflowEngine'; this.predefinedSequences = new Map(); this.customSequences = new Map(); // Initialiser les séquences prédéfinies this.initializePredefinedSequences(); } // ======================================== // SÉQUENCES PRÉDÉFINIES // ======================================== initializePredefinedSequences() { // Séquence par défaut (workflow actuel) this.predefinedSequences.set('default', { name: 'Default Workflow', description: 'Séquence standard: Selective → Adversarial → Human → Pattern', phases: [ { type: 'selective', config: { enabled: true } }, { type: 'adversarial', config: { enabled: true } }, { type: 'human', config: { enabled: true } }, { type: 'pattern', config: { enabled: true } } ] }); // Séquence humanisée d'abord this.predefinedSequences.set('human-first', { name: 'Human-First Workflow', description: 'Humanisation d\'abord: Human → Pattern → Selective → Pattern', phases: [ { type: 'human', config: { enabled: true } }, { type: 'pattern', config: { enabled: true, iteration: 1 } }, { type: 'selective', config: { enabled: true } }, { type: 'pattern', config: { enabled: true, iteration: 2 } } ] }); // Séquence anti-détection intensive this.predefinedSequences.set('stealth-intensive', { name: 'Stealth Intensive', description: 'Anti-détection max: Pattern → Adversarial → Human → Pattern → Adversarial', phases: [ { type: 'pattern', config: { enabled: true, iteration: 1 } }, { type: 'adversarial', config: { enabled: true, iteration: 1 } }, { type: 'human', config: { enabled: true } }, { type: 'pattern', config: { enabled: true, iteration: 2 } }, { type: 'adversarial', config: { enabled: true, iteration: 2 } } ] }); // Séquence qualité d'abord this.predefinedSequences.set('quality-first', { name: 'Quality-First Workflow', description: 'Qualité prioritaire: Selective → Human → Selective → Pattern', phases: [ { type: 'selective', config: { enabled: true, iteration: 1 } }, { type: 'human', config: { enabled: true } }, { type: 'selective', config: { enabled: true, iteration: 2 } }, { type: 'pattern', config: { enabled: true } } ] }); // Séquence équilibrée this.predefinedSequences.set('balanced', { name: 'Balanced Workflow', description: 'Équilibré: Selective → Human → Adversarial → Pattern → Selective', phases: [ { type: 'selective', config: { enabled: true, iteration: 1 } }, { type: 'human', config: { enabled: true } }, { type: 'adversarial', config: { enabled: true } }, { type: 'pattern', config: { enabled: true } }, { type: 'selective', config: { enabled: true, iteration: 2, intensity: 0.7 } } ] }); logSh(`✅ WorkflowEngine: ${this.predefinedSequences.size} séquences prédéfinies chargées`, 'DEBUG'); } // ======================================== // EXÉCUTION WORKFLOW CONFIGURABLE // ======================================== /** * Exécute un workflow selon une séquence configurée */ async executeConfigurableWorkflow(content, config = {}) { return await tracer.run('WorkflowEngine.executeConfigurableWorkflow()', async () => { const { sequenceName = 'default', customSequence = null, selectiveConfig = {}, adversarialConfig = {}, humanConfig = {}, patternConfig = {}, csvData = {}, personalities = {} } = config; await tracer.annotate({ sequenceName: customSequence ? 'custom' : sequenceName, isCustomSequence: !!customSequence, elementsCount: Object.keys(content).length }); logSh(`🔄 WORKFLOW CONFIGURABLE: ${customSequence ? 'custom' : sequenceName}`, 'INFO'); let currentContent = { ...content }; const workflowStats = { sequenceName: customSequence ? 'custom' : sequenceName, phases: [], totalDuration: 0, totalModifications: 0, versioning: new Map() }; try { // Obtenir la séquence à exécuter const sequence = customSequence || this.getSequence(sequenceName); if (!sequence) { throw new Error(`Séquence workflow inconnue: ${sequenceName}`); } logSh(` 📋 Séquence: ${sequence.name} (${sequence.phases.length} phases)`, 'INFO'); logSh(` 📝 Description: ${sequence.description}`, 'INFO'); const startTime = Date.now(); // Exécuter chaque phase de la séquence for (let i = 0; i < sequence.phases.length; i++) { const phase = sequence.phases[i]; const phaseNumber = i + 1; logSh(`📊 PHASE ${phaseNumber}/${sequence.phases.length}: ${phase.type.toUpperCase()}${phase.config.iteration ? ` (${phase.config.iteration})` : ''}`, 'INFO'); const phaseStartTime = Date.now(); let phaseResult = null; try { switch (phase.type) { case 'selective': if (phase.config.enabled) { phaseResult = await this.executeSelectivePhase(currentContent, { ...selectiveConfig, ...phase.config, csvData, personalities }); } break; case 'adversarial': if (phase.config.enabled) { phaseResult = await this.executeAdversarialPhase(currentContent, { ...adversarialConfig, ...phase.config, csvData, personalities }); } break; case 'human': if (phase.config.enabled) { phaseResult = await this.executeHumanPhase(currentContent, { ...humanConfig, ...phase.config, csvData, personalities }); } break; case 'pattern': if (phase.config.enabled) { phaseResult = await this.executePatternPhase(currentContent, { ...patternConfig, ...phase.config, csvData, personalities }); } break; default: logSh(`⚠️ Type de phase inconnue: ${phase.type}`, 'WARNING'); } // Mettre à jour le contenu et les stats if (phaseResult) { currentContent = phaseResult.content; const phaseDuration = Date.now() - phaseStartTime; const phaseStats = { type: phase.type, iteration: phase.config.iteration || 1, duration: phaseDuration, modifications: phaseResult.stats?.modifications || 0, success: true }; workflowStats.phases.push(phaseStats); workflowStats.totalModifications += phaseStats.modifications; // Versioning const versionKey = `v1.${phaseNumber}`; workflowStats.versioning.set(versionKey, { phase: `${phase.type}${phase.config.iteration ? `-${phase.config.iteration}` : ''}`, content: { ...currentContent }, timestamp: new Date().toISOString() }); logSh(` ✅ Phase ${phaseNumber} terminée: ${phaseStats.modifications} modifications en ${phaseDuration}ms`, 'DEBUG'); } else { logSh(` ⏭️ Phase ${phaseNumber} ignorée (désactivée)`, 'DEBUG'); } } catch (error) { logSh(` ❌ Erreur phase ${phaseNumber} (${phase.type}): ${error.message}`, 'ERROR'); workflowStats.phases.push({ type: phase.type, iteration: phase.config.iteration || 1, duration: Date.now() - phaseStartTime, modifications: 0, success: false, error: error.message }); } } workflowStats.totalDuration = Date.now() - startTime; // Version finale workflowStats.versioning.set('v2.0', { phase: 'final', content: { ...currentContent }, timestamp: new Date().toISOString() }); logSh(`✅ WORKFLOW TERMINÉ: ${workflowStats.totalModifications} modifications en ${workflowStats.totalDuration}ms`, 'INFO'); return { content: currentContent, stats: workflowStats, success: true }; } catch (error) { logSh(`❌ Erreur workflow configurable: ${error.message}`, 'ERROR'); workflowStats.totalDuration = Date.now() - startTime; workflowStats.error = error.message; return { content: currentContent, stats: workflowStats, success: false, error: error.message }; } }); } // ======================================== // EXÉCUTION DES PHASES INDIVIDUELLES // ======================================== async executeSelectivePhase(content, config) { const result = await applySelectiveEnhancement(content, config); return { content: result.content || content, stats: { modifications: result.stats?.selectiveEnhancements || 0 } }; } async executeAdversarialPhase(content, config) { const result = await applyAdversarialEnhancement(content, config); return { content: result.content || content, stats: { modifications: result.stats?.adversarialModifications || 0 } }; } async executeHumanPhase(content, config) { const result = await applyHumanSimulation(content, config); return { content: result.content || content, stats: { modifications: result.stats?.humanSimulationModifications || 0 } }; } async executePatternPhase(content, config) { const result = await applyPatternBreaking(content, config); return { content: result.content || content, stats: { modifications: result.stats?.patternBreakingModifications || 0 } }; } // ======================================== // GESTION DES SÉQUENCES // ======================================== /** * Obtenir une séquence (prédéfinie ou personnalisée) */ getSequence(sequenceName) { return this.predefinedSequences.get(sequenceName) || this.customSequences.get(sequenceName); } /** * Créer une séquence personnalisée */ createCustomSequence(name, sequence) { this.customSequences.set(name, sequence); logSh(`✨ Séquence personnalisée créée: ${name}`, 'INFO'); return sequence; } /** * Lister toutes les séquences disponibles */ getAvailableSequences() { const sequences = []; // Séquences prédéfinies for (const [name, sequence] of this.predefinedSequences) { sequences.push({ name, ...sequence, isCustom: false }); } // Séquences personnalisées for (const [name, sequence] of this.customSequences) { sequences.push({ name, ...sequence, isCustom: true }); } return sequences; } /** * Valider une séquence */ validateSequence(sequence) { if (!sequence.name || !sequence.phases || !Array.isArray(sequence.phases)) { return false; } const validTypes = ['selective', 'adversarial', 'human', 'pattern']; for (const phase of sequence.phases) { if (!phase.type || !validTypes.includes(phase.type)) { return false; } if (!phase.config || typeof phase.config !== 'object') { return false; } } return true; } /** * Obtenir le statut du moteur */ getEngineStatus() { return { predefinedSequences: Array.from(this.predefinedSequences.keys()), customSequences: Array.from(this.customSequences.keys()), totalSequences: this.predefinedSequences.size + this.customSequences.size, availablePhaseTypes: ['selective', 'adversarial', 'human', 'pattern'] }; } } // ============= EXPORTS ============= module.exports = { WorkflowEngine }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/APIController.js │ └────────────────────────────────────────────────────────────────────┘ */ /** * Contrôleur API RESTful pour SEO Generator * Centralise toute la logique API métier */ const { logSh } = require('./ErrorReporting'); const { handleFullWorkflow } = require('./Main'); const { getPersonalities, readInstructionsData } = require('./BrainConfig'); const { getStoredArticle, getRecentArticles } = require('./ArticleStorage'); const { DynamicPromptEngine } = require('./prompt-engine/DynamicPromptEngine'); const { TrendManager } = require('./trend-prompts/TrendManager'); const { WorkflowEngine } = require('./workflow-configuration/WorkflowEngine'); class APIController { constructor() { this.articles = new Map(); // Cache articles en mémoire this.projects = new Map(); // Cache projets this.templates = new Map(); // Cache templates // Initialize prompt engine components this.promptEngine = new DynamicPromptEngine(); this.trendManager = new TrendManager(); this.workflowEngine = new WorkflowEngine(); } // ======================================== // GESTION ARTICLES // ======================================== /** * GET /api/articles - Liste tous les articles */ async getArticles(req, res) { try { const { limit = 50, offset = 0, project, status } = req.query; logSh(`📋 Récupération articles: limit=${limit}, offset=${offset}`, 'DEBUG'); // Récupération depuis Google Sheets const articles = await getRecentArticles(parseInt(limit)); // Filtrage optionnel let filteredArticles = articles; if (project) { filteredArticles = articles.filter(a => a.project === project); } if (status) { filteredArticles = filteredArticles.filter(a => a.status === status); } res.json({ success: true, data: { articles: filteredArticles.slice(offset, offset + limit), total: filteredArticles.length, limit: parseInt(limit), offset: parseInt(offset) }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération articles: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des articles', message: error.message }); } } /** * GET /api/articles/:id - Récupère un article spécifique */ async getArticle(req, res) { try { const { id } = req.params; const { format = 'json' } = req.query || {}; logSh(`📄 Récupération article ID: ${id}`, 'DEBUG'); const article = await getStoredArticle(id); if (!article) { return res.status(404).json({ success: false, error: 'Article non trouvé', id }); } // Format de réponse if (format === 'html') { res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(article.htmlContent || article.content); } else if (format === 'text') { res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.send(article.textContent || article.content); } else { res.json({ success: true, data: article, timestamp: new Date().toISOString() }); } } catch (error) { logSh(`❌ Erreur récupération article ${req.params.id}: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération de l\'article', message: error.message }); } } /** * POST /api/articles - Créer un nouvel article */ async createArticle(req, res) { try { const { keyword, rowNumber, project = 'api', config = {}, template, personalityPreference } = req.body; // Validation if (!keyword && !rowNumber) { return res.status(400).json({ success: false, error: 'Mot-clé ou numéro de ligne requis' }); } logSh(`✨ Création article: ${keyword || `ligne ${rowNumber}`}`, 'INFO'); // Configuration par défaut const workflowConfig = { rowNumber: rowNumber || 2, source: 'api', project, selectiveStack: config.selectiveStack || 'standardEnhancement', adversarialMode: config.adversarialMode || 'light', humanSimulationMode: config.humanSimulationMode || 'none', patternBreakingMode: config.patternBreakingMode || 'none', personalityPreference, template, ...config }; // Si mot-clé fourni, créer données temporaires if (keyword && !rowNumber) { workflowConfig.csvData = { mc0: keyword, t0: `Guide complet ${keyword}`, personality: personalityPreference || { nom: 'Marc', style: 'professionnel' } }; } // Exécution du workflow const result = await handleFullWorkflow(workflowConfig); res.status(201).json({ success: true, data: { id: result.id || result.slug, article: result, config: workflowConfig }, message: 'Article créé avec succès', timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur création article: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la création de l\'article', message: error.message }); } } // ======================================== // GESTION PROJETS // ======================================== /** * GET /api/projects - Liste tous les projets */ async getProjects(req, res) { try { const projects = Array.from(this.projects.values()); res.json({ success: true, data: { projects, total: projects.length }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération projets: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des projets', message: error.message }); } } /** * POST /api/projects - Créer un nouveau projet */ async createProject(req, res) { try { // Validation body null/undefined if (!req.body) { return res.status(400).json({ success: false, error: 'Corps de requête requis' }); } const { name, description, config = {} } = req.body; if (!name) { return res.status(400).json({ success: false, error: 'Nom du projet requis' }); } const project = { id: `project_${Date.now()}`, name, description, config, createdAt: new Date().toISOString(), articlesCount: 0 }; this.projects.set(project.id, project); logSh(`📁 Projet créé: ${name}`, 'INFO'); res.status(201).json({ success: true, data: project, message: 'Projet créé avec succès' }); } catch (error) { logSh(`❌ Erreur création projet: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la création du projet', message: error.message }); } } // ======================================== // GESTION TEMPLATES // ======================================== /** * GET /api/templates - Liste tous les templates */ async getTemplates(req, res) { try { const templates = Array.from(this.templates.values()); res.json({ success: true, data: { templates, total: templates.length }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération templates: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des templates', message: error.message }); } } /** * POST /api/templates - Créer un nouveau template */ async createTemplate(req, res) { try { const { name, content, description, category = 'custom' } = req.body; if (!name || !content) { return res.status(400).json({ success: false, error: 'Nom et contenu du template requis' }); } const template = { id: `template_${Date.now()}`, name, content, description, category, createdAt: new Date().toISOString() }; this.templates.set(template.id, template); logSh(`📋 Template créé: ${name}`, 'INFO'); res.status(201).json({ success: true, data: template, message: 'Template créé avec succès' }); } catch (error) { logSh(`❌ Erreur création template: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la création du template', message: error.message }); } } // ======================================== // CONFIGURATION & MONITORING // ======================================== /** * GET /api/config/personalities - Configuration personnalités */ async getPersonalitiesConfig(req, res) { try { const personalities = await getPersonalities(); res.json({ success: true, data: { personalities, total: personalities.length }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur config personnalités: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des personnalités', message: error.message }); } } /** * GET /api/health - Health check */ async getHealth(req, res) { try { const health = { status: 'healthy', timestamp: new Date().toISOString(), version: '1.0.0', uptime: process.uptime(), memory: process.memoryUsage(), environment: process.env.NODE_ENV || 'development' }; res.json({ success: true, data: health }); } catch (error) { res.status(500).json({ success: false, error: 'Health check failed', message: error.message }); } } /** * GET /api/metrics - Métriques système */ async getMetrics(req, res) { try { const metrics = { articles: { total: this.articles.size, recent: Array.from(this.articles.values()).filter( a => new Date(a.createdAt) > new Date(Date.now() - 24 * 60 * 60 * 1000) ).length }, projects: { total: this.projects.size }, templates: { total: this.templates.size }, system: { uptime: process.uptime(), memory: process.memoryUsage(), platform: process.platform, nodeVersion: process.version } }; res.json({ success: true, data: metrics, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur métriques: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des métriques', message: error.message }); } } // ======================================== // PROMPT ENGINE API // ======================================== /** * POST /api/generate-prompt - Génère un prompt adaptatif */ async generatePrompt(req, res) { try { const { templateType = 'technical', content = {}, csvData = null, trend = null, layerConfig = {}, customVariables = {} } = req.body; logSh(`🧠 Génération prompt: template=${templateType}, trend=${trend}`, 'INFO'); // Apply trend if specified if (trend) { await this.trendManager.setTrend(trend); } // Generate adaptive prompt const result = await this.promptEngine.generateAdaptivePrompt({ templateType, content, csvData, trend: this.trendManager.getCurrentTrend(), layerConfig, customVariables }); res.json({ success: true, prompt: result.prompt, metadata: result.metadata, timestamp: new Date().toISOString() }); logSh(`✅ Prompt généré: ${result.prompt.length} caractères`, 'DEBUG'); } catch (error) { logSh(`❌ Erreur génération prompt: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la génération du prompt', message: error.message }); } } /** * GET /api/trends - Liste toutes les tendances disponibles */ async getTrends(req, res) { try { const trends = this.trendManager.getAvailableTrends(); const currentTrend = this.trendManager.getCurrentTrend(); res.json({ success: true, data: { trends, currentTrend, total: trends.length }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération tendances: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des tendances', message: error.message }); } } /** * POST /api/trends/:trendId - Applique une tendance */ async setTrend(req, res) { try { const { trendId } = req.params; const { customConfig = null } = req.body; logSh(`🎯 Application tendance: ${trendId}`, 'INFO'); const trend = await this.trendManager.setTrend(trendId, customConfig); res.json({ success: true, data: { trend, applied: true }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur application tendance ${req.params.trendId}: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de l\'application de la tendance', message: error.message }); } } /** * GET /api/prompt-engine/status - Status du moteur de prompts */ async getPromptEngineStatus(req, res) { try { const engineStatus = this.promptEngine.getEngineStatus(); const trendStatus = this.trendManager.getStatus(); const workflowStatus = this.workflowEngine.getEngineStatus(); res.json({ success: true, data: { engine: engineStatus, trends: trendStatus, workflow: workflowStatus, health: 'operational' }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur status prompt engine: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération du status', message: error.message }); } } /** * GET /api/workflow/sequences - Liste toutes les séquences de workflow */ async getWorkflowSequences(req, res) { try { const sequences = this.workflowEngine.getAvailableSequences(); res.json({ success: true, data: { sequences, total: sequences.length }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération séquences workflow: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des séquences workflow', message: error.message }); } } /** * POST /api/workflow/sequences - Crée une séquence de workflow personnalisée */ async createWorkflowSequence(req, res) { try { const { name, sequence } = req.body; if (!name || !sequence) { return res.status(400).json({ success: false, error: 'Nom et séquence requis' }); } if (!this.workflowEngine.validateSequence(sequence)) { return res.status(400).json({ success: false, error: 'Séquence invalide' }); } const createdSequence = this.workflowEngine.createCustomSequence(name, sequence); res.json({ success: true, data: { name, sequence: createdSequence }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur création séquence workflow: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la création de la séquence workflow', message: error.message }); } } /** * POST /api/workflow/execute - Exécute un workflow configurable */ async executeConfigurableWorkflow(req, res) { try { const { content, sequenceName = 'default', customSequence = null, selectiveConfig = {}, adversarialConfig = {}, humanConfig = {}, patternConfig = {}, csvData = {}, personalities = {} } = req.body; if (!content || typeof content !== 'object') { return res.status(400).json({ success: false, error: 'Contenu requis (objet)' }); } logSh(`🔄 Exécution workflow configurable: ${customSequence ? 'custom' : sequenceName}`, 'INFO'); const result = await this.workflowEngine.executeConfigurableWorkflow(content, { sequenceName, customSequence, selectiveConfig, adversarialConfig, humanConfig, patternConfig, csvData, personalities }); res.json({ success: result.success, data: { content: result.content, stats: result.stats }, error: result.error || null, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur exécution workflow configurable: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de l\'exécution du workflow configurable', message: error.message }); } } } module.exports = { APIController }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/ConfigManager.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: ConfigManager.js // RESPONSABILITÉ: Gestion CRUD des configurations modulaires et pipelines // STOCKAGE: Fichiers JSON dans configs/ et configs/pipelines/ // ======================================== const fs = require('fs').promises; const path = require('path'); const { logSh } = require('./ErrorReporting'); const { PipelineDefinition } = require('./pipeline/PipelineDefinition'); class ConfigManager { constructor() { this.configDir = path.join(__dirname, '../configs'); this.pipelinesDir = path.join(__dirname, '../configs/pipelines'); this.ensureConfigDir(); } async ensureConfigDir() { try { await fs.mkdir(this.configDir, { recursive: true }); await fs.mkdir(this.pipelinesDir, { recursive: true }); logSh(`📁 Dossiers configs vérifiés: ${this.configDir}`, 'DEBUG'); } catch (error) { logSh(`⚠️ Erreur création dossier configs: ${error.message}`, 'WARNING'); } } /** * Sauvegarder une configuration * @param {string} name - Nom de la configuration * @param {object} config - Configuration modulaire * @returns {object} - { success: true, name: sanitizedName } */ async saveConfig(name, config) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.configDir, `${sanitizedName}.json`); const configData = { name: sanitizedName, displayName: name, config, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; await fs.writeFile(filePath, JSON.stringify(configData, null, 2), 'utf-8'); logSh(`💾 Config sauvegardée: ${name} → ${sanitizedName}.json`, 'INFO'); return { success: true, name: sanitizedName }; } /** * Charger une configuration * @param {string} name - Nom de la configuration * @returns {object} - Configuration complète */ async loadConfig(name) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.configDir, `${sanitizedName}.json`); try { const data = await fs.readFile(filePath, 'utf-8'); const configData = JSON.parse(data); logSh(`📂 Config chargée: ${name}`, 'DEBUG'); return configData; } catch (error) { logSh(`❌ Config non trouvée: ${name}`, 'ERROR'); throw new Error(`Configuration "${name}" non trouvée`); } } /** * Lister toutes les configurations * @returns {array} - Liste des configurations avec métadonnées */ async listConfigs() { try { const files = await fs.readdir(this.configDir); const jsonFiles = files.filter(f => f.endsWith('.json')); const configs = await Promise.all( jsonFiles.map(async (file) => { const filePath = path.join(this.configDir, file); const data = await fs.readFile(filePath, 'utf-8'); const configData = JSON.parse(data); return { name: configData.name, displayName: configData.displayName || configData.name, createdAt: configData.createdAt, updatedAt: configData.updatedAt }; }) ); // Trier par date de mise à jour (plus récent en premier) return configs.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt) ); } catch (error) { logSh(`⚠️ Erreur listing configs: ${error.message}`, 'WARNING'); return []; } } /** * Supprimer une configuration * @param {string} name - Nom de la configuration * @returns {object} - { success: true } */ async deleteConfig(name) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.configDir, `${sanitizedName}.json`); await fs.unlink(filePath); logSh(`🗑️ Config supprimée: ${name}`, 'INFO'); return { success: true }; } /** * Vérifier si une configuration existe * @param {string} name - Nom de la configuration * @returns {boolean} */ async configExists(name) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.configDir, `${sanitizedName}.json`); try { await fs.access(filePath); return true; } catch { return false; } } /** * Mettre à jour une configuration existante * @param {string} name - Nom de la configuration * @param {object} config - Nouvelle configuration * @returns {object} - { success: true, name: sanitizedName } */ async updateConfig(name, config) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.configDir, `${sanitizedName}.json`); // Charger config existante pour garder createdAt const existingData = await this.loadConfig(name); const configData = { name: sanitizedName, displayName: name, config, createdAt: existingData.createdAt, // Garder date création updatedAt: new Date().toISOString() }; await fs.writeFile(filePath, JSON.stringify(configData, null, 2), 'utf-8'); logSh(`♻️ Config mise à jour: ${name}`, 'INFO'); return { success: true, name: sanitizedName }; } // ======================================== // PIPELINE MANAGEMENT // ======================================== /** * Sauvegarder un pipeline * @param {object} pipelineDefinition - Définition complète du pipeline * @returns {object} - { success: true, name: sanitizedName } */ async savePipeline(pipelineDefinition) { // Validation du pipeline const validation = PipelineDefinition.validate(pipelineDefinition); if (!validation.valid) { throw new Error(`Pipeline invalide: ${validation.errors.join(', ')}`); } const sanitizedName = pipelineDefinition.name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`); // Ajouter metadata de sauvegarde const pipelineData = { ...pipelineDefinition, metadata: { ...pipelineDefinition.metadata, savedAt: new Date().toISOString() } }; await fs.writeFile(filePath, JSON.stringify(pipelineData, null, 2), 'utf-8'); logSh(`💾 Pipeline sauvegardé: ${pipelineDefinition.name} → ${sanitizedName}.json`, 'INFO'); return { success: true, name: sanitizedName }; } /** * Charger un pipeline * @param {string} name - Nom du pipeline * @returns {object} - Pipeline complet */ async loadPipeline(name) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`); try { const data = await fs.readFile(filePath, 'utf-8'); const pipeline = JSON.parse(data); // Validation du pipeline chargé const validation = PipelineDefinition.validate(pipeline); if (!validation.valid) { throw new Error(`Pipeline chargé invalide: ${validation.errors.join(', ')}`); } logSh(`📂 Pipeline chargé: ${name}`, 'DEBUG'); return pipeline; } catch (error) { logSh(`❌ Pipeline non trouvé: ${name}`, 'ERROR'); throw new Error(`Pipeline "${name}" non trouvé`); } } /** * Lister tous les pipelines * @returns {array} - Liste des pipelines avec métadonnées */ async listPipelines() { try { const files = await fs.readdir(this.pipelinesDir); const jsonFiles = files.filter(f => f.endsWith('.json')); const pipelines = await Promise.all( jsonFiles.map(async (file) => { const filePath = path.join(this.pipelinesDir, file); const data = await fs.readFile(filePath, 'utf-8'); const pipeline = JSON.parse(data); // Obtenir résumé du pipeline const summary = PipelineDefinition.getSummary(pipeline); return { name: pipeline.name, description: pipeline.description, steps: summary.totalSteps, summary: summary.summary, estimatedDuration: summary.duration.formatted, tags: pipeline.metadata?.tags || [], createdAt: pipeline.metadata?.created, savedAt: pipeline.metadata?.savedAt }; }) ); // Trier par date de sauvegarde (plus récent en premier) return pipelines.sort((a, b) => { const dateA = new Date(a.savedAt || a.createdAt || 0); const dateB = new Date(b.savedAt || b.createdAt || 0); return dateB - dateA; }); } catch (error) { logSh(`⚠️ Erreur listing pipelines: ${error.message}`, 'WARNING'); return []; } } /** * Supprimer un pipeline * @param {string} name - Nom du pipeline * @returns {object} - { success: true } */ async deletePipeline(name) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`); await fs.unlink(filePath); logSh(`🗑️ Pipeline supprimé: ${name}`, 'INFO'); return { success: true }; } /** * Vérifier si un pipeline existe * @param {string} name - Nom du pipeline * @returns {boolean} */ async pipelineExists(name) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`); try { await fs.access(filePath); return true; } catch { return false; } } /** * Mettre à jour un pipeline existant * @param {string} name - Nom du pipeline * @param {object} pipelineDefinition - Nouvelle définition * @returns {object} - { success: true, name: sanitizedName } */ async updatePipeline(name, pipelineDefinition) { // Validation const validation = PipelineDefinition.validate(pipelineDefinition); if (!validation.valid) { throw new Error(`Pipeline invalide: ${validation.errors.join(', ')}`); } const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`); // Charger pipeline existant pour garder metadata originale let existingMetadata = {}; try { const existing = await this.loadPipeline(name); existingMetadata = existing.metadata || {}; } catch { // Pipeline n'existe pas encore, on continue } const pipelineData = { ...pipelineDefinition, metadata: { ...existingMetadata, ...pipelineDefinition.metadata, created: existingMetadata.created || pipelineDefinition.metadata?.created, updated: new Date().toISOString(), savedAt: new Date().toISOString() } }; await fs.writeFile(filePath, JSON.stringify(pipelineData, null, 2), 'utf-8'); logSh(`♻️ Pipeline mis à jour: ${name}`, 'INFO'); return { success: true, name: sanitizedName }; } /** * Cloner un pipeline * @param {string} sourceName - Nom du pipeline source * @param {string} newName - Nom du nouveau pipeline * @returns {object} - { success: true, name: sanitizedName } */ async clonePipeline(sourceName, newName) { const sourcePipeline = await this.loadPipeline(sourceName); const clonedPipeline = PipelineDefinition.clone(sourcePipeline, newName); return await this.savePipeline(clonedPipeline); } } module.exports = { ConfigManager }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/StepByStepSessionManager.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: StepByStepSessionManager.js // RESPONSABILITÉ: Gestion des sessions step-by-step // ======================================== // Pas besoin d'uuid externe, on utilise notre générateur simple const { logSh } = require('./ErrorReporting'); /** * GESTIONNAIRE DE SESSIONS STEP-BY-STEP * Gère les sessions de test modulaire pas-à-pas avec TTL */ class StepByStepSessionManager { constructor() { this.sessions = new Map(); this.TTL = 30 * 60 * 1000; // 30 minutes // Nettoyage automatique toutes les 5 minutes setInterval(() => this.cleanupExpiredSessions(), 5 * 60 * 1000); logSh('🎯 SessionManager initialisé', 'DEBUG'); } // ======================================== // GESTION DES SESSIONS // ======================================== /** * Crée une nouvelle session */ createSession(inputData) { const sessionId = this.generateUUID(); const session = { id: sessionId, createdAt: Date.now(), lastAccessedAt: Date.now(), inputData: this.validateInputData(inputData), currentStep: 0, completedSteps: [], results: [], globalStats: { totalDuration: 0, totalTokens: 0, totalCost: 0, llmCalls: [], startTime: Date.now(), endTime: null }, steps: this.generateStepsList(), status: 'initialized' }; this.sessions.set(sessionId, session); logSh(`✅ Session créée: ${sessionId}`, 'INFO'); return session; } /** * Récupère une session */ getSession(sessionId) { const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session introuvable: ${sessionId}`); } if (this.isSessionExpired(session)) { this.deleteSession(sessionId); throw new Error(`Session expirée: ${sessionId}`); } session.lastAccessedAt = Date.now(); return session; } /** * Met à jour une session */ updateSession(sessionId, updates) { const session = this.getSession(sessionId); Object.assign(session, updates); session.lastAccessedAt = Date.now(); logSh(`📝 Session mise à jour: ${sessionId}`, 'DEBUG'); return session; } /** * Supprime une session */ deleteSession(sessionId) { const deleted = this.sessions.delete(sessionId); if (deleted) { logSh(`🗑️ Session supprimée: ${sessionId}`, 'INFO'); } return deleted; } /** * Liste toutes les sessions actives */ listSessions() { const sessions = []; for (const [id, session] of this.sessions) { if (!this.isSessionExpired(session)) { sessions.push({ id: session.id, createdAt: session.createdAt, status: session.status, currentStep: session.currentStep, totalSteps: session.steps.length, inputData: { mc0: session.inputData.mc0, personality: session.inputData.personality } }); } } return sessions; } // ======================================== // GESTION DES ÉTAPES // ======================================== /** * Ajoute le résultat d'une étape */ addStepResult(sessionId, stepId, result) { const session = this.getSession(sessionId); // Marquer l'étape comme complétée if (!session.completedSteps.includes(stepId)) { session.completedSteps.push(stepId); } // Ajouter le résultat const stepResult = { stepId: stepId, system: result.system, timestamp: Date.now(), success: result.success, result: result.result || null, error: result.error || null, stats: result.stats || {}, formatted: result.formatted || null }; session.results.push(stepResult); // Mettre à jour les stats globales this.updateGlobalStats(session, result.stats || {}); // Mettre à jour le statut de l'étape const step = session.steps.find(s => s.id === stepId); if (step) { step.status = result.success ? 'completed' : 'error'; step.duration = (result.stats && result.stats.duration) || 0; step.error = result.error || null; } // Mettre à jour currentStep si nécessaire if (stepId > session.currentStep) { session.currentStep = stepId; } logSh(`📊 Résultat étape ${stepId} ajouté à session ${sessionId}`, 'DEBUG'); return session; } /** * Obtient le résultat d'une étape */ getStepResult(sessionId, stepId) { const session = this.getSession(sessionId); return session.results.find(r => r.stepId === stepId) || null; } /** * Reset une session */ resetSession(sessionId) { const session = this.getSession(sessionId); session.currentStep = 0; session.completedSteps = []; session.results = []; session.globalStats = { totalDuration: 0, totalTokens: 0, totalCost: 0, llmCalls: [], startTime: Date.now(), endTime: null }; session.steps = this.generateStepsList(); session.status = 'initialized'; logSh(`🔄 Session reset: ${sessionId}`, 'INFO'); return session; } // ======================================== // HELPERS PRIVÉS // ======================================== /** * Génère un UUID simple */ generateUUID() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } /** * Valide les données d'entrée */ validateInputData(inputData) { const validated = { mc0: inputData.mc0 || 'mot-clé principal', t0: inputData.t0 || 'titre principal', mcPlus1: inputData.mcPlus1 || '', tPlus1: inputData.tPlus1 || '', personality: inputData.personality || 'random', tMinus1: inputData.tMinus1 || '', xmlTemplate: inputData.xmlTemplate || null }; return validated; } /** * Génère la liste des étapes */ generateStepsList() { return [ { id: 1, system: 'initial-generation', name: 'Initial Generation', description: 'Génération de contenu initial avec Claude', status: 'pending', duration: 0, error: null }, { id: 2, system: 'selective', name: 'Selective Enhancement', description: 'Amélioration sélective (Technique → Transitions → Style)', status: 'pending', duration: 0, error: null }, { id: 3, system: 'adversarial', name: 'Adversarial Generation', description: 'Génération adversariale anti-détection', status: 'pending', duration: 0, error: null }, { id: 4, system: 'human-simulation', name: 'Human Simulation', description: 'Simulation comportements humains', status: 'pending', duration: 0, error: null }, { id: 5, system: 'pattern-breaking', name: 'Pattern Breaking', description: 'Cassage de patterns IA', status: 'pending', duration: 0, error: null } ]; } /** * Met à jour les statistiques globales */ updateGlobalStats(session, stepStats) { const global = session.globalStats; global.totalDuration += stepStats.duration || 0; global.totalTokens += stepStats.tokensUsed || 0; global.totalCost += stepStats.cost || 0; if (stepStats.llmCalls && Array.isArray(stepStats.llmCalls)) { global.llmCalls.push(...stepStats.llmCalls); } // Marquer la fin si toutes les étapes sont complétées if (session.completedSteps.length === session.steps.length) { global.endTime = Date.now(); session.status = 'completed'; } } /** * Vérifie si une session est expirée */ isSessionExpired(session) { return (Date.now() - session.lastAccessedAt) > this.TTL; } /** * Nettoie les sessions expirées */ cleanupExpiredSessions() { let cleaned = 0; for (const [id, session] of this.sessions) { if (this.isSessionExpired(session)) { this.sessions.delete(id); cleaned++; } } if (cleaned > 0) { logSh(`🧹 ${cleaned} sessions expirées nettoyées`, 'DEBUG'); } } // ======================================== // EXPORT/IMPORT // ======================================== /** * Exporte une session au format JSON */ exportSession(sessionId) { const session = this.getSession(sessionId); return { session: { id: session.id, createdAt: new Date(session.createdAt).toISOString(), inputData: session.inputData, results: session.results, globalStats: session.globalStats, steps: session.steps.map(step => ({ ...step, duration: step.duration ? `${step.duration}ms` : '0ms' })) }, exportedAt: new Date().toISOString(), version: '1.0.0' }; } } // Instance singleton const sessionManager = new StepByStepSessionManager(); module.exports = { StepByStepSessionManager, sessionManager }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/shared/QueueProcessor.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // QUEUE PROCESSOR - CLASSE COMMUNE // Responsabilité: Logique partagée de queue, retry, persistance // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { handleModularWorkflow } = require('../Main'); const { readInstructionsData } = require('../BrainConfig'); const fs = require('fs').promises; const path = require('path'); /** * QUEUE PROCESSOR BASE * Classe commune pour la gestion de queue avec retry logic et persistance */ class QueueProcessor { constructor(options = {}) { this.name = options.name || 'QueueProcessor'; this.configPath = options.configPath; this.statusPath = options.statusPath; this.queuePath = options.queuePath; // Configuration par défaut this.config = { selective: 'standardEnhancement', adversarial: 'light', humanSimulation: 'none', patternBreaking: 'none', intensity: 1.0, rowRange: { start: 2, end: 10 }, saveIntermediateSteps: false, maxRetries: 3, delayBetweenItems: 1000, batchSize: 1, ...options.config }; // État du processeur this.isRunning = false; this.isPaused = false; this.currentRow = null; this.queue = []; this.processedItems = []; this.failedItems = []; // Métriques this.startTime = null; this.processedCount = 0; this.errorCount = 0; // Stats détaillées this.stats = { itemsQueued: 0, itemsProcessed: 0, itemsFailed: 0, averageProcessingTime: 0, totalProcessingTime: 0, startTime: Date.now(), lastProcessedAt: null }; // Callbacks optionnels this.onStatusUpdate = null; this.onProgress = null; this.onError = null; this.onComplete = null; this.onItemProcessed = null; } // ======================================== // INITIALISATION // ======================================== /** * Initialise le processeur */ async initialize() { try { await this.loadConfig(); await this.initializeQueue(); logSh(`🎯 ${this.name} initialisé`, 'DEBUG'); } catch (error) { logSh(`❌ Erreur initialisation ${this.name}: ${error.message}`, 'ERROR'); throw error; } } /** * Charge la configuration */ async loadConfig() { if (!this.configPath) return; try { const configData = await fs.readFile(this.configPath, 'utf8'); this.config = { ...this.config, ...JSON.parse(configData) }; logSh(`📋 Configuration ${this.name} chargée`, 'DEBUG'); } catch (error) { logSh(`⚠️ Configuration non trouvée pour ${this.name}, utilisation valeurs par défaut`, 'WARNING'); } } /** * Initialise les fichiers de configuration */ async initializeFiles() { if (!this.configPath) return; try { const configDir = path.dirname(this.configPath); await fs.mkdir(configDir, { recursive: true }); // Créer config par défaut si inexistant try { await fs.access(this.configPath); } catch { await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2)); logSh(`📝 Configuration ${this.name} par défaut créée`, 'DEBUG'); } // Créer status par défaut si inexistant if (this.statusPath) { const defaultStatus = this.getDefaultStatus(); try { await fs.access(this.statusPath); } catch { await fs.writeFile(this.statusPath, JSON.stringify(defaultStatus, null, 2)); logSh(`📊 Status ${this.name} par défaut créé`, 'DEBUG'); } } } catch (error) { logSh(`❌ Erreur initialisation fichiers ${this.name}: ${error.message}`, 'ERROR'); } } // ======================================== // GESTION QUEUE // ======================================== /** * Initialise la queue */ async initializeQueue() { try { // Essayer de charger la queue existante if (this.queuePath) { try { const queueData = await fs.readFile(this.queuePath, 'utf8'); const savedQueue = JSON.parse(queueData); if (savedQueue.queue && Array.isArray(savedQueue.queue)) { this.queue = savedQueue.queue; this.processedCount = savedQueue.processedCount || 0; logSh(`📊 Queue ${this.name} restaurée: ${this.queue.length} éléments`, 'DEBUG'); } } catch { // Queue n'existe pas, on la créera } } // Si queue vide, la populer if (this.queue.length === 0) { await this.populateQueue(); } } catch (error) { logSh(`❌ Erreur initialisation queue ${this.name}: ${error.message}`, 'ERROR'); } } /** * Popule la queue avec les lignes à traiter */ async populateQueue() { try { this.queue = []; const { start, end } = this.config.rowRange; for (let rowNumber = start; rowNumber <= end; rowNumber++) { this.queue.push({ rowNumber, status: 'pending', attempts: 0, maxAttempts: this.config.maxRetries, error: null, result: null, startTime: null, endTime: null, addedAt: Date.now() }); } await this.saveQueue(); this.stats.itemsQueued = this.queue.length; logSh(`📋 Queue ${this.name} populée: ${this.queue.length} lignes (${start} à ${end})`, 'INFO'); } catch (error) { logSh(`❌ Erreur population queue ${this.name}: ${error.message}`, 'ERROR'); throw error; } } /** * Popule la queue depuis Google Sheets (version avancée) */ async populateQueueFromSheets() { try { this.queue = []; let currentRow = this.config.startRow || 2; let consecutiveEmptyRows = 0; const maxEmptyRows = 5; while (currentRow <= (this.config.endRow || 50)) { if (this.config.endRow && currentRow > this.config.endRow) { break; } try { const csvData = await readInstructionsData(currentRow); if (!csvData || !csvData.mc0) { consecutiveEmptyRows++; if (consecutiveEmptyRows >= maxEmptyRows) { logSh(`🛑 Arrêt scan après ${maxEmptyRows} lignes vides consécutives`, 'INFO'); break; } } else { consecutiveEmptyRows = 0; this.queue.push({ rowNumber: currentRow, data: csvData, status: 'pending', attempts: 0, maxAttempts: this.config.maxRetries, error: null, result: null, startTime: null, endTime: null, addedAt: Date.now() }); } } catch (error) { consecutiveEmptyRows++; if (consecutiveEmptyRows >= maxEmptyRows) { break; } } currentRow++; } await this.saveQueue(); this.stats.itemsQueued = this.queue.length; logSh(`📊 Queue ${this.name} chargée depuis Sheets: ${this.stats.itemsQueued} éléments`, 'INFO'); } catch (error) { logSh(`❌ Erreur chargement queue depuis Sheets: ${error.message}`, 'ERROR'); throw error; } } /** * Sauvegarde la queue */ async saveQueue() { if (!this.queuePath) return; try { const queueData = { queue: this.queue, processedCount: this.processedCount, lastUpdate: new Date().toISOString() }; await fs.writeFile(this.queuePath, JSON.stringify(queueData, null, 2)); } catch (error) { logSh(`❌ Erreur sauvegarde queue ${this.name}: ${error.message}`, 'ERROR'); } } // ======================================== // CONTRÔLES PRINCIPAUX // ======================================== /** * Démarre le traitement */ async start() { return tracer.run(`${this.name}.start`, async () => { if (this.isRunning) { throw new Error(`${this.name} est déjà en cours`); } logSh(`🚀 Démarrage ${this.name}`, 'INFO'); this.isRunning = true; this.isPaused = false; this.startTime = new Date(); this.processedCount = 0; this.errorCount = 0; await this.loadConfig(); if (this.queue.length === 0) { await this.populateQueue(); } await this.updateStatus(); // Démarrer le traitement asynchrone this.processQueue().catch(error => { logSh(`❌ Erreur traitement queue ${this.name}: ${error.message}`, 'ERROR'); this.handleError(error); }); return this.getStatus(); }); } /** * Arrête le traitement */ async stop() { return tracer.run(`${this.name}.stop`, async () => { logSh(`🛑 Arrêt ${this.name}`, 'INFO'); this.isRunning = false; this.isPaused = false; this.currentRow = null; await this.updateStatus(); return this.getStatus(); }); } /** * Met en pause le traitement */ async pause() { return tracer.run(`${this.name}.pause`, async () => { if (!this.isRunning) { throw new Error(`Aucun traitement ${this.name} en cours`); } logSh(`⏸️ Mise en pause ${this.name}`, 'INFO'); this.isPaused = true; await this.updateStatus(); return this.getStatus(); }); } /** * Reprend le traitement */ async resume() { return tracer.run(`${this.name}.resume`, async () => { if (!this.isRunning || !this.isPaused) { throw new Error(`Aucun traitement ${this.name} en pause`); } logSh(`▶️ Reprise ${this.name}`, 'INFO'); this.isPaused = false; await this.updateStatus(); // Reprendre le traitement this.processQueue().catch(error => { logSh(`❌ Erreur reprise traitement ${this.name}: ${error.message}`, 'ERROR'); this.handleError(error); }); return this.getStatus(); }); } // ======================================== // TRAITEMENT QUEUE // ======================================== /** * Traite la queue */ async processQueue() { return tracer.run(`${this.name}.processQueue`, async () => { while (this.isRunning && !this.isPaused) { const nextItem = this.queue.find(item => item.status === 'pending' || (item.status === 'error' && item.attempts < item.maxAttempts)); if (!nextItem) { logSh(`✅ Traitement ${this.name} terminé`, 'INFO'); await this.complete(); break; } await this.processItem(nextItem); if (this.config.delayBetweenItems > 0) { await this.sleep(this.config.delayBetweenItems); } } }); } /** * Traite un élément de la queue */ async processItem(item) { return tracer.run(`${this.name}.processItem`, async () => { logSh(`🔄 Traitement ${this.name} ligne ${item.rowNumber} (tentative ${item.attempts + 1}/${item.maxAttempts})`, 'INFO'); this.currentRow = item.rowNumber; item.status = 'processing'; item.startTime = new Date().toISOString(); item.attempts++; await this.updateStatus(); await this.saveQueue(); try { const result = await this.processRow(item.rowNumber, item.data); // Succès item.status = 'completed'; item.result = result; item.endTime = new Date().toISOString(); item.error = null; this.processedCount++; this.processedItems.push(item); const duration = Date.now() - new Date(item.startTime).getTime(); this.stats.itemsProcessed++; this.stats.totalProcessingTime += duration; this.stats.averageProcessingTime = Math.round(this.stats.totalProcessingTime / this.stats.itemsProcessed); this.stats.lastProcessedAt = Date.now(); logSh(`✅ ${this.name} ligne ${item.rowNumber} traitée avec succès (${duration}ms)`, 'INFO'); if (this.onItemProcessed) { this.onItemProcessed(item, result); } if (this.onProgress) { this.onProgress(item, this.getProgress()); } } catch (error) { item.error = { message: error.message, stack: error.stack, timestamp: new Date().toISOString() }; if (item.attempts >= item.maxAttempts) { item.status = 'failed'; this.errorCount++; this.failedItems.push(item); logSh(`❌ ${this.name} ligne ${item.rowNumber} échouée définitivement après ${item.attempts} tentatives`, 'ERROR'); } else { item.status = 'error'; logSh(`⚠️ ${this.name} ligne ${item.rowNumber} échouée, retry possible`, 'WARNING'); } if (this.onError) { this.onError(item, error); } } this.currentRow = null; await this.updateStatus(); await this.saveQueue(); }); } /** * Traite une ligne spécifique - à surcharger dans les classes enfants */ async processRow(rowNumber, data = null) { const rowConfig = this.buildRowConfig(rowNumber, data); logSh(`🎯 Configuration ${this.name} ligne ${rowNumber}: ${JSON.stringify(rowConfig)}`, 'DEBUG'); const result = await handleModularWorkflow(rowConfig); logSh(`📊 Résultat ${this.name} ligne ${rowNumber}: ${result ? 'SUCCESS' : 'FAILED'}`, 'INFO'); return result; } /** * Construit la configuration pour une ligne - à surcharger si nécessaire */ buildRowConfig(rowNumber, data = null) { return { rowNumber, source: `${this.name.toLowerCase()}_row_${rowNumber}`, selectiveStack: this.config.selective, adversarialMode: this.config.adversarial, humanSimulationMode: this.config.humanSimulation, patternBreakingMode: this.config.patternBreaking, intensity: this.config.intensity, saveIntermediateSteps: this.config.saveIntermediateSteps, data }; } // ======================================== // GESTION ÉTAT // ======================================== /** * Met à jour le status */ async updateStatus() { const status = this.getStatus(); if (this.statusPath) { try { await fs.writeFile(this.statusPath, JSON.stringify(status, null, 2)); } catch (error) { logSh(`❌ Erreur mise à jour status ${this.name}: ${error.message}`, 'ERROR'); } } if (this.onStatusUpdate) { this.onStatusUpdate(status); } } /** * Retourne le status actuel */ getStatus() { const now = new Date(); const completedItems = this.queue.filter(item => item.status === 'completed').length; const failedItems = this.queue.filter(item => item.status === 'failed').length; const totalItems = this.queue.length; const progress = totalItems > 0 ? ((completedItems + failedItems) / totalItems) * 100 : 0; let status = 'idle'; if (this.isRunning && this.isPaused) { status = 'paused'; } else if (this.isRunning) { status = 'running'; } else if (completedItems + failedItems === totalItems && totalItems > 0) { status = 'completed'; } return { status, currentRow: this.currentRow, totalRows: totalItems, completedRows: completedItems, failedRows: failedItems, progress: Math.round(progress), startTime: this.startTime ? this.startTime.toISOString() : null, estimatedEnd: this.estimateCompletionTime(), errors: this.queue.filter(item => item.error).map(item => ({ rowNumber: item.rowNumber, error: item.error, attempts: item.attempts })), lastResult: this.getLastResult(), config: this.config, queue: this.queue, stats: this.stats }; } /** * Retourne la progression détaillée */ getProgress() { // Calcul direct des métriques sans appeler getStatus() pour éviter la récursion const now = new Date(); const elapsed = this.startTime ? now - this.startTime : 0; const completedRows = this.processedItems.length; const failedRows = this.failedItems.length; const totalRows = this.queue.length + completedRows + failedRows; const avgTimePerRow = completedRows > 0 ? elapsed / completedRows : 0; const remainingRows = totalRows - completedRows - failedRows; const estimatedRemaining = avgTimePerRow * remainingRows; return { status: this.status, currentRow: this.currentItem ? this.currentItem.rowNumber : null, totalRows: totalRows, completedRows: completedRows, failedRows: failedRows, progress: totalRows > 0 ? Math.round((completedRows / totalRows) * 100) : 0, startTime: this.startTime ? this.startTime.toISOString() : null, estimatedEnd: null, // Calculé séparément pour éviter récursion errors: this.failedItems.map(item => ({ row: item.rowNumber, error: item.error })), lastResult: this.processedItems.length > 0 ? this.processedItems[this.processedItems.length - 1].result : null, config: this.config, queue: this.queue, stats: { itemsQueued: this.queue.length, itemsProcessed: completedRows, itemsFailed: failedRows, averageProcessingTime: avgTimePerRow, totalProcessingTime: elapsed, startTime: this.startTime ? this.startTime.getTime() : null, lastProcessedAt: this.processedItems.length > 0 ? this.processedItems[this.processedItems.length - 1].endTime : null }, metrics: { elapsedTime: elapsed, avgTimePerRow: avgTimePerRow, estimatedRemaining: estimatedRemaining, completionPercentage: totalRows > 0 ? (completedRows / totalRows) * 100 : 0, throughput: completedRows > 0 && elapsed > 0 ? (completedRows / (elapsed / 1000 / 60)) : 0 } }; } /** * Estime l'heure de fin */ estimateCompletionTime() { if (!this.startTime || !this.isRunning || this.isPaused) { return null; } // Calcul direct sans appeler getProgress() pour éviter la récursion const now = new Date(); const elapsed = now - this.startTime; const completedRows = this.processedItems.length; if (completedRows > 0) { const avgTimePerRow = elapsed / completedRows; const remainingRows = this.queue.length; const estimatedRemaining = avgTimePerRow * remainingRows; if (estimatedRemaining > 0) { const endTime = new Date(Date.now() + estimatedRemaining); return endTime.toISOString(); } } return null; } /** * Retourne le dernier résultat */ getLastResult() { const completedItems = this.queue.filter(item => item.status === 'completed'); if (completedItems.length === 0) return null; const lastItem = completedItems[completedItems.length - 1]; return { rowNumber: lastItem.rowNumber, result: lastItem.result, endTime: lastItem.endTime }; } /** * Status par défaut */ getDefaultStatus() { return { status: 'idle', currentRow: null, totalRows: 0, progress: 0, startTime: null, estimatedEnd: null, errors: [], lastResult: null, config: this.config }; } // ======================================== // GESTION ERREURS // ======================================== /** * Gère les erreurs critiques */ async handleError(error) { logSh(`💥 Erreur critique ${this.name}: ${error.message}`, 'ERROR'); this.isRunning = false; this.isPaused = false; await this.updateStatus(); if (this.onError) { this.onError(null, error); } } /** * Termine le traitement */ async complete() { logSh(`🏁 Traitement ${this.name} terminé`, 'INFO'); this.isRunning = false; this.isPaused = false; this.currentRow = null; await this.updateStatus(); if (this.onComplete) { this.onComplete(this.getStatus()); } } // ======================================== // UTILITAIRES // ======================================== /** * Pause l'exécution */ async sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Reset la queue */ async resetQueue() { logSh(`🔄 Reset de la queue ${this.name}`, 'INFO'); this.queue = []; this.processedCount = 0; this.errorCount = 0; await this.populateQueue(); await this.updateStatus(); } /** * Configure les callbacks */ setCallbacks({ onStatusUpdate, onProgress, onError, onComplete, onItemProcessed }) { this.onStatusUpdate = onStatusUpdate; this.onProgress = onProgress; this.onError = onError; this.onComplete = onComplete; this.onItemProcessed = onItemProcessed; } } // ============= EXPORTS ============= module.exports = { QueueProcessor }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/batch/BatchProcessor.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // BATCH PROCESSOR - REFACTORISÉ // Responsabilité: Traitement batch interface web avec configuration flexible // ======================================== const { QueueProcessor } = require('../shared/QueueProcessor'); const { logSh } = require('../ErrorReporting'); const path = require('path'); /** * BATCH PROCESSOR * Spécialisé pour interface web avec configuration modulaire flexible */ class BatchProcessor extends QueueProcessor { constructor() { super({ name: 'BatchProcessor', configPath: path.join(__dirname, '../../config/batch-config.json'), statusPath: path.join(__dirname, '../../config/batch-status.json'), queuePath: path.join(__dirname, '../../config/batch-queue.json'), config: { selective: 'standardEnhancement', adversarial: 'light', humanSimulation: 'none', patternBreaking: 'none', intensity: 1.0, rowRange: { start: 2, end: 10 }, saveIntermediateSteps: false, maxRetries: 3, delayBetweenItems: 1000 } }); // Initialisation différée pour éviter le blocage au démarrage serveur // this.initialize().catch(error => { // logSh(`❌ Erreur initialisation BatchProcessor: ${error.message}`, 'ERROR'); // }); } /** * Alias pour compatibilité - Initialise les fichiers */ async initializeFiles() { return await super.initializeFiles(); } /** * Alias pour compatibilité - Initialise le processeur */ async initializeProcessor() { return await this.initialize(); } /** * Construit la configuration spécifique BatchProcessor */ buildRowConfig(rowNumber, data = null) { return { rowNumber, source: 'batch_processor', selectiveStack: this.config.selective, adversarialMode: this.config.adversarial, humanSimulationMode: this.config.humanSimulation, patternBreakingMode: this.config.patternBreaking, intensity: this.config.intensity, saveIntermediateSteps: this.config.saveIntermediateSteps }; } /** * API spécifique BatchProcessor - Configuration */ async updateConfiguration(newConfig) { try { // Validation basique const requiredFields = ['selective', 'adversarial', 'humanSimulation', 'patternBreaking', 'intensity', 'rowRange']; for (const field of requiredFields) { if (!(field in newConfig)) { throw new Error(`Champ requis manquant: ${field}`); } } // Validation intensité if (newConfig.intensity < 0.5 || newConfig.intensity > 1.5) { throw new Error('Intensité doit être entre 0.5 et 1.5'); } // Validation rowRange if (!newConfig.rowRange.start || !newConfig.rowRange.end || newConfig.rowRange.start >= newConfig.rowRange.end) { throw new Error('Plage de lignes invalide'); } // Mettre à jour la configuration this.config = { ...this.config, ...newConfig }; this.config.lastUpdated = new Date().toISOString(); // Sauvegarder if (this.configPath) { const fs = require('fs').promises; await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2)); } logSh(`✅ Configuration BatchProcessor mise à jour: ${JSON.stringify(newConfig)}`, 'INFO'); return { success: true, config: this.config }; } catch (error) { logSh(`❌ Erreur mise à jour configuration: ${error.message}`, 'ERROR'); throw error; } } /** * Retourne les options disponibles */ getAvailableOptions() { return { selective: ['lightEnhancement', 'standardEnhancement', 'fullEnhancement', 'personalityFocus', 'fluidityFocus'], adversarial: ['none', 'light', 'standard', 'heavy', 'adaptive'], humanSimulation: ['none', 'lightSimulation', 'personalityFocus', 'adaptive'], patternBreaking: ['none', 'syntaxFocus', 'connectorsFocus', 'adaptive'], intensityRange: { min: 0.5, max: 1.5, step: 0.1 } }; } /** * Status étendu avec options disponibles */ getExtendedStatus() { const baseStatus = this.getStatus(); return { ...baseStatus, availableOptions: this.getAvailableOptions(), mode: 'BATCH_MANUAL', timestamp: new Date().toISOString() }; } } // ============= EXPORTS ============= module.exports = { BatchProcessor }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/batch/BatchController.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // BATCH CONTROLLER - API ENDPOINTS // Responsabilité: Gestion API pour traitement batch avec configuration pipeline // ======================================== const { logSh } = require('../ErrorReporting'); const fs = require('fs').promises; const path = require('path'); const { BatchProcessor } = require('./BatchProcessor'); const { DigitalOceanTemplates } = require('./DigitalOceanTemplates'); const { TrendManager } = require('../trend-prompts/TrendManager'); /** * BATCH CONTROLLER * Gestion complète de l'interface de traitement batch */ class BatchController { constructor() { this.configPath = path.join(__dirname, '../../config/batch-config.json'); this.statusPath = path.join(__dirname, '../../config/batch-status.json'); // Initialiser les composants Phase 2 this.batchProcessor = new BatchProcessor(); this.digitalOceanTemplates = new DigitalOceanTemplates(); this.trendManager = new TrendManager(); // Configuration par défaut this.defaultConfig = { selective: 'standardEnhancement', adversarial: 'light', humanSimulation: 'none', patternBreaking: 'none', intensity: 1.0, rowRange: { start: 2, end: 10 }, saveIntermediateSteps: false, trendId: null, // Tendance à appliquer (optionnel) lastUpdated: new Date().toISOString() }; // État par défaut this.defaultStatus = { status: 'idle', currentRow: null, totalRows: 0, progress: 0, startTime: null, estimatedEnd: null, errors: [], lastResult: null, config: this.defaultConfig }; this.initializeFiles(); } /** * Initialise les fichiers de configuration */ async initializeFiles() { try { // Créer le dossier config s'il n'existe pas const configDir = path.dirname(this.configPath); await fs.mkdir(configDir, { recursive: true }); // Créer config par défaut si inexistant try { await fs.access(this.configPath); } catch { await fs.writeFile(this.configPath, JSON.stringify(this.defaultConfig, null, 2)); logSh('📝 Configuration batch par défaut créée', 'DEBUG'); } // Créer status par défaut si inexistant try { await fs.access(this.statusPath); } catch { await fs.writeFile(this.statusPath, JSON.stringify(this.defaultStatus, null, 2)); logSh('📊 Status batch par défaut créé', 'DEBUG'); } } catch (error) { logSh(`❌ Erreur initialisation fichiers batch: ${error.message}`, 'ERROR'); } } // ======================================== // ENDPOINTS CONFIGURATION // ======================================== /** * GET /api/batch/config * Récupère la configuration actuelle */ async getConfig(req, res) { try { // Utiliser la nouvelle API du BatchProcessor refactorisé const status = this.batchProcessor.getExtendedStatus(); // Ajouter les tendances disponibles const availableTrends = this.trendManager.getAvailableTrends(); const currentTrend = this.trendManager.getCurrentTrend(); logSh('📋 Configuration batch récupérée', 'DEBUG'); res.json({ success: true, config: status.config, availableOptions: status.availableOptions, trends: { available: availableTrends, current: currentTrend, categories: this.groupTrendsByCategory(availableTrends) } }); } catch (error) { logSh(`❌ Erreur récupération config: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur récupération configuration', details: error.message }); } } /** * POST /api/batch/config * Sauvegarde la configuration */ async saveConfig(req, res) { try { const newConfig = req.body; // Utiliser la nouvelle API du BatchProcessor refactorisé const result = await this.batchProcessor.updateConfiguration(newConfig); res.json({ success: true, message: 'Configuration sauvegardée avec succès', config: result.config }); } catch (error) { logSh(`❌ Erreur sauvegarde config: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur sauvegarde configuration', details: error.message }); } } // ======================================== // ENDPOINTS CONTRÔLE TRAITEMENT // ======================================== /** * POST /api/batch/start * Démarre le traitement batch */ async startBatch(req, res) { try { // Démarrer le traitement via BatchProcessor const status = await this.batchProcessor.start(); logSh(`🚀 Traitement batch démarré - ${status.totalRows} lignes`, 'INFO'); res.json({ success: true, message: 'Traitement batch démarré', status: status }); } catch (error) { logSh(`❌ Erreur démarrage batch: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur démarrage traitement', details: error.message }); } } /** * POST /api/batch/stop * Arrête le traitement batch */ async stopBatch(req, res) { try { const status = await this.batchProcessor.stop(); logSh('🛑 Traitement batch arrêté', 'INFO'); res.json({ success: true, message: 'Traitement batch arrêté', status: status }); } catch (error) { logSh(`❌ Erreur arrêt batch: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur arrêt traitement', details: error.message }); } } /** * POST /api/batch/pause * Met en pause le traitement */ async pauseBatch(req, res) { try { const status = await this.batchProcessor.pause(); logSh('⏸️ Traitement batch mis en pause', 'INFO'); res.json({ success: true, message: 'Traitement mis en pause', status: status }); } catch (error) { logSh(`❌ Erreur pause batch: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur pause traitement', details: error.message }); } } /** * POST /api/batch/resume * Reprend le traitement */ async resumeBatch(req, res) { try { const status = await this.batchProcessor.resume(); logSh('▶️ Traitement batch repris', 'INFO'); res.json({ success: true, message: 'Traitement repris', status: status }); } catch (error) { logSh(`❌ Erreur reprise batch: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur reprise traitement', details: error.message }); } } // ======================================== // ENDPOINTS MONITORING // ======================================== /** * GET /api/batch/status * Récupère l'état actuel du traitement */ async getStatus(req, res) { try { const status = this.batchProcessor.getStatus(); res.json({ success: true, status: status, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération status: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur récupération status', details: error.message }); } } /** * GET /api/batch/progress * Récupère la progression détaillée */ async getProgress(req, res) { try { const progress = this.batchProcessor.getProgress(); res.json({ success: true, progress: progress, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération progress: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur récupération progression', details: error.message }); } } // ======================================== // ENDPOINTS TENDANCES // ======================================== /** * GET /api/batch/trends * Liste toutes les tendances disponibles */ async getTrends(req, res) { try { const trends = this.trendManager.getAvailableTrends(); const current = this.trendManager.getCurrentTrend(); const status = this.trendManager.getStatus(); res.json({ success: true, trends: { available: trends, current: current, categories: this.groupTrendsByCategory(trends), status: status }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération tendances: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur récupération tendances', details: error.message }); } } /** * POST /api/batch/trends/select * Sélectionne une tendance */ async selectTrend(req, res) { try { const { trendId } = req.body; if (!trendId) { return res.status(400).json({ success: false, error: 'ID de tendance requis' }); } const result = await this.trendManager.setTrend(trendId); logSh(`🎯 Tendance sélectionnée: ${result.name}`, 'INFO'); res.json({ success: true, trend: result, message: `Tendance "${result.name}" appliquée`, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur sélection tendance: ${error.message}`, 'ERROR'); res.status(400).json({ success: false, error: 'Erreur sélection tendance', details: error.message }); } } /** * DELETE /api/batch/trends * Désactive la tendance actuelle */ async clearTrend(req, res) { try { this.trendManager.clearTrend(); logSh('🔄 Tendance désactivée', 'INFO'); res.json({ success: true, message: 'Aucune tendance appliquée', timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur désactivation tendance: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur désactivation tendance', details: error.message }); } } // ======================================== // HELPER METHODS TENDANCES // ======================================== /** * Groupe les tendances par catégorie */ groupTrendsByCategory(trends) { const categories = {}; trends.forEach(trend => { const category = trend.category || 'autre'; if (!categories[category]) { categories[category] = []; } categories[category].push(trend); }); return categories; } // ======================================== // ENDPOINTS DIGITAL OCEAN // ======================================== /** * GET /api/batch/templates * Liste les templates disponibles */ async getTemplates(req, res) { try { const templates = await this.digitalOceanTemplates.listAvailableTemplates(); const stats = this.digitalOceanTemplates.getCacheStats(); res.json({ success: true, templates: templates, cacheStats: stats, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération templates: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur récupération templates', details: error.message }); } } /** * GET /api/batch/templates/:filename * Récupère un template spécifique */ async getTemplate(req, res) { try { const { filename } = req.params; const template = await this.digitalOceanTemplates.getTemplate(filename); res.json({ success: true, filename: filename, template: template, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération template ${req.params.filename}: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur récupération template', details: error.message }); } } /** * DELETE /api/batch/cache * Vide le cache des templates */ async clearCache(req, res) { try { await this.digitalOceanTemplates.clearCache(); res.json({ success: true, message: 'Cache vidé avec succès', timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur vidage cache: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur vidage cache', details: error.message }); } } } // ============= EXPORTS ============= module.exports = { BatchController }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/batch/BatchProcessor.original.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // BATCH PROCESSOR - SYSTÈME DE QUEUE // Responsabilité: Traitement batch des lignes Google Sheets avec pipeline modulaire // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { handleModularWorkflow } = require('../Main'); const { readInstructionsData } = require('../BrainConfig'); const fs = require('fs').promises; const path = require('path'); /** * BATCH PROCESSOR * Système de queue pour traiter les lignes Google Sheets une par une */ class BatchProcessor { constructor() { this.statusPath = path.join(__dirname, '../../config/batch-status.json'); this.configPath = path.join(__dirname, '../../config/batch-config.json'); this.queuePath = path.join(__dirname, '../../config/batch-queue.json'); // État du processeur this.isRunning = false; this.isPaused = false; this.currentRow = null; this.queue = []; this.errors = []; this.results = []; // Configuration par défaut this.config = { selective: 'standardEnhancement', adversarial: 'light', humanSimulation: 'none', patternBreaking: 'none', intensity: 1.0, rowRange: { start: 2, end: 10 }, saveIntermediateSteps: false }; // Métriques this.startTime = null; this.processedCount = 0; this.errorCount = 0; // Callbacks pour updates this.onStatusUpdate = null; this.onProgress = null; this.onError = null; this.onComplete = null; this.initializeProcessor(); } /** * Initialise le processeur */ async initializeProcessor() { try { // Charger la configuration await this.loadConfig(); // Initialiser la queue si elle n'existe pas await this.initializeQueue(); logSh('🎯 BatchProcessor initialisé', 'DEBUG'); } catch (error) { logSh(`❌ Erreur initialisation BatchProcessor: ${error.message}`, 'ERROR'); } } /** * Initialise les fichiers de configuration (alias pour compatibilité tests) */ async initializeFiles() { try { // Créer le dossier config s'il n'existe pas const configDir = path.dirname(this.configPath); await fs.mkdir(configDir, { recursive: true }); // Créer config par défaut si inexistant try { await fs.access(this.configPath); } catch { await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2)); logSh('📝 Configuration batch par défaut créée', 'DEBUG'); } // Créer status par défaut si inexistant const defaultStatus = { status: 'idle', currentRow: null, totalRows: 0, progress: 0, startTime: null, estimatedEnd: null, errors: [], lastResult: null, config: this.config }; try { await fs.access(this.statusPath); } catch { await fs.writeFile(this.statusPath, JSON.stringify(defaultStatus, null, 2)); logSh('📊 Status batch par défaut créé', 'DEBUG'); } } catch (error) { logSh(`❌ Erreur initialisation fichiers batch: ${error.message}`, 'ERROR'); } } /** * Charge la configuration */ async loadConfig() { try { const configData = await fs.readFile(this.configPath, 'utf8'); this.config = JSON.parse(configData); logSh(`📋 Configuration chargée: ${JSON.stringify(this.config)}`, 'DEBUG'); } catch (error) { logSh('⚠️ Configuration non trouvée, utilisation des valeurs par défaut', 'WARNING'); } } /** * Initialise la queue */ async initializeQueue() { try { // Essayer de charger la queue existante try { const queueData = await fs.readFile(this.queuePath, 'utf8'); const savedQueue = JSON.parse(queueData); if (savedQueue.queue && Array.isArray(savedQueue.queue)) { this.queue = savedQueue.queue; this.processedCount = savedQueue.processedCount || 0; logSh(`📊 Queue restaurée: ${this.queue.length} éléments`, 'DEBUG'); } } catch { // Queue n'existe pas, on la créera } // Si queue vide, la populer depuis la configuration if (this.queue.length === 0) { await this.populateQueue(); } } catch (error) { logSh(`❌ Erreur initialisation queue: ${error.message}`, 'ERROR'); } } /** * Popule la queue avec les lignes à traiter */ async populateQueue() { try { this.queue = []; const { start, end } = this.config.rowRange; for (let rowNumber = start; rowNumber <= end; rowNumber++) { this.queue.push({ rowNumber, status: 'pending', attempts: 0, maxAttempts: 3, error: null, result: null, startTime: null, endTime: null }); } await this.saveQueue(); logSh(`📋 Queue populée: ${this.queue.length} lignes (${start} à ${end})`, 'INFO'); } catch (error) { logSh(`❌ Erreur population queue: ${error.message}`, 'ERROR'); throw error; } } /** * Sauvegarde la queue */ async saveQueue() { try { const queueData = { queue: this.queue, processedCount: this.processedCount, lastUpdate: new Date().toISOString() }; await fs.writeFile(this.queuePath, JSON.stringify(queueData, null, 2)); } catch (error) { logSh(`❌ Erreur sauvegarde queue: ${error.message}`, 'ERROR'); } } // ======================================== // CONTRÔLES PRINCIPAUX // ======================================== /** * Démarre le traitement batch */ async start() { return tracer.run('BatchProcessor.start', async () => { if (this.isRunning) { throw new Error('Le traitement est déjà en cours'); } logSh('🚀 Démarrage traitement batch', 'INFO'); this.isRunning = true; this.isPaused = false; this.startTime = new Date(); this.processedCount = 0; this.errorCount = 0; // Charger la configuration la plus récente await this.loadConfig(); // Si queue vide ou configuration changée, repopuler if (this.queue.length === 0) { await this.populateQueue(); } // Mettre à jour le status await this.updateStatus(); // Démarrer le traitement asynchrone this.processQueue().catch(error => { logSh(`❌ Erreur traitement queue: ${error.message}`, 'ERROR'); this.handleError(error); }); return this.getStatus(); }); } /** * Arrête le traitement batch */ async stop() { return tracer.run('BatchProcessor.stop', async () => { logSh('🛑 Arrêt traitement batch', 'INFO'); this.isRunning = false; this.isPaused = false; this.currentRow = null; await this.updateStatus(); return this.getStatus(); }); } /** * Met en pause le traitement */ async pause() { return tracer.run('BatchProcessor.pause', async () => { if (!this.isRunning) { throw new Error('Aucun traitement en cours'); } logSh('⏸️ Mise en pause traitement batch', 'INFO'); this.isPaused = true; await this.updateStatus(); return this.getStatus(); }); } /** * Reprend le traitement */ async resume() { return tracer.run('BatchProcessor.resume', async () => { if (!this.isRunning || !this.isPaused) { throw new Error('Aucun traitement en pause'); } logSh('▶️ Reprise traitement batch', 'INFO'); this.isPaused = false; await this.updateStatus(); // Reprendre le traitement this.processQueue().catch(error => { logSh(`❌ Erreur reprise traitement: ${error.message}`, 'ERROR'); this.handleError(error); }); return this.getStatus(); }); } // ======================================== // TRAITEMENT QUEUE // ======================================== /** * Traite la queue élément par élément */ async processQueue() { return tracer.run('BatchProcessor.processQueue', async () => { while (this.isRunning && !this.isPaused) { // Chercher le prochain élément à traiter const nextItem = this.queue.find(item => item.status === 'pending' || (item.status === 'error' && item.attempts < item.maxAttempts)); if (!nextItem) { // Queue terminée logSh('✅ Traitement queue terminé', 'INFO'); await this.complete(); break; } // Traiter l'élément await this.processItem(nextItem); // Pause entre les éléments (pour éviter rate limiting) await this.sleep(1000); } }); } /** * Traite un élément de la queue */ async processItem(item) { return tracer.run('BatchProcessor.processItem', async () => { logSh(`🔄 Traitement ligne ${item.rowNumber} (tentative ${item.attempts + 1}/${item.maxAttempts})`, 'INFO'); this.currentRow = item.rowNumber; item.status = 'processing'; item.startTime = new Date().toISOString(); item.attempts++; await this.updateStatus(); await this.saveQueue(); try { // Traiter la ligne avec le pipeline modulaire const result = await this.processRow(item.rowNumber); // Succès item.status = 'completed'; item.result = result; item.endTime = new Date().toISOString(); item.error = null; this.processedCount++; logSh(`✅ Ligne ${item.rowNumber} traitée avec succès`, 'INFO'); // Callback succès if (this.onProgress) { this.onProgress(item, this.getProgress()); } } catch (error) { // Erreur item.error = { message: error.message, stack: error.stack, timestamp: new Date().toISOString() }; if (item.attempts >= item.maxAttempts) { item.status = 'failed'; this.errorCount++; logSh(`❌ Ligne ${item.rowNumber} échouée définitivement après ${item.attempts} tentatives`, 'ERROR'); } else { item.status = 'error'; logSh(`⚠️ Ligne ${item.rowNumber} échouée, retry possible`, 'WARNING'); } // Callback erreur if (this.onError) { this.onError(item, error); } } this.currentRow = null; await this.updateStatus(); await this.saveQueue(); }); } /** * Traite une ligne spécifique */ async processRow(rowNumber) { return tracer.run('BatchProcessor.processRow', { rowNumber }, async () => { // Configuration pour cette ligne const rowConfig = { rowNumber, source: 'batch_processor', selectiveStack: this.config.selective, adversarialMode: this.config.adversarial, humanSimulationMode: this.config.humanSimulation, patternBreakingMode: this.config.patternBreaking, intensity: this.config.intensity, saveIntermediateSteps: this.config.saveIntermediateSteps }; logSh(`🎯 Configuration ligne ${rowNumber}: ${JSON.stringify(rowConfig)}`, 'DEBUG'); // Exécuter le workflow modulaire const result = await handleModularWorkflow(rowConfig); logSh(`📊 Résultat ligne ${rowNumber}: ${result ? 'SUCCESS' : 'FAILED'}`, 'INFO'); return result; }); } // ======================================== // GESTION ÉTAT // ======================================== /** * Met à jour le status */ async updateStatus() { const status = this.getStatus(); try { await fs.writeFile(this.statusPath, JSON.stringify(status, null, 2)); // Callback update if (this.onStatusUpdate) { this.onStatusUpdate(status); } } catch (error) { logSh(`❌ Erreur mise à jour status: ${error.message}`, 'ERROR'); } } /** * Retourne le status actuel */ getStatus() { const now = new Date(); const completedItems = this.queue.filter(item => item.status === 'completed').length; const failedItems = this.queue.filter(item => item.status === 'failed').length; const totalItems = this.queue.length; const progress = totalItems > 0 ? ((completedItems + failedItems) / totalItems) * 100 : 0; let status = 'idle'; if (this.isRunning && this.isPaused) { status = 'paused'; } else if (this.isRunning) { status = 'running'; } else if (completedItems + failedItems === totalItems && totalItems > 0) { status = 'completed'; } return { status, currentRow: this.currentRow, totalRows: totalItems, completedRows: completedItems, failedRows: failedItems, progress: Math.round(progress), startTime: this.startTime ? this.startTime.toISOString() : null, estimatedEnd: this.estimateCompletionTime(), errors: this.queue.filter(item => item.error).map(item => ({ rowNumber: item.rowNumber, error: item.error, attempts: item.attempts })), lastResult: this.getLastResult(), config: this.config, queue: this.queue }; } /** * Retourne la progression détaillée */ getProgress() { const status = this.getStatus(); const now = new Date(); const elapsed = this.startTime ? now - this.startTime : 0; const avgTimePerRow = status.completedRows > 0 ? elapsed / status.completedRows : 0; const remainingRows = status.totalRows - status.completedRows - status.failedRows; const estimatedRemaining = avgTimePerRow * remainingRows; return { ...status, metrics: { elapsedTime: elapsed, avgTimePerRow: avgTimePerRow, estimatedRemaining: estimatedRemaining, completionPercentage: status.progress, throughput: status.completedRows > 0 && elapsed > 0 ? (status.completedRows / (elapsed / 1000 / 60)) : 0 // rows/minute } }; } /** * Estime l'heure de fin */ estimateCompletionTime() { if (!this.startTime || !this.isRunning || this.isPaused) { return null; } const progress = this.getProgress(); if (progress.metrics.estimatedRemaining > 0) { const endTime = new Date(Date.now() + progress.metrics.estimatedRemaining); return endTime.toISOString(); } return null; } /** * Retourne le dernier résultat */ getLastResult() { const completedItems = this.queue.filter(item => item.status === 'completed'); if (completedItems.length === 0) return null; const lastItem = completedItems[completedItems.length - 1]; return { rowNumber: lastItem.rowNumber, result: lastItem.result, endTime: lastItem.endTime }; } /** * Gère les erreurs critiques */ async handleError(error) { logSh(`💥 Erreur critique BatchProcessor: ${error.message}`, 'ERROR'); this.isRunning = false; this.isPaused = false; await this.updateStatus(); if (this.onError) { this.onError(null, error); } } /** * Termine le traitement */ async complete() { logSh('🏁 Traitement batch terminé', 'INFO'); this.isRunning = false; this.isPaused = false; this.currentRow = null; await this.updateStatus(); if (this.onComplete) { this.onComplete(this.getStatus()); } } // ======================================== // UTILITAIRES // ======================================== /** * Pause l'exécution */ async sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Reset la queue */ async resetQueue() { logSh('🔄 Reset de la queue', 'INFO'); this.queue = []; this.processedCount = 0; this.errorCount = 0; await this.populateQueue(); await this.updateStatus(); } /** * Configure les callbacks */ setCallbacks({ onStatusUpdate, onProgress, onError, onComplete }) { this.onStatusUpdate = onStatusUpdate; this.onProgress = onProgress; this.onError = onError; this.onComplete = onComplete; } } // ============= EXPORTS ============= module.exports = { BatchProcessor }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/modes/AutoProcessor.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: AutoProcessor.js // RESPONSABILITÉ: Mode AUTO - Traitement Batch Google Sheets // FONCTIONNALITÉS: Processing queue, scheduling, monitoring // ======================================== const { logSh } = require('../ErrorReporting'); const { handleModularWorkflow } = require('../Main'); const { readInstructionsData } = require('../BrainConfig'); /** * PROCESSEUR MODE AUTO * Traitement automatique et séquentiel des lignes Google Sheets */ class AutoProcessor { constructor(options = {}) { this.config = { batchSize: options.batchSize || 5, // Lignes par batch delayBetweenItems: options.delayBetweenItems || 2000, // 2s entre chaque ligne delayBetweenBatches: options.delayBetweenBatches || 30000, // 30s entre batches maxRetries: options.maxRetries || 3, startRow: options.startRow || 2, endRow: options.endRow || null, // null = jusqu'à la fin autoMode: options.autoMode || 'standardEnhancement', // Config par défaut monitoringPort: options.monitoringPort || 3001, ...options }; this.processingQueue = []; this.processedItems = []; this.failedItems = []; this.state = { isProcessing: false, isPaused: false, currentItem: null, startTime: null, lastActivity: null, totalProcessed: 0, totalErrors: 0 }; this.stats = { itemsQueued: 0, itemsProcessed: 0, itemsFailed: 0, averageProcessingTime: 0, totalProcessingTime: 0, startTime: Date.now(), lastProcessedAt: null }; this.monitoringServer = null; this.processingInterval = null; this.isRunning = false; } // ======================================== // DÉMARRAGE ET ARRÊT // ======================================== /** * Démarre le processeur AUTO complet */ async start() { if (this.isRunning) { logSh('⚠️ AutoProcessor déjà en cours d\'exécution', 'WARNING'); return; } logSh('🤖 Démarrage AutoProcessor...', 'INFO'); try { // 1. Charger la queue depuis Google Sheets await this.loadProcessingQueue(); // 2. Serveur de monitoring (lecture seule) await this.startMonitoringServer(); // 3. Démarrer le traitement this.startProcessingLoop(); // 4. Monitoring périodique this.startHealthMonitoring(); this.isRunning = true; this.state.startTime = Date.now(); logSh(`✅ AutoProcessor démarré: ${this.stats.itemsQueued} éléments en queue`, 'INFO'); logSh(`📊 Monitoring sur http://localhost:${this.config.monitoringPort}`, 'INFO'); } catch (error) { logSh(`❌ Erreur démarrage AutoProcessor: ${error.message}`, 'ERROR'); await this.stop(); throw error; } } /** * Arrête le processeur AUTO */ async stop() { if (!this.isRunning) return; logSh('🛑 Arrêt AutoProcessor...', 'INFO'); try { // Marquer comme en arrêt this.isRunning = false; // Arrêter la boucle de traitement if (this.processingInterval) { clearInterval(this.processingInterval); this.processingInterval = null; } // Attendre la fin du traitement en cours if (this.state.isProcessing) { logSh('⏳ Attente fin traitement en cours...', 'INFO'); await this.waitForCurrentProcessing(); } // Arrêter monitoring if (this.healthInterval) { clearInterval(this.healthInterval); this.healthInterval = null; } // Arrêter serveur monitoring if (this.monitoringServer) { await new Promise((resolve) => { this.monitoringServer.close(() => resolve()); }); this.monitoringServer = null; } // Sauvegarder progression await this.saveProgress(); logSh('✅ AutoProcessor arrêté', 'INFO'); } catch (error) { logSh(`⚠️ Erreur arrêt AutoProcessor: ${error.message}`, 'WARNING'); } } // ======================================== // CHARGEMENT QUEUE // ======================================== /** * Charge la queue de traitement depuis Google Sheets */ async loadProcessingQueue() { logSh('📋 Chargement queue depuis Google Sheets...', 'INFO'); try { // Restaurer progression si disponible - TEMPORAIREMENT DÉSACTIVÉ // const savedProgress = await this.loadProgress(); // const processedRows = new Set(savedProgress?.processedRows || []); const processedRows = new Set(); // Ignore la progression sauvegardée // Scanner les lignes disponibles let currentRow = this.config.startRow; let consecutiveEmptyRows = 0; const maxEmptyRows = 5; // Arrêt après 5 lignes vides consécutives while (currentRow <= (this.config.endRow || 10)) { // 🔧 LIMITE MAX POUR ÉVITER BOUCLE INFINIE // Vérifier limite max si définie if (this.config.endRow && currentRow > this.config.endRow) { break; } try { // Tenter de lire la ligne const csvData = await readInstructionsData(currentRow); if (!csvData || !csvData.mc0) { // Ligne vide ou invalide consecutiveEmptyRows++; if (consecutiveEmptyRows >= maxEmptyRows) { logSh(`🛑 Arrêt scan après ${maxEmptyRows} lignes vides consécutives à partir de la ligne ${currentRow - maxEmptyRows + 1}`, 'INFO'); break; } } else { // Ligne valide trouvée consecutiveEmptyRows = 0; // Ajouter à la queue si pas déjà traitée if (!processedRows.has(currentRow)) { this.processingQueue.push({ rowNumber: currentRow, data: csvData, attempts: 0, status: 'pending', addedAt: Date.now() }); } else { logSh(`⏭️ Ligne ${currentRow} déjà traitée, ignorée`, 'DEBUG'); } } } catch (error) { // Erreur de lecture = ligne probablement vide consecutiveEmptyRows++; if (consecutiveEmptyRows >= maxEmptyRows) { break; } } currentRow++; } this.stats.itemsQueued = this.processingQueue.length; logSh(`📊 Queue chargée: ${this.stats.itemsQueued} éléments (lignes ${this.config.startRow}-${currentRow - 1})`, 'INFO'); if (this.stats.itemsQueued === 0) { logSh('⚠️ Aucun élément à traiter trouvé', 'WARNING'); } } catch (error) { logSh(`❌ Erreur chargement queue: ${error.message}`, 'ERROR'); throw error; } } // ======================================== // BOUCLE DE TRAITEMENT // ======================================== /** * Démarre la boucle principale de traitement */ startProcessingLoop() { if (this.processingQueue.length === 0) { logSh('⚠️ Queue vide, pas de traitement à démarrer', 'WARNING'); return; } logSh('🔄 Démarrage boucle de traitement...', 'INFO'); // Traitement immédiat du premier batch setTimeout(() => { this.processNextBatch(); }, 1000); // Puis traitement périodique this.processingInterval = setInterval(() => { if (!this.state.isProcessing && !this.state.isPaused) { this.processNextBatch(); } }, this.config.delayBetweenBatches); } /** * Traite le prochain batch d'éléments */ async processNextBatch() { if (this.state.isProcessing || this.state.isPaused || !this.isRunning) { return; } // Vérifier s'il reste des éléments const pendingItems = this.processingQueue.filter(item => item.status === 'pending'); if (pendingItems.length === 0) { logSh('✅ Tous les éléments ont été traités', 'INFO'); await this.completeProcessing(); return; } // Prendre le prochain batch const batchItems = pendingItems.slice(0, this.config.batchSize); logSh(`🚀 Traitement batch: ${batchItems.length} éléments`, 'INFO'); this.state.isProcessing = true; this.state.lastActivity = Date.now(); try { // Traiter chaque élément du batch séquentiellement for (const item of batchItems) { if (!this.isRunning) break; // Arrêt demandé await this.processItem(item); // Délai entre éléments if (this.config.delayBetweenItems > 0) { await this.sleep(this.config.delayBetweenItems); } } logSh(`✅ Batch terminé: ${batchItems.length} éléments traités`, 'INFO'); } catch (error) { logSh(`❌ Erreur traitement batch: ${error.message}`, 'ERROR'); } finally { this.state.isProcessing = false; this.state.currentItem = null; } } /** * Traite un élément individuel */ async processItem(item) { const startTime = Date.now(); this.state.currentItem = item; logSh(`🎯 Traitement ligne ${item.rowNumber}: ${item.data.mc0}`, 'INFO'); try { item.status = 'processing'; item.attempts++; item.startedAt = startTime; // Configuration de traitement automatique const processingConfig = { rowNumber: item.rowNumber, selectiveStack: this.config.autoMode, adversarialMode: 'light', humanSimulationMode: 'lightSimulation', patternBreakingMode: 'standardPatternBreaking', source: `auto_processor_row_${item.rowNumber}` }; // Exécution du workflow modulaire const result = await handleModularWorkflow(processingConfig); const duration = Date.now() - startTime; // Succès item.status = 'completed'; item.completedAt = Date.now(); item.duration = duration; item.result = { stats: result.stats, success: true }; this.processedItems.push(item); this.stats.itemsProcessed++; this.stats.totalProcessingTime += duration; this.stats.averageProcessingTime = Math.round(this.stats.totalProcessingTime / this.stats.itemsProcessed); this.stats.lastProcessedAt = Date.now(); logSh(`✅ Ligne ${item.rowNumber} terminée (${duration}ms) - ${result.stats.totalModifications || 0} modifications`, 'INFO'); } catch (error) { const duration = Date.now() - startTime; // Échec item.status = 'failed'; item.failedAt = Date.now(); item.duration = duration; item.error = error.message; this.stats.totalErrors++; logSh(`❌ Échec ligne ${item.rowNumber} (tentative ${item.attempts}/${this.config.maxRetries}): ${error.message}`, 'ERROR'); // Retry si possible if (item.attempts < this.config.maxRetries) { logSh(`🔄 Retry programmé pour ligne ${item.rowNumber}`, 'INFO'); item.status = 'pending'; // Remettre en queue } else { logSh(`💀 Ligne ${item.rowNumber} abandonnée après ${item.attempts} tentatives`, 'WARNING'); this.failedItems.push(item); this.stats.itemsFailed++; } } // Sauvegarder progression périodiquement if (this.stats.itemsProcessed % 5 === 0) { await this.saveProgress(); } } // ======================================== // SERVEUR MONITORING // ======================================== /** * Démarre le serveur de monitoring (lecture seule) */ async startMonitoringServer() { const express = require('express'); const app = express(); app.use(express.json()); // Page de status principale app.get('/', (req, res) => { res.send(this.generateStatusPage()); }); // API status JSON app.get('/api/status', (req, res) => { res.json(this.getDetailedStatus()); }); // API stats JSON app.get('/api/stats', (req, res) => { res.json({ success: true, stats: { ...this.stats }, queue: { total: this.processingQueue.length, pending: this.processingQueue.filter(i => i.status === 'pending').length, processing: this.processingQueue.filter(i => i.status === 'processing').length, completed: this.processingQueue.filter(i => i.status === 'completed').length, failed: this.processingQueue.filter(i => i.status === 'failed').length }, timestamp: new Date().toISOString() }); }); // Actions de contrôle (limitées) app.post('/api/pause', (req, res) => { this.pauseProcessing(); res.json({ success: true, message: 'Traitement mis en pause' }); }); app.post('/api/resume', (req, res) => { this.resumeProcessing(); res.json({ success: true, message: 'Traitement repris' }); }); // 404 pour autres routes app.use('*', (req, res) => { res.status(404).json({ success: false, error: 'Route non trouvée', mode: 'AUTO', message: 'Interface de monitoring en lecture seule' }); }); // Démarrage serveur return new Promise((resolve, reject) => { try { this.monitoringServer = app.listen(this.config.monitoringPort, '0.0.0.0', () => { logSh(`📊 Serveur monitoring démarré sur http://localhost:${this.config.monitoringPort}`, 'DEBUG'); resolve(); }); this.monitoringServer.on('error', (error) => { reject(error); }); } catch (error) { reject(error); } }); } /** * Génère la page de status HTML */ generateStatusPage() { const uptime = Math.floor((Date.now() - this.stats.startTime) / 1000); const progress = this.stats.itemsQueued > 0 ? Math.round((this.stats.itemsProcessed / this.stats.itemsQueued) * 100) : 0; const pendingCount = this.processingQueue.filter(i => i.status === 'pending').length; const completedCount = this.processingQueue.filter(i => i.status === 'completed').length; const failedCount = this.processingQueue.filter(i => i.status === 'failed').length; return ` SEO Generator - Mode AUTO

🤖 SEO Generator Server

MODE AUTO

Traitement Automatique Google Sheets

🤖 Mode AUTO Actif
Traitement batch des Google Sheets • Interface monitoring lecture seule
Progression: ${progress}% (${completedCount}/${this.stats.itemsQueued})
${uptime}s
Uptime
${pendingCount}
En Attente
${this.state.isProcessing ? '1' : '0'}
En Traitement
${completedCount}
Terminés
${failedCount}
Échecs
${this.stats.averageProcessingTime}ms
Temps Moyen
${this.state.currentItem ? `
🎯 Traitement en cours:
Ligne ${this.state.currentItem.rowNumber}: ${this.state.currentItem.data.mc0}
Tentative ${this.state.currentItem.attempts}/${this.config.maxRetries}
` : ''}

🎛️ Contrôles

${this.state.isPaused ? '' : '' } 📊 Stats JSON

📋 Configuration

  • Batch Size: ${this.config.batchSize} éléments
  • Délai entre éléments: ${this.config.delayBetweenItems}ms
  • Délai entre batches: ${this.config.delayBetweenBatches}ms
  • Max Retries: ${this.config.maxRetries}
  • Mode Auto: ${this.config.autoMode}
  • Lignes: ${this.config.startRow} - ${this.config.endRow || '∞'}
`; } // ======================================== // CONTRÔLES ET ÉTAT // ======================================== /** * Met en pause le traitement */ pauseProcessing() { this.state.isPaused = true; logSh('⏸️ Traitement mis en pause', 'INFO'); } /** * Reprend le traitement */ resumeProcessing() { this.state.isPaused = false; logSh('▶️ Traitement repris', 'INFO'); } /** * Vérifie si le processeur est en cours de traitement */ isProcessing() { return this.state.isProcessing; } /** * Attendre la fin du traitement actuel */ async waitForCurrentProcessing(timeout = 30000) { const startWait = Date.now(); while (this.state.isProcessing && (Date.now() - startWait) < timeout) { await this.sleep(1000); } if (this.state.isProcessing) { logSh('⚠️ Timeout attente fin traitement', 'WARNING'); } } /** * Termine le traitement (tous éléments traités) */ async completeProcessing() { logSh('🎉 Traitement terminé - Tous les éléments ont été traités', 'INFO'); const summary = { totalItems: this.stats.itemsQueued, processed: this.stats.itemsProcessed, failed: this.stats.itemsFailed, totalTime: Date.now() - this.stats.startTime, averageTime: this.stats.averageProcessingTime }; logSh(`📊 Résumé final: ${summary.processed}/${summary.totalItems} traités, ${summary.failed} échecs`, 'INFO'); logSh(`⏱️ Temps total: ${Math.floor(summary.totalTime / 1000)}s, moyenne: ${summary.averageTime}ms/item`, 'INFO'); // Arrêter la boucle if (this.processingInterval) { clearInterval(this.processingInterval); this.processingInterval = null; } // Sauvegarder résultats finaux await this.saveProgress(); this.state.isProcessing = false; } // ======================================== // MONITORING ET HEALTH // ======================================== /** * Démarre le monitoring de santé */ startHealthMonitoring() { const HEALTH_INTERVAL = 60000; // 1 minute this.healthInterval = setInterval(() => { this.performHealthCheck(); }, HEALTH_INTERVAL); logSh('💓 Health monitoring AutoProcessor démarré', 'DEBUG'); } /** * Health check périodique */ performHealthCheck() { const memUsage = process.memoryUsage(); const uptime = Date.now() - this.stats.startTime; const queueStatus = { pending: this.processingQueue.filter(i => i.status === 'pending').length, completed: this.processingQueue.filter(i => i.status === 'completed').length, failed: this.processingQueue.filter(i => i.status === 'failed').length }; logSh(`💓 AutoProcessor Health - Queue: ${queueStatus.pending}P/${queueStatus.completed}C/${queueStatus.failed}F | RAM: ${Math.round(memUsage.rss / 1024 / 1024)}MB`, 'TRACE'); // Alertes if (memUsage.rss > 2 * 1024 * 1024 * 1024) { // > 2GB logSh('⚠️ Utilisation mémoire très élevée', 'WARNING'); } if (this.stats.itemsFailed > this.stats.itemsProcessed * 0.5) { logSh('⚠️ Taux d\'échec élevé détecté', 'WARNING'); } } /** * Retourne le status détaillé */ getDetailedStatus() { return { success: true, mode: 'AUTO', isRunning: this.isRunning, state: { ...this.state }, stats: { ...this.stats, uptime: Date.now() - this.stats.startTime }, queue: { total: this.processingQueue.length, pending: this.processingQueue.filter(i => i.status === 'pending').length, processing: this.processingQueue.filter(i => i.status === 'processing').length, completed: this.processingQueue.filter(i => i.status === 'completed').length, failed: this.processingQueue.filter(i => i.status === 'failed').length }, config: { ...this.config }, currentItem: this.state.currentItem ? { rowNumber: this.state.currentItem.rowNumber, data: this.state.currentItem.data.mc0, attempts: this.state.currentItem.attempts } : null, urls: { monitoring: `http://localhost:${this.config.monitoringPort}`, api: `http://localhost:${this.config.monitoringPort}/api/stats` }, timestamp: new Date().toISOString() }; } // ======================================== // PERSISTANCE ET RÉCUPÉRATION // ======================================== /** * Sauvegarde la progression */ async saveProgress() { try { const fs = require('fs').promises; const path = require('path'); const progressFile = path.join(__dirname, '../../auto-processor-progress.json'); const progress = { processedRows: this.processedItems.map(item => item.rowNumber), failedRows: this.failedItems.map(item => ({ rowNumber: item.rowNumber, error: item.error, attempts: item.attempts })), stats: { ...this.stats }, lastSaved: Date.now(), timestamp: new Date().toISOString() }; await fs.writeFile(progressFile, JSON.stringify(progress, null, 2)); } catch (error) { logSh(`⚠️ Erreur sauvegarde progression: ${error.message}`, 'WARNING'); } } /** * Charge la progression sauvegardée */ async loadProgress() { try { const fs = require('fs').promises; const path = require('path'); const progressFile = path.join(__dirname, '../../auto-processor-progress.json'); try { const data = await fs.readFile(progressFile, 'utf8'); const progress = JSON.parse(data); logSh(`📂 Progression restaurée: ${progress.processedRows?.length || 0} éléments déjà traités`, 'INFO'); return progress; } catch (readError) { if (readError.code !== 'ENOENT') { logSh(`⚠️ Erreur lecture progression: ${readError.message}`, 'WARNING'); } return null; } } catch (error) { logSh(`⚠️ Erreur chargement progression: ${error.message}`, 'WARNING'); return null; } } // ======================================== // UTILITAIRES // ======================================== /** * Pause asynchrone */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } // ============= EXPORTS ============= module.exports = { AutoProcessor }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/modes/AutoProcessor.refactored.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // AUTO PROCESSOR - REFACTORISÉ // Responsabilité: Mode AUTO - Traitement Batch Google Sheets automatique // ======================================== const { QueueProcessor } = require('../shared/QueueProcessor'); const { logSh } = require('../ErrorReporting'); const path = require('path'); /** * AUTO PROCESSOR * Spécialisé pour traitement automatique avec monitoring intégré */ class AutoProcessor extends QueueProcessor { constructor(options = {}) { super({ name: 'AutoProcessor', config: { batchSize: options.batchSize || 5, delayBetweenItems: options.delayBetweenItems || 2000, delayBetweenBatches: options.delayBetweenBatches || 30000, maxRetries: options.maxRetries || 3, startRow: options.startRow || 2, endRow: options.endRow || null, selective: 'standardEnhancement', // Config fixe pour AUTO adversarial: 'light', humanSimulation: 'lightSimulation', patternBreaking: 'standardPatternBreaking', intensity: 1.0, monitoringPort: options.monitoringPort || 3001, ...options } }); this.monitoringServer = null; this.processingInterval = null; this.healthInterval = null; } // ======================================== // DÉMARRAGE ET ARRÊT SPÉCIALISÉS // ======================================== /** * Démarrage AutoProcessor complet avec monitoring */ async start() { if (this.isRunning) { logSh('⚠️ AutoProcessor déjà en cours d\'exécution', 'WARNING'); return; } logSh('🤖 Démarrage AutoProcessor...', 'INFO'); try { // 1. Charger la queue depuis Google Sheets await this.populateQueueFromSheets(); // 2. Serveur de monitoring await this.startMonitoringServer(); // 3. Démarrer le traitement avec batches this.startBatchProcessing(); // 4. Monitoring périodique this.startHealthMonitoring(); this.isRunning = true; this.startTime = new Date(); logSh(`✅ AutoProcessor démarré: ${this.stats.itemsQueued} éléments en queue`, 'INFO'); logSh(`📊 Monitoring sur http://localhost:${this.config.monitoringPort}`, 'INFO'); } catch (error) { logSh(`❌ Erreur démarrage AutoProcessor: ${error.message}`, 'ERROR'); await this.stop(); throw error; } } /** * Arrêt AutoProcessor complet */ async stop() { if (!this.isRunning) return; logSh('🛑 Arrêt AutoProcessor...', 'INFO'); try { this.isRunning = false; // Arrêter la boucle de traitement if (this.processingInterval) { clearInterval(this.processingInterval); this.processingInterval = null; } // Attendre la fin du traitement en cours if (this.currentRow) { logSh('⏳ Attente fin traitement en cours...', 'INFO'); await this.waitForCurrentProcessing(); } // Arrêter monitoring if (this.healthInterval) { clearInterval(this.healthInterval); this.healthInterval = null; } // Arrêter serveur monitoring if (this.monitoringServer) { await new Promise((resolve) => { this.monitoringServer.close(() => resolve()); }); this.monitoringServer = null; } // Sauvegarder progression await this.saveProgress(); logSh('✅ AutoProcessor arrêté', 'INFO'); } catch (error) { logSh(`⚠️ Erreur arrêt AutoProcessor: ${error.message}`, 'WARNING'); } } // ======================================== // TRAITEMENT BATCH SPÉCIALISÉ // ======================================== /** * Démarre le traitement par batches */ startBatchProcessing() { if (this.queue.length === 0) { logSh('⚠️ Queue vide, pas de traitement à démarrer', 'WARNING'); return; } logSh('🔄 Démarrage traitement par batches...', 'INFO'); // Traitement immédiat du premier batch setTimeout(() => { this.processNextBatch(); }, 1000); // Puis traitement périodique this.processingInterval = setInterval(() => { if (!this.isPaused) { this.processNextBatch(); } }, this.config.delayBetweenBatches); } /** * Traite le prochain batch */ async processNextBatch() { if (this.isPaused || !this.isRunning || this.currentRow) { return; } const pendingItems = this.queue.filter(item => item.status === 'pending'); if (pendingItems.length === 0) { logSh('✅ Tous les éléments ont été traités', 'INFO'); await this.complete(); return; } const batchItems = pendingItems.slice(0, this.config.batchSize); logSh(`🚀 Traitement batch: ${batchItems.length} éléments`, 'INFO'); try { for (const item of batchItems) { if (!this.isRunning) break; await this.processItem(item); if (this.config.delayBetweenItems > 0) { await this.sleep(this.config.delayBetweenItems); } } logSh(`✅ Batch terminé: ${batchItems.length} éléments traités`, 'INFO'); } catch (error) { logSh(`❌ Erreur traitement batch: ${error.message}`, 'ERROR'); } } /** * Configuration spécifique AutoProcessor */ buildRowConfig(rowNumber, data = null) { return { rowNumber, selectiveStack: this.config.selective, adversarialMode: this.config.adversarial, humanSimulationMode: this.config.humanSimulation, patternBreakingMode: this.config.patternBreaking, source: `auto_processor_row_${rowNumber}` }; } // ======================================== // SERVEUR MONITORING // ======================================== /** * Démarre le serveur de monitoring */ async startMonitoringServer() { const express = require('express'); const app = express(); app.use(express.json()); // Page de status principale app.get('/', (req, res) => { res.send(this.generateStatusPage()); }); // API status JSON app.get('/api/status', (req, res) => { res.json(this.getDetailedStatus()); }); // API stats JSON app.get('/api/stats', (req, res) => { res.json({ success: true, stats: { ...this.stats }, queue: { total: this.queue.length, pending: this.queue.filter(i => i.status === 'pending').length, processing: this.queue.filter(i => i.status === 'processing').length, completed: this.queue.filter(i => i.status === 'completed').length, failed: this.queue.filter(i => i.status === 'failed').length }, timestamp: new Date().toISOString() }); }); // Actions de contrôle app.post('/api/pause', (req, res) => { this.pauseProcessing(); res.json({ success: true, message: 'Traitement mis en pause' }); }); app.post('/api/resume', (req, res) => { this.resumeProcessing(); res.json({ success: true, message: 'Traitement repris' }); }); // 404 pour autres routes app.use('*', (req, res) => { res.status(404).json({ success: false, error: 'Route non trouvée', mode: 'AUTO', message: 'Interface de monitoring en lecture seule' }); }); // Démarrage serveur return new Promise((resolve, reject) => { try { this.monitoringServer = app.listen(this.config.monitoringPort, '0.0.0.0', () => { logSh(`📊 Serveur monitoring démarré sur http://localhost:${this.config.monitoringPort}`, 'DEBUG'); resolve(); }); this.monitoringServer.on('error', (error) => { reject(error); }); } catch (error) { reject(error); } }); } /** * Génère la page de status HTML */ generateStatusPage() { const uptime = Math.floor((Date.now() - this.stats.startTime) / 1000); const progress = this.stats.itemsQueued > 0 ? Math.round((this.stats.itemsProcessed / this.stats.itemsQueued) * 100) : 0; const pendingCount = this.queue.filter(i => i.status === 'pending').length; const completedCount = this.queue.filter(i => i.status === 'completed').length; const failedCount = this.queue.filter(i => i.status === 'failed').length; return ` SEO Generator - Mode AUTO

🤖 SEO Generator Server

MODE AUTO - REFACTORISÉ

Traitement Automatique Google Sheets

🤖 Mode AUTO Actif
Traitement batch des Google Sheets • Interface monitoring lecture seule
Progression: ${progress}% (${completedCount}/${this.stats.itemsQueued})
${uptime}s
Uptime
${pendingCount}
En Attente
${this.currentRow ? '1' : '0'}
En Traitement
${completedCount}
Terminés
${failedCount}
Échecs
${this.stats.averageProcessingTime}ms
Temps Moyen
${this.currentRow ? `
🎯 Traitement en cours:
Ligne ${this.currentRow}
` : ''}

🎛️ Contrôles

${this.isPaused ? '' : '' } 📊 Stats JSON

📋 Configuration AUTO

  • Batch Size: ${this.config.batchSize} éléments
  • Délai entre éléments: ${this.config.delayBetweenItems}ms
  • Délai entre batches: ${this.config.delayBetweenBatches}ms
  • Max Retries: ${this.config.maxRetries}
  • Mode Selective: ${this.config.selective}
  • Mode Adversarial: ${this.config.adversarial}
  • Lignes: ${this.config.startRow} - ${this.config.endRow || '∞'}
`; } // ======================================== // CONTRÔLES SPÉCIFIQUES // ======================================== /** * Met en pause le traitement */ pauseProcessing() { this.isPaused = true; logSh('⏸️ Traitement AutoProcessor mis en pause', 'INFO'); } /** * Reprend le traitement */ resumeProcessing() { this.isPaused = false; logSh('▶️ Traitement AutoProcessor repris', 'INFO'); } /** * Attendre la fin du traitement actuel */ async waitForCurrentProcessing(timeout = 30000) { const startWait = Date.now(); while (this.currentRow && (Date.now() - startWait) < timeout) { await this.sleep(1000); } if (this.currentRow) { logSh('⚠️ Timeout attente fin traitement', 'WARNING'); } } // ======================================== // MONITORING ET HEALTH // ======================================== /** * Démarre le monitoring de santé */ startHealthMonitoring() { const HEALTH_INTERVAL = 60000; // 1 minute this.healthInterval = setInterval(() => { this.performHealthCheck(); }, HEALTH_INTERVAL); logSh('💓 Health monitoring AutoProcessor démarré', 'DEBUG'); } /** * Health check périodique */ performHealthCheck() { const memUsage = process.memoryUsage(); const queueStatus = { pending: this.queue.filter(i => i.status === 'pending').length, completed: this.queue.filter(i => i.status === 'completed').length, failed: this.queue.filter(i => i.status === 'failed').length }; logSh(`💓 AutoProcessor Health - Queue: ${queueStatus.pending}P/${queueStatus.completed}C/${queueStatus.failed}F | RAM: ${Math.round(memUsage.rss / 1024 / 1024)}MB`, 'TRACE'); // Alertes if (memUsage.rss > 2 * 1024 * 1024 * 1024) { // > 2GB logSh('⚠️ Utilisation mémoire très élevée', 'WARNING'); } if (this.stats.itemsFailed > this.stats.itemsProcessed * 0.5) { logSh('⚠️ Taux d\'échec élevé détecté', 'WARNING'); } } /** * Retourne le status détaillé */ getDetailedStatus() { const baseStatus = this.getStatus(); return { success: true, mode: 'AUTO', isRunning: this.isRunning, state: { isRunning: this.isRunning, isPaused: this.isPaused, currentRow: this.currentRow, startTime: this.startTime, lastActivity: Date.now() }, stats: { ...this.stats, uptime: Date.now() - this.stats.startTime }, queue: { total: this.queue.length, pending: this.queue.filter(i => i.status === 'pending').length, processing: this.queue.filter(i => i.status === 'processing').length, completed: this.queue.filter(i => i.status === 'completed').length, failed: this.queue.filter(i => i.status === 'failed').length }, config: { ...this.config }, currentItem: this.currentRow ? { rowNumber: this.currentRow } : null, urls: { monitoring: `http://localhost:${this.config.monitoringPort}`, api: `http://localhost:${this.config.monitoringPort}/api/stats` }, timestamp: new Date().toISOString() }; } // ======================================== // PERSISTANCE // ======================================== /** * Sauvegarde la progression */ async saveProgress() { try { const fs = require('fs').promises; const progressFile = path.join(__dirname, '../../auto-processor-progress.json'); const progress = { processedRows: this.processedItems.map(item => item.rowNumber), failedRows: this.failedItems.map(item => ({ rowNumber: item.rowNumber, error: item.error, attempts: item.attempts })), stats: { ...this.stats }, lastSaved: Date.now(), timestamp: new Date().toISOString() }; await fs.writeFile(progressFile, JSON.stringify(progress, null, 2)); } catch (error) { logSh(`⚠️ Erreur sauvegarde progression: ${error.message}`, 'WARNING'); } } } // ============= EXPORTS ============= module.exports = { AutoProcessor }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/pipeline/PipelineTemplates.js │ └────────────────────────────────────────────────────────────────────┘ */ /** * PipelineTemplates.js * * Templates prédéfinis pour pipelines modulaires. * Fournit des configurations ready-to-use pour différents cas d'usage. */ /** * Templates de pipelines */ const TEMPLATES = { /** * Light & Fast - Pipeline minimal pour génération rapide */ 'light-fast': { name: 'Light & Fast', description: 'Pipeline rapide pour contenu basique, idéal pour tests et prototypes', pipeline: [ { step: 1, module: 'generation', mode: 'simple', intensity: 1.0 }, { step: 2, module: 'selective', mode: 'lightEnhancement', intensity: 0.7 } ], metadata: { author: 'system', created: '2025-10-08', version: '1.0', tags: ['fast', 'light', 'basic'], estimatedDuration: '35s' } }, /** * Standard SEO - Pipeline équilibré pour usage quotidien */ 'standard-seo': { name: 'Standard SEO', description: 'Pipeline équilibré avec protection anti-détection standard', pipeline: [ { step: 1, module: 'generation', mode: 'simple', intensity: 1.0 }, { step: 2, module: 'selective', mode: 'standardEnhancement', intensity: 1.0 }, { step: 3, module: 'adversarial', mode: 'light', intensity: 0.8, parameters: { detector: 'general', method: 'enhancement' } }, { step: 4, module: 'human', mode: 'lightSimulation', intensity: 0.6 } ], metadata: { author: 'system', created: '2025-10-08', version: '1.0', tags: ['standard', 'seo', 'balanced'], estimatedDuration: '75s' } }, /** * Premium SEO - Pipeline complet pour contenu premium */ 'premium-seo': { name: 'Premium SEO', description: 'Pipeline complet avec anti-détection avancée et qualité maximale', pipeline: [ { step: 1, module: 'generation', mode: 'simple', intensity: 1.0 }, { step: 2, module: 'selective', mode: 'fullEnhancement', intensity: 1.0, saveCheckpoint: true }, { step: 3, module: 'adversarial', mode: 'standard', intensity: 1.0, parameters: { detector: 'general', method: 'regeneration' } }, { step: 4, module: 'human', mode: 'standardSimulation', intensity: 0.8, parameters: { fatigueLevel: 0.5, errorRate: 0.3 } }, { step: 5, module: 'pattern', mode: 'standardPatternBreaking', intensity: 0.9 }, { step: 6, module: 'adversarial', mode: 'light', intensity: 0.7, parameters: { detector: 'general', method: 'enhancement' } } ], metadata: { author: 'system', created: '2025-10-08', version: '1.0', tags: ['premium', 'complete', 'quality'], estimatedDuration: '130s' } }, /** * Heavy Guard - Protection maximale anti-détection */ 'heavy-guard': { name: 'Heavy Guard', description: 'Protection maximale avec multi-passes adversarial et human simulation', pipeline: [ { step: 1, module: 'generation', mode: 'simple', intensity: 1.0 }, { step: 2, module: 'selective', mode: 'fullEnhancement', intensity: 1.0 }, { step: 3, module: 'adversarial', mode: 'heavy', intensity: 1.2, parameters: { detector: 'gptZero', method: 'regeneration' }, saveCheckpoint: true }, { step: 4, module: 'human', mode: 'heavySimulation', intensity: 1.0, parameters: { fatigueLevel: 0.7, errorRate: 0.4 } }, { step: 5, module: 'pattern', mode: 'heavyPatternBreaking', intensity: 1.0 }, { step: 6, module: 'adversarial', mode: 'adaptive', intensity: 1.5, parameters: { detector: 'originality', method: 'hybrid' } }, { step: 7, module: 'human', mode: 'personalityFocus', intensity: 1.3 }, { step: 8, module: 'pattern', mode: 'syntaxFocus', intensity: 1.1 } ], metadata: { author: 'system', created: '2025-10-08', version: '1.0', tags: ['heavy', 'protection', 'anti-detection'], estimatedDuration: '180s' } }, /** * Personality Focus - Mise en avant de la personnalité */ 'personality-focus': { name: 'Personality Focus', description: 'Pipeline optimisé pour un style personnel marqué', pipeline: [ { step: 1, module: 'generation', mode: 'simple', intensity: 1.0 }, { step: 2, module: 'selective', mode: 'personalityFocus', intensity: 1.2 }, { step: 3, module: 'human', mode: 'personalityFocus', intensity: 1.5 }, { step: 4, module: 'adversarial', mode: 'light', intensity: 0.6, parameters: { detector: 'general', method: 'enhancement' } } ], metadata: { author: 'system', created: '2025-10-08', version: '1.0', tags: ['personality', 'style', 'unique'], estimatedDuration: '70s' } }, /** * Fluidity Master - Transitions et fluidité maximale */ 'fluidity-master': { name: 'Fluidity Master', description: 'Pipeline axé sur transitions fluides et connecteurs naturels', pipeline: [ { step: 1, module: 'generation', mode: 'simple', intensity: 1.0 }, { step: 2, module: 'selective', mode: 'fluidityFocus', intensity: 1.3 }, { step: 3, module: 'pattern', mode: 'connectorsFocus', intensity: 1.2 }, { step: 4, module: 'human', mode: 'standardSimulation', intensity: 0.7 } ], metadata: { author: 'system', created: '2025-10-08', version: '1.0', tags: ['fluidity', 'transitions', 'natural'], estimatedDuration: '73s' } }, /** * Adaptive Smart - Pipeline intelligent avec modes adaptatifs */ 'adaptive-smart': { name: 'Adaptive Smart', description: 'Pipeline intelligent qui s\'adapte au contenu', pipeline: [ { step: 1, module: 'generation', mode: 'simple', intensity: 1.0 }, { step: 2, module: 'selective', mode: 'adaptive', intensity: 1.0 }, { step: 3, module: 'adversarial', mode: 'adaptive', intensity: 1.0, parameters: { detector: 'general', method: 'hybrid' } }, { step: 4, module: 'human', mode: 'adaptiveSimulation', intensity: 1.0 }, { step: 5, module: 'pattern', mode: 'adaptivePatternBreaking', intensity: 1.0 } ], metadata: { author: 'system', created: '2025-10-08', version: '1.0', tags: ['adaptive', 'smart', 'intelligent'], estimatedDuration: '105s' } }, /** * GPTZero Killer - Spécialisé anti-GPTZero */ 'gptzero-killer': { name: 'GPTZero Killer', description: 'Pipeline optimisé pour contourner GPTZero spécifiquement', pipeline: [ { step: 1, module: 'generation', mode: 'simple', intensity: 1.0 }, { step: 2, module: 'selective', mode: 'fullEnhancement', intensity: 1.0 }, { step: 3, module: 'adversarial', mode: 'heavy', intensity: 1.5, parameters: { detector: 'gptZero', method: 'regeneration' } }, { step: 4, module: 'human', mode: 'heavySimulation', intensity: 1.2 }, { step: 5, module: 'pattern', mode: 'heavyPatternBreaking', intensity: 1.1 }, { step: 6, module: 'adversarial', mode: 'standard', intensity: 1.0, parameters: { detector: 'gptZero', method: 'hybrid' } } ], metadata: { author: 'system', created: '2025-10-08', version: '1.0', tags: ['gptzero', 'anti-detection', 'specialized'], estimatedDuration: '155s' } }, /** * Originality Bypass - Spécialisé anti-Originality.ai */ 'originality-bypass': { name: 'Originality Bypass', description: 'Pipeline optimisé pour contourner Originality.ai', pipeline: [ { step: 1, module: 'generation', mode: 'simple', intensity: 1.0 }, { step: 2, module: 'selective', mode: 'fullEnhancement', intensity: 1.0 }, { step: 3, module: 'adversarial', mode: 'heavy', intensity: 1.4, parameters: { detector: 'originality', method: 'regeneration' } }, { step: 4, module: 'human', mode: 'temporalFocus', intensity: 1.1 }, { step: 5, module: 'pattern', mode: 'syntaxFocus', intensity: 1.2 }, { step: 6, module: 'adversarial', mode: 'adaptive', intensity: 1.3, parameters: { detector: 'originality', method: 'hybrid' } } ], metadata: { author: 'system', created: '2025-10-08', version: '1.0', tags: ['originality', 'anti-detection', 'specialized'], estimatedDuration: '160s' } }, /** * Minimal Test - Pipeline minimal pour tests rapides */ 'minimal-test': { name: 'Minimal Test', description: 'Pipeline minimal pour tests de connectivité et validation', pipeline: [ { step: 1, module: 'generation', mode: 'simple', intensity: 1.0 } ], metadata: { author: 'system', created: '2025-10-08', version: '1.0', tags: ['test', 'minimal', 'debug'], estimatedDuration: '15s' } } }; /** * Catégories de templates */ const CATEGORIES = { basic: ['minimal-test', 'light-fast'], standard: ['standard-seo', 'premium-seo'], advanced: ['heavy-guard', 'adaptive-smart'], specialized: ['gptzero-killer', 'originality-bypass'], focus: ['personality-focus', 'fluidity-master'] }; /** * Obtenir un template par nom */ function getTemplate(name) { return TEMPLATES[name] || null; } /** * Lister tous les templates */ function listTemplates() { return Object.entries(TEMPLATES).map(([key, template]) => ({ id: key, name: template.name, description: template.description, steps: template.pipeline.length, tags: template.metadata.tags, estimatedDuration: template.metadata.estimatedDuration })); } /** * Lister templates par catégorie */ function listTemplatesByCategory(category) { const templateIds = CATEGORIES[category] || []; return templateIds.map(id => ({ id, ...TEMPLATES[id] })); } /** * Obtenir toutes les catégories */ function getCategories() { return Object.entries(CATEGORIES).map(([name, templateIds]) => ({ name, count: templateIds.length, templates: templateIds })); } /** * Rechercher templates par tag */ function searchByTag(tag) { return Object.entries(TEMPLATES) .filter(([_, template]) => template.metadata.tags.includes(tag)) .map(([id, template]) => ({ id, ...template })); } module.exports = { TEMPLATES, CATEGORIES, getTemplate, listTemplates, listTemplatesByCategory, getCategories, searchByTag }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/modes/ManualServer.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: ManualServer.js // RESPONSABILITÉ: Mode MANUAL - Interface Client + API + WebSocket // FONCTIONNALITÉS: Dashboard, tests modulaires, API complète // ======================================== const express = require('express'); const cors = require('cors'); const path = require('path'); const WebSocket = require('ws'); const { logSh } = require('../ErrorReporting'); const { handleModularWorkflow, benchmarkStacks } = require('../Main'); const { APIController } = require('../APIController'); const { BatchController } = require('../batch/BatchController'); /** * SERVEUR MODE MANUAL * Interface client complète avec API, WebSocket et dashboard */ class ManualServer { constructor(options = {}) { this.config = { port: options.port || process.env.MANUAL_PORT || 3000, wsPort: options.wsPort || process.env.WS_PORT || 8081, host: options.host || '0.0.0.0', ...options }; this.app = null; this.server = null; this.wsServer = null; this.activeClients = new Set(); this.stats = { sessions: 0, requests: 0, testsExecuted: 0, startTime: Date.now(), lastActivity: null }; this.isRunning = false; this.apiController = new APIController(); this.batchController = new BatchController(); } // ======================================== // DÉMARRAGE ET ARRÊT // ======================================== /** * Démarre le serveur MANUAL complet */ async start() { if (this.isRunning) { logSh('⚠️ ManualServer déjà en cours d\'exécution', 'WARNING'); return; } logSh('🎯 Démarrage ManualServer...', 'INFO'); try { // 1. Configuration Express await this.setupExpressApp(); // 2. Routes API this.setupAPIRoutes(); // 3. Interface Web this.setupWebInterface(); // 4. WebSocket pour logs temps réel await this.setupWebSocketServer(); // 5. Démarrage serveur HTTP await this.startHTTPServer(); // 6. Monitoring this.startMonitoring(); this.isRunning = true; this.stats.startTime = Date.now(); logSh(`✅ ManualServer démarré sur http://localhost:${this.config.port}`, 'INFO'); logSh(`📡 WebSocket logs sur ws://localhost:${this.config.wsPort}`, 'INFO'); } catch (error) { logSh(`❌ Erreur démarrage ManualServer: ${error.message}`, 'ERROR'); await this.stop(); throw error; } } /** * Arrête le serveur MANUAL */ async stop() { if (!this.isRunning) return; logSh('🛑 Arrêt ManualServer...', 'INFO'); try { // Déconnecter tous les clients WebSocket this.disconnectAllClients(); // Arrêter WebSocket server if (this.wsServer) { this.wsServer.close(); this.wsServer = null; } // Arrêter serveur HTTP if (this.server) { await new Promise((resolve) => { this.server.close(() => resolve()); }); this.server = null; } this.isRunning = false; logSh('✅ ManualServer arrêté', 'INFO'); } catch (error) { logSh(`⚠️ Erreur arrêt ManualServer: ${error.message}`, 'WARNING'); } } // ======================================== // CONFIGURATION EXPRESS // ======================================== /** * Configure l'application Express */ async setupExpressApp() { this.app = express(); // Middleware de base this.app.use(express.json({ limit: '10mb' })); this.app.use(express.urlencoded({ extended: true })); this.app.use(cors()); // Middleware de logs des requêtes this.app.use((req, res, next) => { this.stats.requests++; this.stats.lastActivity = Date.now(); logSh(`📥 ${req.method} ${req.path} - ${req.ip}`, 'TRACE'); next(); }); // Fichiers statiques this.app.use(express.static(path.join(__dirname, '../../public'))); // Route spécifique pour l'interface step-by-step this.app.get('/step-by-step', (req, res) => { res.sendFile(path.join(__dirname, '../../public/step-by-step.html')); }); logSh('⚙️ Express configuré', 'DEBUG'); } /** * Configure les routes API */ setupAPIRoutes() { // Route de status this.app.get('/api/status', (req, res) => { res.json({ success: true, mode: 'MANUAL', status: 'running', uptime: Date.now() - this.stats.startTime, stats: { ...this.stats }, clients: this.activeClients.size, timestamp: new Date().toISOString() }); }); // Test modulaire individuel this.app.post('/api/test-modulaire', async (req, res) => { await this.handleTestModulaire(req, res); }); // 🆕 Workflow modulaire avec sauvegarde par étapes this.app.post('/api/workflow-modulaire', async (req, res) => { await this.handleWorkflowModulaire(req, res); }); // Benchmark modulaire complet this.app.post('/api/benchmark-modulaire', async (req, res) => { await this.handleBenchmarkModulaire(req, res); }); // Configuration modulaire disponible this.app.get('/api/modulaire-config', (req, res) => { this.handleModulaireConfig(req, res); }); // Stats détaillées this.app.get('/api/stats', (req, res) => { res.json({ success: true, stats: { ...this.stats, uptime: Date.now() - this.stats.startTime, activeClients: this.activeClients.size, memory: process.memoryUsage(), timestamp: new Date().toISOString() } }); }); // Lancer le log viewer avec WebSocket this.app.post('/api/start-log-viewer', (req, res) => { this.handleStartLogViewer(req, res); }); // ======================================== // APIs STEP-BY-STEP // ======================================== // Initialiser une session step-by-step this.app.post('/api/step-by-step/init', async (req, res) => { await this.handleStepByStepInit(req, res); }); // Exécuter une étape this.app.post('/api/step-by-step/execute', async (req, res) => { await this.handleStepByStepExecute(req, res); }); // Status d'une session this.app.get('/api/step-by-step/status/:sessionId', (req, res) => { this.handleStepByStepStatus(req, res); }); // Reset une session this.app.post('/api/step-by-step/reset', (req, res) => { this.handleStepByStepReset(req, res); }); // Export résultats this.app.get('/api/step-by-step/export/:sessionId', (req, res) => { this.handleStepByStepExport(req, res); }); // Liste des sessions actives this.app.get('/api/step-by-step/sessions', (req, res) => { this.handleStepByStepSessions(req, res); }); // API pour récupérer les personnalités this.app.get('/api/personalities', async (req, res) => { await this.handleGetPersonalities(req, res); }); // 🆕 API simple pour générer un article avec mot-clé this.app.post('/api/generate-simple', async (req, res) => { await this.handleGenerateSimple(req, res); }); // ======================================== // ENDPOINTS GESTION CONFIGURATIONS // ======================================== // Sauvegarder une configuration this.app.post('/api/config/save', async (req, res) => { try { const { name, config } = req.body; if (!name || !config) { return res.status(400).json({ success: false, error: 'Nom et configuration requis' }); } const { ConfigManager } = require('../ConfigManager'); const configManager = new ConfigManager(); const result = await configManager.saveConfig(name, config); res.json({ success: true, message: `Configuration "${name}" sauvegardée`, savedName: result.name }); } catch (error) { logSh(`❌ Erreur save config: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } }); // Lister les configurations this.app.get('/api/config/list', async (req, res) => { try { const { ConfigManager } = require('../ConfigManager'); const configManager = new ConfigManager(); const configs = await configManager.listConfigs(); res.json({ success: true, configs, count: configs.length }); } catch (error) { logSh(`❌ Erreur list configs: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } }); // Charger une configuration this.app.get('/api/config/:name', async (req, res) => { try { const { name } = req.params; const { ConfigManager } = require('../ConfigManager'); const configManager = new ConfigManager(); const configData = await configManager.loadConfig(name); res.json({ success: true, config: configData }); } catch (error) { logSh(`❌ Erreur load config: ${error.message}`, 'ERROR'); res.status(404).json({ success: false, error: error.message }); } }); // Supprimer une configuration this.app.delete('/api/config/:name', async (req, res) => { try { const { name } = req.params; const { ConfigManager } = require('../ConfigManager'); const configManager = new ConfigManager(); await configManager.deleteConfig(name); res.json({ success: true, message: `Configuration "${name}" supprimée` }); } catch (error) { logSh(`❌ Erreur delete config: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } }); // ======================================== // ENDPOINTS PIPELINE MANAGEMENT // ======================================== // Sauvegarder un pipeline this.app.post('/api/pipeline/save', async (req, res) => { try { const { pipelineDefinition } = req.body; if (!pipelineDefinition) { return res.status(400).json({ success: false, error: 'pipelineDefinition requis' }); } const { ConfigManager } = require('../ConfigManager'); const configManager = new ConfigManager(); const result = await configManager.savePipeline(pipelineDefinition); res.json({ success: true, message: `Pipeline "${pipelineDefinition.name}" sauvegardé`, savedName: result.name }); } catch (error) { logSh(`❌ Erreur save pipeline: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } }); // Lister les pipelines this.app.get('/api/pipeline/list', async (req, res) => { try { const { ConfigManager } = require('../ConfigManager'); const configManager = new ConfigManager(); const pipelines = await configManager.listPipelines(); res.json({ success: true, pipelines, count: pipelines.length }); } catch (error) { logSh(`❌ Erreur list pipelines: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } }); // Charger un pipeline this.app.get('/api/pipeline/:name', async (req, res) => { try { const { name } = req.params; const { ConfigManager } = require('../ConfigManager'); const configManager = new ConfigManager(); const pipeline = await configManager.loadPipeline(name); res.json({ success: true, pipeline }); } catch (error) { logSh(`❌ Erreur load pipeline: ${error.message}`, 'ERROR'); res.status(404).json({ success: false, error: error.message }); } }); // Supprimer un pipeline this.app.delete('/api/pipeline/:name', async (req, res) => { try { const { name } = req.params; const { ConfigManager } = require('../ConfigManager'); const configManager = new ConfigManager(); await configManager.deletePipeline(name); res.json({ success: true, message: `Pipeline "${name}" supprimé` }); } catch (error) { logSh(`❌ Erreur delete pipeline: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } }); // Exécuter un pipeline this.app.post('/api/pipeline/execute', async (req, res) => { try { const { pipelineConfig, rowNumber } = req.body; if (!pipelineConfig) { return res.status(400).json({ success: false, error: 'pipelineConfig requis' }); } if (!rowNumber || rowNumber < 2) { return res.status(400).json({ success: false, error: 'rowNumber requis (minimum 2)' }); } logSh(`🚀 Exécution pipeline: ${pipelineConfig.name} (row ${rowNumber})`, 'INFO'); const { handleFullWorkflow } = require('../Main'); const result = await handleFullWorkflow({ pipelineConfig, rowNumber, source: 'pipeline_api' }); res.json({ success: true, result: { finalContent: result.finalContent, executionLog: result.executionLog, stats: result.stats } }); } catch (error) { logSh(`❌ Erreur execute pipeline: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } }); // Obtenir templates prédéfinis this.app.get('/api/pipeline/templates', async (req, res) => { try { const { listTemplates, getCategories } = require('../pipeline/PipelineTemplates'); const templates = listTemplates(); const categories = getCategories(); res.json({ success: true, templates, categories }); } catch (error) { logSh(`❌ Erreur get templates: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } }); // Obtenir template par nom this.app.get('/api/pipeline/templates/:name', async (req, res) => { try { const { name } = req.params; const { getTemplate } = require('../pipeline/PipelineTemplates'); const template = getTemplate(name); if (!template) { return res.status(404).json({ success: false, error: `Template "${name}" non trouvé` }); } res.json({ success: true, template }); } catch (error) { logSh(`❌ Erreur get template: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } }); // Obtenir modules disponibles this.app.get('/api/pipeline/modules', async (req, res) => { try { const { PipelineDefinition } = require('../pipeline/PipelineDefinition'); const modules = PipelineDefinition.listModules(); res.json({ success: true, modules }); } catch (error) { logSh(`❌ Erreur get modules: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } }); // Valider un pipeline this.app.post('/api/pipeline/validate', async (req, res) => { try { const { pipelineDefinition } = req.body; if (!pipelineDefinition) { return res.status(400).json({ success: false, error: 'pipelineDefinition requis' }); } const { PipelineDefinition } = require('../pipeline/PipelineDefinition'); const validation = PipelineDefinition.validate(pipelineDefinition); res.json({ success: validation.valid, valid: validation.valid, errors: validation.errors }); } catch (error) { logSh(`❌ Erreur validate pipeline: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } }); // Estimer durée/coût d'un pipeline this.app.post('/api/pipeline/estimate', async (req, res) => { try { const { pipelineDefinition } = req.body; if (!pipelineDefinition) { return res.status(400).json({ success: false, error: 'pipelineDefinition requis' }); } const { PipelineDefinition } = require('../pipeline/PipelineDefinition'); const summary = PipelineDefinition.getSummary(pipelineDefinition); const duration = PipelineDefinition.estimateDuration(pipelineDefinition); res.json({ success: true, estimate: { totalSteps: summary.totalSteps, summary: summary.summary, estimatedDuration: duration.formatted, estimatedSeconds: duration.seconds } }); } catch (error) { logSh(`❌ Erreur estimate pipeline: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } }); // ======================================== // ENDPOINT PRODUCTION RUN // ======================================== this.app.post('/api/production-run', async (req, res) => { try { const { rowNumber, selectiveStack, adversarialMode, humanSimulationMode, patternBreakingMode, saveIntermediateSteps = true } = req.body; if (!rowNumber) { return res.status(400).json({ success: false, error: 'rowNumber requis' }); } logSh(`🚀 PRODUCTION RUN: Row ${rowNumber}`, 'INFO'); // Appel handleFullWorkflow depuis Main.js const { handleFullWorkflow } = require('../Main'); const result = await handleFullWorkflow({ rowNumber, selectiveStack: selectiveStack || 'standardEnhancement', adversarialMode: adversarialMode || 'light', humanSimulationMode: humanSimulationMode || 'none', patternBreakingMode: patternBreakingMode || 'none', saveIntermediateSteps, source: 'production_web' }); res.json({ success: true, result: { wordCount: result.compiledWordCount, duration: result.totalDuration, llmUsed: result.llmUsed, cost: result.estimatedCost, slug: result.slug, gsheetsLink: `https://docs.google.com/spreadsheets/d/${process.env.GOOGLE_SHEETS_ID}` } }); } catch (error) { logSh(`❌ Erreur production run: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } }); // ======================================== // 🚀 NOUVEAUX ENDPOINTS API RESTful // ======================================== // === GESTION ARTICLES === this.app.get('/api/articles', async (req, res) => { await this.apiController.getArticles(req, res); }); this.app.get('/api/articles/:id', async (req, res) => { await this.apiController.getArticle(req, res); }); this.app.post('/api/articles', async (req, res) => { await this.apiController.createArticle(req, res); }); // ======================================== // 🎯 BATCH PROCESSING API ENDPOINTS // ======================================== // Configuration batch this.app.get('/api/batch/config', async (req, res) => { await this.batchController.getConfig(req, res); }); this.app.post('/api/batch/config', async (req, res) => { await this.batchController.saveConfig(req, res); }); // Contrôle traitement batch this.app.post('/api/batch/start', async (req, res) => { await this.batchController.startBatch(req, res); }); this.app.post('/api/batch/stop', async (req, res) => { await this.batchController.stopBatch(req, res); }); this.app.post('/api/batch/pause', async (req, res) => { await this.batchController.pauseBatch(req, res); }); this.app.post('/api/batch/resume', async (req, res) => { await this.batchController.resumeBatch(req, res); }); // Monitoring batch this.app.get('/api/batch/status', async (req, res) => { await this.batchController.getStatus(req, res); }); this.app.get('/api/batch/progress', async (req, res) => { await this.batchController.getProgress(req, res); }); // Templates Digital Ocean this.app.get('/api/batch/templates', async (req, res) => { await this.batchController.getTemplates(req, res); }); this.app.get('/api/batch/templates/:filename', async (req, res) => { await this.batchController.getTemplate(req, res); }); this.app.delete('/api/batch/cache', async (req, res) => { await this.batchController.clearCache(req, res); }); // === GESTION PROJETS === this.app.get('/api/projects', async (req, res) => { await this.apiController.getProjects(req, res); }); this.app.post('/api/projects', async (req, res) => { await this.apiController.createProject(req, res); }); // === GESTION TEMPLATES === this.app.get('/api/templates', async (req, res) => { await this.apiController.getTemplates(req, res); }); this.app.post('/api/templates', async (req, res) => { await this.apiController.createTemplate(req, res); }); // === CONFIGURATION === this.app.get('/api/config/personalities', async (req, res) => { await this.apiController.getPersonalitiesConfig(req, res); }); // === MONITORING === this.app.get('/api/health', async (req, res) => { await this.apiController.getHealth(req, res); }); this.app.get('/api/metrics', async (req, res) => { await this.apiController.getMetrics(req, res); }); // === PROMPT ENGINE API === this.app.post('/api/generate-prompt', async (req, res) => { await this.apiController.generatePrompt(req, res); }); this.app.get('/api/trends', async (req, res) => { await this.apiController.getTrends(req, res); }); this.app.post('/api/trends/:trendId', async (req, res) => { await this.apiController.setTrend(req, res); }); this.app.get('/api/prompt-engine/status', async (req, res) => { await this.apiController.getPromptEngineStatus(req, res); }); // === WORKFLOW CONFIGURATION API === this.app.get('/api/workflow/sequences', async (req, res) => { await this.apiController.getWorkflowSequences(req, res); }); this.app.post('/api/workflow/sequences', async (req, res) => { await this.apiController.createWorkflowSequence(req, res); }); this.app.post('/api/workflow/execute', async (req, res) => { await this.apiController.executeConfigurableWorkflow(req, res); }); // Gestion d'erreurs API this.app.use('/api/*', (error, req, res, next) => { logSh(`❌ Erreur API ${req.path}: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur serveur interne', message: error.message, timestamp: new Date().toISOString() }); }); logSh('🛠️ Routes API configurées', 'DEBUG'); } // ======================================== // HANDLERS API // ======================================== /** * Gère les tests modulaires individuels */ async handleTestModulaire(req, res) { try { const config = req.body; this.stats.testsExecuted++; logSh(`🧪 Test modulaire: ${config.selectiveStack} + ${config.adversarialMode} + ${config.humanSimulationMode} + ${config.patternBreakingMode}`, 'INFO'); // Validation des paramètres if (!config.rowNumber || config.rowNumber < 2) { return res.status(400).json({ success: false, error: 'Numéro de ligne invalide (minimum 2)' }); } // Exécution du test const result = await handleModularWorkflow({ ...config, source: 'manual_server_api' }); logSh(`✅ Test modulaire terminé: ${result.stats.totalDuration}ms`, 'INFO'); res.json({ success: true, message: 'Test modulaire terminé avec succès', stats: result.stats, config: config, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur test modulaire: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message, config: req.body, timestamp: new Date().toISOString() }); } } /** * Gère les benchmarks modulaires */ async handleBenchmarkModulaire(req, res) { try { const { rowNumber = 2 } = req.body; logSh(`📊 Benchmark modulaire ligne ${rowNumber}...`, 'INFO'); if (rowNumber < 2) { return res.status(400).json({ success: false, error: 'Numéro de ligne invalide (minimum 2)' }); } const benchResults = await benchmarkStacks(rowNumber); const successfulTests = benchResults.filter(r => r.success); const avgDuration = successfulTests.length > 0 ? successfulTests.reduce((sum, r) => sum + r.duration, 0) / successfulTests.length : 0; this.stats.testsExecuted += benchResults.length; logSh(`📊 Benchmark terminé: ${successfulTests.length}/${benchResults.length} tests réussis`, 'INFO'); res.json({ success: true, message: `Benchmark terminé: ${successfulTests.length}/${benchResults.length} tests réussis`, summary: { totalTests: benchResults.length, successfulTests: successfulTests.length, failedTests: benchResults.length - successfulTests.length, averageDuration: Math.round(avgDuration), rowNumber: rowNumber }, results: benchResults, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur benchmark modulaire: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message, timestamp: new Date().toISOString() }); } } /** * 🆕 Gère les workflows modulaires avec sauvegarde par étapes */ async handleWorkflowModulaire(req, res) { try { const config = req.body; this.stats.testsExecuted++; // Configuration par défaut avec sauvegarde activée const workflowConfig = { rowNumber: config.rowNumber || 2, selectiveStack: config.selectiveStack || 'standardEnhancement', adversarialMode: config.adversarialMode || 'light', humanSimulationMode: config.humanSimulationMode || 'none', patternBreakingMode: config.patternBreakingMode || 'none', saveIntermediateSteps: config.saveIntermediateSteps !== false, // Par défaut true source: 'api_manual_server' }; logSh(`🔗 Workflow modulaire avec étapes: ligne ${workflowConfig.rowNumber}`, 'INFO'); logSh(` 📋 Config: ${workflowConfig.selectiveStack} + ${workflowConfig.adversarialMode} + ${workflowConfig.humanSimulationMode} + ${workflowConfig.patternBreakingMode}`, 'DEBUG'); logSh(` 💾 Sauvegarde étapes: ${workflowConfig.saveIntermediateSteps ? 'ACTIVÉE' : 'DÉSACTIVÉE'}`, 'INFO'); // Validation des paramètres if (workflowConfig.rowNumber < 2) { return res.status(400).json({ success: false, error: 'Numéro de ligne invalide (minimum 2)' }); } // Exécution du workflow complet const startTime = Date.now(); const result = await handleModularWorkflow(workflowConfig); const duration = Date.now() - startTime; // Statistiques finales const finalStats = { duration, success: result.success, versionsCreated: result.stats?.versionHistory?.length || 1, parentArticleId: result.stats?.parentArticleId, finalArticleId: result.storageResult?.articleId, totalModifications: { selective: result.stats?.selectiveEnhancements || 0, adversarial: result.stats?.adversarialModifications || 0, human: result.stats?.humanSimulationModifications || 0, pattern: result.stats?.patternBreakingModifications || 0 }, finalLength: result.stats?.finalLength || 0 }; logSh(`✅ Workflow modulaire terminé: ${finalStats.versionsCreated} versions créées en ${duration}ms`, 'INFO'); res.json({ success: true, message: `Workflow modulaire terminé avec succès (${finalStats.versionsCreated} versions sauvegardées)`, config: workflowConfig, stats: finalStats, versionHistory: result.stats?.versionHistory, result: { parentArticleId: finalStats.parentArticleId, finalArticleId: finalStats.finalArticleId, duration: finalStats.duration, modificationsCount: Object.values(finalStats.totalModifications).reduce((sum, val) => sum + val, 0), finalWordCount: result.storageResult?.wordCount, personality: result.stats?.personality }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur workflow modulaire: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message, timestamp: new Date().toISOString() }); } } /** * Retourne la configuration modulaire */ handleModulaireConfig(req, res) { try { const config = { selectiveStacks: [ { value: 'lightEnhancement', name: 'Light Enhancement', description: 'Améliorations légères' }, { value: 'standardEnhancement', name: 'Standard Enhancement', description: 'Améliorations standard' }, { value: 'fullEnhancement', name: 'Full Enhancement', description: 'Améliorations complètes' }, { value: 'personalityFocus', name: 'Personality Focus', description: 'Focus personnalité' }, { value: 'fluidityFocus', name: 'Fluidity Focus', description: 'Focus fluidité' }, { value: 'adaptive', name: 'Adaptive', description: 'Adaptation automatique' } ], adversarialModes: [ { value: 'none', name: 'None', description: 'Aucune technique adversariale' }, { value: 'light', name: 'Light', description: 'Techniques adversariales légères' }, { value: 'standard', name: 'Standard', description: 'Techniques adversariales standard' }, { value: 'heavy', name: 'Heavy', description: 'Techniques adversariales intensives' }, { value: 'adaptive', name: 'Adaptive', description: 'Adaptation automatique' } ], humanSimulationModes: [ { value: 'none', name: 'None', description: 'Aucune simulation humaine' }, { value: 'lightSimulation', name: 'Light Simulation', description: 'Simulation légère' }, { value: 'standardSimulation', name: 'Standard Simulation', description: 'Simulation standard' }, { value: 'heavySimulation', name: 'Heavy Simulation', description: 'Simulation intensive' }, { value: 'adaptiveSimulation', name: 'Adaptive Simulation', description: 'Simulation adaptative' }, { value: 'personalityFocus', name: 'Personality Focus', description: 'Focus personnalité' }, { value: 'temporalFocus', name: 'Temporal Focus', description: 'Focus temporel' } ], patternBreakingModes: [ { value: 'none', name: 'None', description: 'Aucun pattern breaking' }, { value: 'lightPatternBreaking', name: 'Light Pattern Breaking', description: 'Pattern breaking léger' }, { value: 'standardPatternBreaking', name: 'Standard Pattern Breaking', description: 'Pattern breaking standard' }, { value: 'heavyPatternBreaking', name: 'Heavy Pattern Breaking', description: 'Pattern breaking intensif' }, { value: 'adaptivePatternBreaking', name: 'Adaptive Pattern Breaking', description: 'Pattern breaking adaptatif' }, { value: 'syntaxFocus', name: 'Syntax Focus', description: 'Focus syntaxe uniquement' }, { value: 'connectorsFocus', name: 'Connectors Focus', description: 'Focus connecteurs uniquement' } ] }; res.json({ success: true, config: config, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur config modulaire: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } } /** * Lance le log viewer avec WebSocket */ handleStartLogViewer(req, res) { try { const { spawn } = require('child_process'); const path = require('path'); const os = require('os'); // Démarrer le WebSocket pour logs process.env.ENABLE_LOG_WS = 'true'; const { initWebSocketServer } = require('../ErrorReporting'); initWebSocketServer(); // Servir le log viewer via une route HTTP au lieu d'un fichier local const logViewerUrl = `http://localhost:${this.config.port}/logs-viewer.html`; // Ouvrir dans le navigateur selon l'OS let command, args; switch (os.platform()) { case 'darwin': // macOS command = 'open'; args = [logViewerUrl]; break; case 'win32': // Windows command = 'cmd'; args = ['/c', 'start', logViewerUrl]; break; default: // Linux et WSL // Pour WSL, utiliser explorer.exe de Windows if (process.env.WSL_DISTRO_NAME) { command = '/mnt/c/Windows/System32/cmd.exe'; args = ['/c', 'start', logViewerUrl]; } else { command = 'xdg-open'; args = [logViewerUrl]; } break; } spawn(command, args, { detached: true, stdio: 'ignore' }); const logPort = process.env.LOG_WS_PORT || 8082; logSh(`🌐 Log viewer lancé avec WebSocket sur port ${logPort}`, 'INFO'); res.json({ success: true, message: 'Log viewer lancé', wsPort: logPort, viewerUrl: logViewerUrl, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur lancement log viewer: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lancement log viewer', message: error.message, timestamp: new Date().toISOString() }); } } // ======================================== // HANDLERS STEP-BY-STEP // ======================================== /** * Initialise une nouvelle session step-by-step */ async handleStepByStepInit(req, res) { try { const { sessionManager } = require('../StepByStepSessionManager'); const inputData = req.body; logSh(`🎯 Initialisation session step-by-step`, 'INFO'); logSh(` Input: ${JSON.stringify(inputData)}`, 'DEBUG'); const session = sessionManager.createSession(inputData); res.json({ success: true, sessionId: session.id, steps: session.steps.map(step => ({ id: step.id, system: step.system, name: step.name, description: step.description, status: step.status })), inputData: session.inputData, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur init step-by-step: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur initialisation session', message: error.message, timestamp: new Date().toISOString() }); } } /** * Exécute une étape */ async handleStepByStepExecute(req, res) { try { const { sessionManager } = require('../StepByStepSessionManager'); const { StepExecutor } = require('../StepExecutor'); const { sessionId, stepId, options = {} } = req.body; if (!sessionId || !stepId) { return res.status(400).json({ success: false, error: 'sessionId et stepId requis', timestamp: new Date().toISOString() }); } logSh(`🚀 Exécution étape ${stepId} pour session ${sessionId}`, 'INFO'); // Récupérer la session const session = sessionManager.getSession(sessionId); // Trouver l'étape const step = session.steps.find(s => s.id === stepId); if (!step) { return res.status(400).json({ success: false, error: `Étape ${stepId} introuvable`, timestamp: new Date().toISOString() }); } // Marquer l'étape comme en cours step.status = 'executing'; // Créer l'exécuteur et lancer l'étape const executor = new StepExecutor(); logSh(`🚀 Execution step ${step.system} avec données: ${JSON.stringify(session.inputData)}`, 'DEBUG'); // Récupérer le contenu de l'étape précédente pour chaînage let inputContent = null; if (stepId > 1) { const previousResult = session.results.find(r => r.stepId === stepId - 1); logSh(`🔍 DEBUG Chaînage: previousResult=${!!previousResult}`, 'DEBUG'); if (previousResult) { logSh(`🔍 DEBUG Chaînage: previousResult.result=${!!previousResult.result}`, 'DEBUG'); if (previousResult.result) { // StepExecutor retourne un objet avec une propriété 'content' if (previousResult.result.content) { inputContent = previousResult.result.content; logSh(`🔄 Chaînage: utilisation contenu.content étape ${stepId - 1}`, 'DEBUG'); } else { // Fallback si c'est juste le contenu directement inputContent = previousResult.result; logSh(`🔄 Chaînage: utilisation contenu direct étape ${stepId - 1}`, 'DEBUG'); } logSh(`🔍 DEBUG: inputContent type=${typeof inputContent}, keys=${Object.keys(inputContent || {})}`, 'DEBUG'); } else { logSh(`🚨 DEBUG: previousResult.result est vide ou null !`, 'ERROR'); } } else { logSh(`🚨 DEBUG: Pas de previousResult trouvé pour stepId=${stepId - 1}`, 'ERROR'); } } // Ajouter le contenu d'entrée aux options si disponible const executionOptions = { ...options, inputContent: inputContent }; const result = await executor.executeStep(step.system, session.inputData, executionOptions); logSh(`📊 Résultat step ${step.system}: success=${result.success}, content=${Object.keys(result.content || {}).length} éléments, duration=${result.stats?.duration}ms`, 'INFO'); // Si pas d'erreur mais temps < 100ms, forcer une erreur pour debug if (result.success && result.stats?.duration < 100) { logSh(`⚠️ WARN: Step trop rapide (${result.stats?.duration}ms), probablement pas d'appel LLM réel`, 'WARN'); result.debugWarning = `⚠️ Exécution suspecte: ${result.stats?.duration}ms (probablement pas d'appel LLM)`; } // Ajouter le résultat à la session sessionManager.addStepResult(sessionId, stepId, result); // Déterminer la prochaine étape const nextStep = session.steps.find(s => s.id === stepId + 1); res.json({ success: true, stepId: stepId, system: step.system, name: step.name, result: { success: result.success, content: result.result, formatted: result.formatted, xmlFormatted: result.xmlFormatted, error: result.error, debugWarning: result.debugWarning }, stats: result.stats, nextStep: nextStep ? nextStep.id : null, sessionStatus: session.status, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur exécution step-by-step: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur exécution étape', message: error.message, timestamp: new Date().toISOString() }); } } /** * Récupère le status d'une session */ handleStepByStepStatus(req, res) { try { const { sessionManager } = require('../StepByStepSessionManager'); const { sessionId } = req.params; const session = sessionManager.getSession(sessionId); res.json({ success: true, session: { id: session.id, status: session.status, createdAt: new Date(session.createdAt).toISOString(), currentStep: session.currentStep, completedSteps: session.completedSteps, totalSteps: session.steps.length, inputData: session.inputData, steps: session.steps, globalStats: session.globalStats, results: session.results.map(r => ({ stepId: r.stepId, system: r.system, success: r.success, timestamp: new Date(r.timestamp).toISOString(), stats: r.stats })) }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur status step-by-step: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur récupération status', message: error.message, timestamp: new Date().toISOString() }); } } /** * Reset une session */ handleStepByStepReset(req, res) { try { const { sessionManager } = require('../StepByStepSessionManager'); const { sessionId } = req.body; if (!sessionId) { return res.status(400).json({ success: false, error: 'sessionId requis', timestamp: new Date().toISOString() }); } const session = sessionManager.resetSession(sessionId); res.json({ success: true, sessionId: session.id, message: 'Session reset avec succès', steps: session.steps, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur reset step-by-step: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur reset session', message: error.message, timestamp: new Date().toISOString() }); } } /** * Export les résultats d'une session */ handleStepByStepExport(req, res) { try { const { sessionManager } = require('../StepByStepSessionManager'); const { sessionId } = req.params; const exportData = sessionManager.exportSession(sessionId); res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Disposition', `attachment; filename="step-by-step-${sessionId}.json"`); res.json(exportData); } catch (error) { logSh(`❌ Erreur export step-by-step: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur export session', message: error.message, timestamp: new Date().toISOString() }); } } /** * Liste les sessions actives */ handleStepByStepSessions(req, res) { try { const { sessionManager } = require('../StepByStepSessionManager'); const sessions = sessionManager.listSessions(); res.json({ success: true, sessions: sessions, total: sessions.length, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur list sessions step-by-step: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur récupération sessions', message: error.message, timestamp: new Date().toISOString() }); } } /** * Handler pour récupérer les personnalités disponibles */ async handleGetPersonalities(req, res) { try { const { getPersonalities } = require('../BrainConfig'); const personalities = await getPersonalities(); res.json({ success: true, personalities: personalities || [], total: (personalities || []).length, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération personnalités: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur récupération personnalités', message: error.message, timestamp: new Date().toISOString() }); } } /** * 🆕 Handler pour génération simple d'article avec mot-clé */ async handleGenerateSimple(req, res) { try { const { keyword } = req.body; // Validation basique if (!keyword || typeof keyword !== 'string' || keyword.trim().length === 0) { return res.status(400).json({ success: false, error: 'Mot-clé requis', message: 'Le paramètre "keyword" est obligatoire et doit être une chaîne non vide' }); } const cleanKeyword = keyword.trim(); logSh(`🎯 Génération simple pour mot-clé: "${cleanKeyword}"`, 'INFO'); // Créer un template XML simple basé sur le mot-clé const simpleTemplate = `

|Titre_Principal{{${cleanKeyword}}}{Rédige un titre H1 accrocheur pour "${cleanKeyword}"}|

|Introduction{{${cleanKeyword}}}{Rédige une introduction engageante de 2-3 phrases pour "${cleanKeyword}"}|

|Sous_Titre_1{{${cleanKeyword}}}{Rédige un sous-titre H2 pour "${cleanKeyword}"}|

|Contenu_1{{${cleanKeyword}}}{Rédige un paragraphe détaillé sur "${cleanKeyword}"}|

|Sous_Titre_2{{${cleanKeyword}}}{Rédige un autre sous-titre H2 pour "${cleanKeyword}"}|

|Contenu_2{{${cleanKeyword}}}{Rédige un autre paragraphe sur "${cleanKeyword}"}|
|Conclusion{{${cleanKeyword}}}{Rédige une conclusion pour l'article sur "${cleanKeyword}"}|
`; // Préparer les données pour le workflow const workflowData = { csvData: { mc0: cleanKeyword, t0: `Guide complet sur ${cleanKeyword}`, personality: { nom: 'Marc', style: 'professionnel' }, tMinus1: cleanKeyword, mcPlus1: `${cleanKeyword},guide ${cleanKeyword},tout savoir ${cleanKeyword}`, tPlus1: `Guide ${cleanKeyword},Conseils ${cleanKeyword},${cleanKeyword} pratique` }, xmlTemplate: Buffer.from(simpleTemplate).toString('base64'), source: 'api_generate_simple' }; logSh(`📝 Template créé pour "${cleanKeyword}"`, 'DEBUG'); // Utiliser le workflow modulaire simple (juste génération de base) const { handleModularWorkflow } = require('../Main'); const config = { selectiveStack: 'lightEnhancement', adversarialMode: 'none', humanSimulationMode: 'none', patternBreakingMode: 'none', saveVersions: false, source: 'api_generate_simple' }; logSh(`🚀 Démarrage génération modulaire pour "${cleanKeyword}"`, 'INFO'); const result = await handleModularWorkflow(workflowData, config); logSh(`✅ Génération terminée pour "${cleanKeyword}"`, 'INFO'); // Réponse simplifiée res.json({ success: true, keyword: cleanKeyword, article: { content: result.compiledText || result.generatedTexts || 'Contenu généré', title: result.generatedTexts?.Titre_Principal || `Article sur ${cleanKeyword}`, meta: { processing_time: result.processingTime || 'N/A', personality: result.personality?.nom || 'Marc', version: result.version || 'v1.0' } }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur génération simple: ${error.message}`, 'ERROR'); logSh(`Stack: ${error.stack}`, 'DEBUG'); res.status(500).json({ success: false, error: 'Erreur lors de la génération', message: error.message, timestamp: new Date().toISOString() }); } } // ======================================== // INTERFACE WEB // ======================================== /** * Configure l'interface web */ setupWebInterface() { // Page d'accueil - Dashboard MANUAL this.app.get('/', (req, res) => { res.send(this.generateManualDashboard()); }); // Route pour le log viewer this.app.get('/logs-viewer.html', (req, res) => { const fs = require('fs'); const logViewerPath = path.join(__dirname, '../../tools/logs-viewer.html'); try { const content = fs.readFileSync(logViewerPath, 'utf-8'); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(content); } catch (error) { logSh(`❌ Erreur lecture log viewer: ${error.message}`, 'ERROR'); res.status(500).send(`Erreur: ${error.message}`); } }); // Route 404 this.app.use('*', (req, res) => { res.status(404).json({ success: false, error: 'Route non trouvée', path: req.originalUrl, mode: 'MANUAL', message: 'Cette route n\'existe pas en mode MANUAL' }); }); logSh('🌐 Interface web configurée', 'DEBUG'); } /** * Génère le dashboard HTML du mode MANUAL */ generateManualDashboard() { const uptime = Math.floor((Date.now() - this.stats.startTime) / 1000); return ` SEO Generator - Mode MANUAL

🎯 SEO Generator Server

MODE MANUAL

Interface Client + API + Tests Modulaires

✅ Mode MANUAL Actif
Interface complète disponible • WebSocket temps réel • API complète
${uptime}s
Uptime
${this.stats.requests}
Requêtes
${this.activeClients.size}
Clients WebSocket
${this.stats.testsExecuted}
Tests Exécutés

🧪 Interface Test Modulaire

Interface avancée pour tester toutes les combinaisons modulaires avec logs temps réel.

🚀 Ouvrir Interface Test ⚡ Interface Step-by-Step 📋 Configuration API

📊 Monitoring & API

Endpoints disponibles en mode MANUAL.

📊 Status API 📈 Statistiques

🌐 WebSocket Logs

Logs temps réel sur ws://localhost:${this.config.wsPort}

Status: Déconnecté

💡 Informations Mode MANUAL

  • Interface Client : Dashboard complet et interface de test
  • API Complète : Tests individuels, benchmarks, configuration
  • WebSocket : Logs temps réel sur port ${this.config.wsPort}
  • Multi-Client : Plusieurs utilisateurs simultanés
  • Pas de GSheets : Données test simulées ou fournies
`; } // ======================================== // WEBSOCKET SERVER // ======================================== /** * Configure le serveur WebSocket pour logs temps réel */ async setupWebSocketServer() { try { this.wsServer = new WebSocket.Server({ port: this.config.wsPort, host: this.config.host }); this.wsServer.on('connection', (ws, req) => { this.handleWebSocketConnection(ws, req); }); this.wsServer.on('error', (error) => { logSh(`❌ Erreur WebSocket: ${error.message}`, 'ERROR'); }); logSh(`📡 WebSocket Server démarré sur ws://${this.config.host}:${this.config.wsPort}`, 'DEBUG'); } catch (error) { logSh(`⚠️ Impossible de démarrer WebSocket: ${error.message}`, 'WARNING'); // Continue sans WebSocket si erreur } } /** * Gère les nouvelles connexions WebSocket */ handleWebSocketConnection(ws, req) { const clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const clientIP = req.socket.remoteAddress; const clientData = { id: clientId, ws, ip: clientIP, connectedAt: Date.now() }; this.activeClients.add(clientData); this.stats.sessions++; logSh(`📡 Nouveau client WebSocket: ${clientId} (${clientIP})`, 'TRACE'); // Message de bienvenue ws.send(JSON.stringify({ type: 'welcome', message: 'Connecté aux logs temps réel SEO Generator (Mode MANUAL)', clientId: clientId, timestamp: new Date().toISOString() })); // Gestion fermeture ws.on('close', () => { this.activeClients.delete(clientData); logSh(`📡 Client WebSocket déconnecté: ${clientId}`, 'TRACE'); }); // Gestion erreurs ws.on('error', (error) => { this.activeClients.delete(clientData); logSh(`⚠️ Erreur client WebSocket ${clientId}: ${error.message}`, 'WARNING'); }); } /** * Diffuse un message à tous les clients WebSocket */ broadcastToClients(logData) { if (this.activeClients.size === 0) return; const message = JSON.stringify({ type: 'log', ...logData, timestamp: new Date().toISOString() }); this.activeClients.forEach(client => { if (client.ws.readyState === WebSocket.OPEN) { try { client.ws.send(message); } catch (error) { // Client déconnecté, le supprimer this.activeClients.delete(client); } } }); } /** * Déconnecte tous les clients WebSocket */ disconnectAllClients() { this.activeClients.forEach(client => { try { client.ws.close(); } catch (error) { // Ignore les erreurs de fermeture } }); this.activeClients.clear(); logSh('📡 Tous les clients WebSocket déconnectés', 'DEBUG'); } // ======================================== // SERVEUR HTTP // ======================================== /** * Démarre le serveur HTTP */ async startHTTPServer() { return new Promise((resolve, reject) => { try { this.server = this.app.listen(this.config.port, this.config.host, () => { resolve(); }); this.server.on('error', (error) => { reject(error); }); } catch (error) { reject(error); } }); } // ======================================== // MONITORING // ======================================== /** * Démarre le monitoring du serveur */ startMonitoring() { const MONITOR_INTERVAL = 30000; // 30 secondes this.monitorInterval = setInterval(() => { this.performMonitoring(); }, MONITOR_INTERVAL); logSh('💓 Monitoring ManualServer démarré', 'DEBUG'); } /** * Effectue le monitoring périodique */ performMonitoring() { const memUsage = process.memoryUsage(); const uptime = Date.now() - this.stats.startTime; logSh(`💓 ManualServer Health - Clients: ${this.activeClients.size} | Requêtes: ${this.stats.requests} | RAM: ${Math.round(memUsage.rss / 1024 / 1024)}MB`, 'TRACE'); // Nettoyage clients WebSocket morts this.cleanupDeadClients(); } /** * Nettoie les clients WebSocket déconnectés */ cleanupDeadClients() { let cleaned = 0; const deadClients = []; this.activeClients.forEach(client => { if (client.ws.readyState !== WebSocket.OPEN) { deadClients.push(client); cleaned++; } }); // Supprimer les clients morts deadClients.forEach(client => { this.activeClients.delete(client); }); if (cleaned > 0) { logSh(`🧹 ${cleaned} clients WebSocket morts nettoyés`, 'TRACE'); } } // ======================================== // ÉTAT ET CONTRÔLES // ======================================== /** * Vérifie s'il y a des clients actifs */ hasActiveClients() { return this.activeClients.size > 0; } /** * Retourne l'état du serveur MANUAL */ getStatus() { return { isRunning: this.isRunning, config: { ...this.config }, stats: { ...this.stats, uptime: Date.now() - this.stats.startTime }, activeClients: this.activeClients.size, urls: { dashboard: `http://localhost:${this.config.port}`, testInterface: `http://localhost:${this.config.port}/test-modulaire.html`, apiStatus: `http://localhost:${this.config.port}/api/status`, websocket: `ws://localhost:${this.config.wsPort}` } }; } } // ============= EXPORTS ============= module.exports = { ManualServer }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/modes/ModeManager.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: ModeManager.js // RESPONSABILITÉ: Gestionnaire modes exclusifs serveur // MODES: MANUAL (interface client) | AUTO (traitement batch GSheets) // ======================================== const { logSh } = require('../ErrorReporting'); const fs = require('fs'); const path = require('path'); /** * GESTIONNAIRE MODES EXCLUSIFS * Gère le basculement entre mode MANUAL et AUTO de façon exclusive */ class ModeManager { // ======================================== // CONSTANTES ET ÉTAT // ======================================== static MODES = { MANUAL: 'manual', // Interface client + API + WebSocket AUTO: 'auto' // Traitement batch Google Sheets }; static currentMode = null; static isLocked = false; static lockReason = null; static modeStartTime = null; static activeServices = { manualServer: null, autoProcessor: null, websocketServer: null }; // Stats par mode static stats = { manual: { sessions: 0, requests: 0, lastActivity: null }, auto: { processed: 0, errors: 0, lastProcessing: null } }; // ======================================== // INITIALISATION ET DÉTECTION MODE // ======================================== /** * Initialise le gestionnaire de modes * @param {string} initialMode - Mode initial (manual|auto|detect) */ static async initialize(initialMode = 'detect') { logSh('🎛️ Initialisation ModeManager...', 'INFO'); try { // Détecter mode selon arguments ou config const detectedMode = this.detectIntendedMode(initialMode); logSh(`🎯 Mode détecté: ${detectedMode.toUpperCase()}`, 'INFO'); // Nettoyer état précédent si nécessaire await this.cleanupPreviousState(); // Basculer vers le mode détecté await this.switchToMode(detectedMode); // Sauvegarder état this.saveModeState(); logSh(`✅ ModeManager initialisé en mode ${this.currentMode.toUpperCase()}`, 'INFO'); return this.currentMode; } catch (error) { logSh(`❌ Erreur initialisation ModeManager: ${error.message}`, 'ERROR'); throw new Error(`Échec initialisation ModeManager: ${error.message}`); } } /** * Détecte le mode souhaité selon arguments CLI et env */ static detectIntendedMode(initialMode) { // 1. Argument explicite if (initialMode === this.MODES.MANUAL || initialMode === this.MODES.AUTO) { return initialMode; } // 2. Arguments de ligne de commande const args = process.argv.slice(2); const modeArg = args.find(arg => arg.startsWith('--mode=')); if (modeArg) { const mode = modeArg.split('=')[1]; if (Object.values(this.MODES).includes(mode)) { return mode; } } // 3. Variable d'environnement const envMode = process.env.SERVER_MODE?.toLowerCase(); if (Object.values(this.MODES).includes(envMode)) { return envMode; } // 4. Script npm spécifique const npmScript = process.env.npm_lifecycle_event; if (npmScript === 'auto') return this.MODES.AUTO; // 5. Défaut = MANUAL return this.MODES.MANUAL; } // ======================================== // CHANGEMENT DE MODES // ======================================== /** * Bascule vers un mode spécifique */ static async switchToMode(targetMode, force = false) { if (!Object.values(this.MODES).includes(targetMode)) { throw new Error(`Mode invalide: ${targetMode}`); } if (this.currentMode === targetMode) { logSh(`Mode ${targetMode} déjà actif`, 'DEBUG'); return true; } // Vérifier si changement possible if (!force && !await this.canSwitchToMode(targetMode)) { throw new Error(`Impossible de basculer vers ${targetMode}: ${this.lockReason}`); } logSh(`🔄 Basculement ${this.currentMode || 'NONE'} → ${targetMode}...`, 'INFO'); try { // Arrêter mode actuel await this.stopCurrentMode(); // Démarrer nouveau mode await this.startMode(targetMode); // Mettre à jour état this.currentMode = targetMode; this.modeStartTime = Date.now(); this.lockReason = null; logSh(`✅ Basculement terminé: Mode ${targetMode.toUpperCase()} actif`, 'INFO'); return true; } catch (error) { logSh(`❌ Échec basculement vers ${targetMode}: ${error.message}`, 'ERROR'); // Tentative de récupération try { await this.emergencyRecovery(); } catch (recoveryError) { logSh(`❌ Échec récupération d'urgence: ${recoveryError.message}`, 'ERROR'); } throw error; } } /** * Vérifie si le basculement est possible */ static async canSwitchToMode(targetMode) { // Mode verrouillé if (this.isLocked) { this.lockReason = 'Mode verrouillé pour opération critique'; return false; } // Vérifications spécifiques par mode switch (targetMode) { case this.MODES.MANUAL: return await this.canSwitchToManual(); case this.MODES.AUTO: return await this.canSwitchToAuto(); default: return false; } } /** * Peut-on basculer vers MANUAL ? */ static async canSwitchToManual() { // Si mode AUTO actif, vérifier processus if (this.currentMode === this.MODES.AUTO) { const autoProcessor = this.activeServices.autoProcessor; if (autoProcessor && autoProcessor.isProcessing()) { this.lockReason = 'Traitement automatique en cours, arrêt requis'; return false; } } return true; } /** * Peut-on basculer vers AUTO ? */ static async canSwitchToAuto() { // Si mode MANUAL actif, vérifier clients if (this.currentMode === this.MODES.MANUAL) { const manualServer = this.activeServices.manualServer; if (manualServer && manualServer.hasActiveClients()) { this.lockReason = 'Clients actifs en mode MANUAL, déconnexion requise'; return false; } } return true; } // ======================================== // DÉMARRAGE ET ARRÊT SERVICES // ======================================== /** * Démarre un mode spécifique */ static async startMode(mode) { logSh(`🚀 Démarrage mode ${mode.toUpperCase()}...`, 'DEBUG'); switch (mode) { case this.MODES.MANUAL: await this.startManualMode(); break; case this.MODES.AUTO: await this.startAutoMode(); break; default: throw new Error(`Mode de démarrage inconnu: ${mode}`); } } /** * Démarre le mode MANUAL */ static async startManualMode() { const { ManualServer } = require('./ManualServer'); logSh('🎯 Démarrage ManualServer...', 'DEBUG'); this.activeServices.manualServer = new ManualServer(); await this.activeServices.manualServer.start(); logSh('✅ Mode MANUAL démarré', 'DEBUG'); } /** * Démarre le mode AUTO */ static async startAutoMode() { const { AutoProcessor } = require('./AutoProcessor'); logSh('🤖 Démarrage AutoProcessor...', 'DEBUG'); this.activeServices.autoProcessor = new AutoProcessor(); await this.activeServices.autoProcessor.start(); logSh('✅ Mode AUTO démarré', 'DEBUG'); } /** * Arrête le mode actuel */ static async stopCurrentMode() { if (!this.currentMode) return; logSh(`🛑 Arrêt mode ${this.currentMode.toUpperCase()}...`, 'DEBUG'); try { switch (this.currentMode) { case this.MODES.MANUAL: await this.stopManualMode(); break; case this.MODES.AUTO: await this.stopAutoMode(); break; } logSh(`✅ Mode ${this.currentMode.toUpperCase()} arrêté`, 'DEBUG'); } catch (error) { logSh(`⚠️ Erreur arrêt mode ${this.currentMode}: ${error.message}`, 'WARNING'); // Continue malgré l'erreur pour permettre le changement } } /** * Arrête le mode MANUAL */ static async stopManualMode() { if (this.activeServices.manualServer) { await this.activeServices.manualServer.stop(); this.activeServices.manualServer = null; } } /** * Arrête le mode AUTO */ static async stopAutoMode() { if (this.activeServices.autoProcessor) { await this.activeServices.autoProcessor.stop(); this.activeServices.autoProcessor = null; } } // ======================================== // ÉTAT ET MONITORING // ======================================== /** * État actuel du gestionnaire */ static getStatus() { return { currentMode: this.currentMode, isLocked: this.isLocked, lockReason: this.lockReason, modeStartTime: this.modeStartTime, uptime: this.modeStartTime ? Date.now() - this.modeStartTime : 0, stats: { ...this.stats }, activeServices: { manualServer: !!this.activeServices.manualServer, autoProcessor: !!this.activeServices.autoProcessor } }; } /** * Vérifie si mode MANUAL actif */ static isManualMode() { return this.currentMode === this.MODES.MANUAL; } /** * Vérifie si mode AUTO actif */ static isAutoMode() { return this.currentMode === this.MODES.AUTO; } /** * Verrouille le mode actuel */ static lockMode(reason = 'Opération critique') { this.isLocked = true; this.lockReason = reason; logSh(`🔒 Mode ${this.currentMode} verrouillé: ${reason}`, 'INFO'); } /** * Déverrouille le mode */ static unlockMode() { this.isLocked = false; this.lockReason = null; logSh(`🔓 Mode ${this.currentMode} déverrouillé`, 'INFO'); } // ======================================== // GESTION ERREURS ET RÉCUPÉRATION // ======================================== /** * Nettoyage état précédent */ static async cleanupPreviousState() { logSh('🧹 Nettoyage état précédent...', 'DEBUG'); // Arrêter tous les services actifs await this.stopCurrentMode(); // Reset état this.isLocked = false; this.lockReason = null; logSh('✅ Nettoyage terminé', 'DEBUG'); } /** * Récupération d'urgence */ static async emergencyRecovery() { logSh('🚨 Récupération d\'urgence...', 'WARNING'); try { // Forcer arrêt de tous les services await this.forceStopAllServices(); // Reset état complet this.currentMode = null; this.isLocked = false; this.lockReason = null; this.modeStartTime = null; logSh('✅ Récupération d\'urgence terminée', 'INFO'); } catch (error) { logSh(`❌ Échec récupération d'urgence: ${error.message}`, 'ERROR'); throw error; } } /** * Arrêt forcé de tous les services */ static async forceStopAllServices() { const services = Object.keys(this.activeServices); for (const serviceKey of services) { const service = this.activeServices[serviceKey]; if (service) { try { if (typeof service.stop === 'function') { await service.stop(); } } catch (error) { logSh(`⚠️ Erreur arrêt forcé ${serviceKey}: ${error.message}`, 'WARNING'); } this.activeServices[serviceKey] = null; } } } // ======================================== // PERSISTANCE ET CONFIGURATION // ======================================== /** * Sauvegarde l'état du mode */ static saveModeState() { try { const stateFile = path.join(__dirname, '../..', 'mode-state.json'); const state = { currentMode: this.currentMode, modeStartTime: this.modeStartTime, stats: this.stats, timestamp: new Date().toISOString() }; fs.writeFileSync(stateFile, JSON.stringify(state, null, 2)); } catch (error) { logSh(`⚠️ Erreur sauvegarde état mode: ${error.message}`, 'WARNING'); } } /** * Restaure l'état du mode */ static loadModeState() { try { const stateFile = path.join(__dirname, '../..', 'mode-state.json'); if (fs.existsSync(stateFile)) { const state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); this.stats = state.stats || this.stats; return state; } } catch (error) { logSh(`⚠️ Erreur chargement état mode: ${error.message}`, 'WARNING'); } return null; } } // ============= EXPORTS ============= module.exports = { ModeManager };