/*
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
Éléments extraits: ${report.stats.elementsExtracted}
Contenus générés: ${report.stats.contentGenerated}
Tags remplacés: ${report.stats.tagsReplaced}
Tags restants: ${report.stats.tagsRemaining}
Informations Système
Plateforme: Node.js
Version: ${process.version}
Mémoire: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB
Uptime: ${Math.round(process.uptime())}s
`;
return html;
}
// 🔄 NODE.JS EXPORTS
module.exports = {
logSh,
cleanLogSheet,
validateWorkflowIntegrity,
detectDuplicateTags,
detectMissingCSVVariables,
assessGenerationQuality,
sendErrorReport,
createHTMLReport,
initWebSocketServer
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/trace.js │
└────────────────────────────────────────────────────────────────────┘
*/
// lib/trace.js
const { AsyncLocalStorage } = require('node:async_hooks');
const { randomUUID } = require('node:crypto');
const { logSh } = require('./ErrorReporting');
const als = new AsyncLocalStorage();
function now() { return performance.now(); }
function dur(ms) {
if (ms < 1e3) return `${ms.toFixed(1)}ms`;
const s = ms / 1e3;
return s < 60 ? `${s.toFixed(2)}s` : `${(s/60).toFixed(2)}m`;
}
class Span {
constructor({ name, parent = null, attrs = {} }) {
this.id = randomUUID();
this.name = name;
this.parent = parent;
this.children = [];
this.attrs = attrs;
this.start = now();
this.end = null;
this.status = 'ok';
this.error = null;
}
pathNames() {
const names = [];
let cur = this;
while (cur) { names.unshift(cur.name); cur = cur.parent; }
return names.join(' > ');
}
finish() { this.end = now(); }
duration() { return (this.end ?? now()) - this.start; }
}
class Tracer {
constructor() {
this.rootSpans = [];
}
current() { return als.getStore(); }
async startSpan(name, attrs = {}) {
const parent = this.current();
const span = new Span({ name, parent, attrs });
if (parent) parent.children.push(span);
else this.rootSpans.push(span);
// Formater les paramètres pour affichage
const paramsStr = this.formatParams(attrs);
await logSh(`▶ ${name}${paramsStr}`, 'TRACE');
return span;
}
async run(name, fn, attrs = {}) {
const parent = this.current();
const span = await this.startSpan(name, attrs);
return await als.run(span, async () => {
try {
const res = await fn();
span.finish();
const paramsStr = this.formatParams(span.attrs);
await logSh(`✔ ${name}${paramsStr} (${dur(span.duration())})`, 'TRACE');
return res;
} catch (err) {
span.status = 'error';
span.error = { message: err?.message, stack: err?.stack };
span.finish();
const paramsStr = this.formatParams(span.attrs);
await logSh(`✖ ${name}${paramsStr} FAILED (${dur(span.duration())})`, 'ERROR');
await logSh(`Stack trace: ${span.error.message}`, 'ERROR');
if (span.error.stack) {
const stackLines = span.error.stack.split('\n').slice(1, 6); // Première 5 lignes du stack
for (const line of stackLines) {
await logSh(` ${line.trim()}`, 'ERROR');
}
}
throw err;
}
});
}
async event(msg, extra = {}) {
const span = this.current();
const data = { trace: true, evt: 'span.event', ...extra };
if (span) {
data.span = span.id;
data.path = span.pathNames();
data.since_ms = +( (now() - span.start).toFixed(1) );
}
await logSh(`• ${msg}`, 'TRACE');
}
async annotate(fields = {}) {
const span = this.current();
if (span) Object.assign(span.attrs, fields);
await logSh('… annotate', 'TRACE');
}
formatParams(attrs = {}) {
const params = Object.entries(attrs)
.filter(([key, value]) => value !== undefined && value !== null)
.map(([key, value]) => {
// Tronquer les valeurs trop longues
const strValue = String(value);
const truncated = strValue.length > 50 ? strValue.substring(0, 47) + '...' : strValue;
return `${key}=${truncated}`;
});
return params.length > 0 ? `(${params.join(', ')})` : '';
}
printSummary() {
const lines = [];
const draw = (node, depth = 0) => {
const pad = ' '.repeat(depth);
const icon = node.status === 'error' ? '✖' : '✔';
lines.push(`${pad}${icon} ${node.name} (${dur(node.duration())})`);
if (Object.keys(node.attrs ?? {}).length) {
lines.push(`${pad} attrs: ${JSON.stringify(node.attrs)}`);
}
for (const ch of node.children) draw(ch, depth + 1);
if (node.status === 'error' && node.error?.message) {
lines.push(`${pad} error: ${node.error.message}`);
if (node.error.stack) {
const stackLines = String(node.error.stack || '').split('\n').slice(1, 4).map(s => s.trim());
if (stackLines.length) {
lines.push(`${pad} stack:`);
stackLines.forEach(line => {
if (line) lines.push(`${pad} ${line}`);
});
}
}
}
};
for (const r of this.rootSpans) draw(r, 0);
const summary = lines.join('\n');
logSh(`\n—— TRACE SUMMARY ——\n${summary}\n—— END TRACE ——`, 'INFO');
return summary;
}
}
const tracer = new Tracer();
module.exports = {
Span,
Tracer,
tracer
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/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}}}|
|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}}}|
`;
}
/**
* 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}${tag.toLowerCase()}>`)
.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
🤖 Mode AUTO Actif
Traitement batch des Google Sheets • Interface monitoring lecture seule
Progression: ${progress}% (${completedCount}/${this.stats.itemsQueued})
${pendingCount}
En Attente
${this.state.isProcessing ? '1' : '0'}
En Traitement
${completedCount}
Terminés
${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 ?
'
▶️ Reprendre ' :
'
⏸️ Pause '
}
🔄 Actualiser
📊 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
🤖 Mode AUTO Actif
Traitement batch des Google Sheets • Interface monitoring lecture seule
Progression: ${progress}% (${completedCount}/${this.stats.itemsQueued})
${pendingCount}
En Attente
${this.currentRow ? '1' : '0'}
En Traitement
${completedCount}
Terminés
${this.stats.averageProcessingTime}ms
Temps Moyen
${this.currentRow ? `
🎯 Traitement en cours:
Ligne ${this.currentRow}
` : ''}
🎛️ Contrôles
${this.isPaused ?
'
▶️ Reprendre ' :
'
⏸️ Pause '
}
🔄 Actualiser
📊 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
✅ Mode MANUAL Actif
Interface complète disponible • WebSocket temps réel • API complète
${this.stats.requests}
Requêtes
${this.activeClients.size}
Clients WebSocket
${this.stats.testsExecuted}
Tests Exécutés
🌐 WebSocket Logs
Logs temps réel sur ws://localhost:${this.config.wsPort}
🔍 Ouvrir Log Viewer
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 };