seo-generator-server/code.js

26193 lines
886 KiB
JavaScript

/*
code.js — bundle concaténé
Généré: 2025-09-10T11:46:20.952Z
Source: lib
Fichiers: 55
Ordre: topo
*/
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/ErrorReporting.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: lib/error-reporting.js - CONVERTI POUR NODE.JS
// Description: Système de validation et rapport d'erreur
// ========================================
// Lazy loading des modules externes (évite blocage googleapis)
let google, nodemailer;
const fs = require('fs').promises;
const path = require('path');
const pino = require('pino');
const pretty = require('pino-pretty');
const { PassThrough } = require('stream');
const WebSocket = require('ws');
// Configuration
const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
// WebSocket server for real-time logs
let wsServer;
const wsClients = new Set();
// Enhanced Pino logger configuration with real-time streaming and dated files
const now = new Date();
const timestamp = now.toISOString().slice(0, 10) + '_' +
now.toLocaleTimeString('fr-FR').replace(/:/g, '-');
const logFile = path.join(__dirname, '..', 'logs', `seo-generator-${timestamp}.log`);
const prettyStream = pretty({
colorize: true,
translateTime: 'HH:MM:ss.l',
ignore: 'pid,hostname',
});
const tee = new PassThrough();
// Lazy loading des pipes console (évite blocage à l'import)
let consolePipeInitialized = false;
// File destination with dated filename - FORCE DEBUG LEVEL
const fileDest = pino.destination({
dest: logFile,
mkdir: true,
sync: false,
minLength: 0 // Force immediate write even for small logs
});
tee.pipe(fileDest);
// Custom levels for Pino to include TRACE, PROMPT, and LLM
const customLevels = {
trace: 5, // Below debug (10)
debug: 10,
info: 20,
prompt: 25, // New level for prompts (between info and warn)
llm: 26, // New level for LLM interactions (between prompt and warn)
warn: 30,
error: 40,
fatal: 50
};
// Pino logger instance with enhanced configuration and custom levels
const logger = pino(
{
level: 'debug', // FORCE DEBUG LEVEL for file logging
base: undefined,
timestamp: pino.stdTimeFunctions.isoTime,
customLevels: customLevels,
useOnlyCustomLevels: true
},
tee
);
// Initialize WebSocket server (only when explicitly requested)
function initWebSocketServer() {
if (!wsServer && process.env.ENABLE_LOG_WS === 'true') {
try {
const logPort = process.env.LOG_WS_PORT || 8082;
wsServer = new WebSocket.Server({ port: logPort });
wsServer.on('connection', (ws) => {
wsClients.add(ws);
logger.info('Client connected to log WebSocket');
ws.on('close', () => {
wsClients.delete(ws);
logger.info('Client disconnected from log WebSocket');
});
ws.on('error', (error) => {
logger.error('WebSocket error:', error.message);
wsClients.delete(ws);
});
});
wsServer.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
logger.warn(`WebSocket port ${logPort} already in use`);
wsServer = null;
} else {
logger.error('WebSocket server error:', error.message);
}
});
logger.info(`Log WebSocket server started on port ${logPort}`);
} catch (error) {
logger.warn(`Failed to start WebSocket server: ${error.message}`);
wsServer = null;
}
}
}
// Broadcast log to WebSocket clients
function broadcastLog(message, level) {
const logData = {
timestamp: new Date().toISOString(),
level: level.toUpperCase(),
message: message
};
wsClients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
try {
ws.send(JSON.stringify(logData));
} catch (error) {
logger.error('Failed to send log to WebSocket client:', error.message);
wsClients.delete(ws);
}
}
});
}
// 🔄 NODE.JS : Google Sheets API setup (remplace SpreadsheetApp)
let sheets;
let auth;
async function initGoogleSheets() {
if (!sheets) {
// Lazy load googleapis seulement quand nécessaire
if (!google) {
google = require('googleapis').google;
}
// Configuration auth Google Sheets API
// Pour la démo, on utilise une clé de service (à configurer)
auth = new google.auth.GoogleAuth({
keyFile: process.env.GOOGLE_CREDENTIALS_PATH, // Chemin vers fichier JSON credentials
scopes: ['https://www.googleapis.com/auth/spreadsheets']
});
sheets = google.sheets({ version: 'v4', auth });
}
return sheets;
}
async function logSh(message, level = 'INFO') {
// Initialize WebSocket server if not already done
if (!wsServer) {
initWebSocketServer();
}
// Initialize console pipe if needed (lazy loading)
if (!consolePipeInitialized && process.env.ENABLE_CONSOLE_LOG === 'true') {
tee.pipe(prettyStream).pipe(process.stdout);
consolePipeInitialized = true;
}
// Convert level to lowercase for Pino
const pinoLevel = level.toLowerCase();
// Enhanced trace metadata for hierarchical logging
const traceData = {};
if (message.includes('▶') || message.includes('✔') || message.includes('✖') || message.includes('•')) {
traceData.trace = true;
traceData.evt = message.includes('▶') ? 'span.start' :
message.includes('✔') ? 'span.end' :
message.includes('✖') ? 'span.error' : 'span.event';
}
// Log with Pino (handles console output with pretty formatting and file logging)
switch (pinoLevel) {
case 'error':
logger.error(traceData, message);
break;
case 'warning':
case 'warn':
logger.warn(traceData, message);
break;
case 'debug':
logger.debug(traceData, message);
break;
case 'trace':
logger.trace(traceData, message);
break;
case 'prompt':
logger.prompt(traceData, message);
break;
case 'llm':
logger.llm(traceData, message);
break;
default:
logger.info(traceData, message);
}
// Broadcast to WebSocket clients for real-time viewing
broadcastLog(message, level);
// Force immediate flush to ensure real-time display and prevent log loss
logger.flush();
// Log to Google Sheets if enabled (async, non-blocking)
if (process.env.ENABLE_SHEETS_LOGGING === 'true') {
setImmediate(() => {
logToGoogleSheets(message, level).catch(err => {
// Silent fail for Google Sheets logging to avoid recursion
});
});
}
}
// Fonction pour déterminer si on doit logger en console
function shouldLogToConsole(messageLevel, configLevel) {
const levels = { DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3 };
return levels[messageLevel] >= levels[configLevel];
}
// Log to file is now handled by Pino transport
// This function is kept for compatibility but does nothing
async function logToFile(message, level) {
// Pino handles file logging via transport configuration
// This function is deprecated and kept for compatibility only
}
// 🔄 NODE.JS : Log vers Google Sheets (version async)
async function logToGoogleSheets(message, level) {
try {
const sheetsApi = await initGoogleSheets();
const values = [[
new Date().toISOString(),
level,
message,
'Node.js workflow'
]];
await sheetsApi.spreadsheets.values.append({
spreadsheetId: SHEET_ID,
range: 'Logs!A:D',
valueInputOption: 'RAW',
insertDataOption: 'INSERT_ROWS',
resource: { values }
});
} catch (error) {
logSh('Échec log Google Sheets: ' + error.message, 'WARNING'); // Using logSh instead of console.warn
}
}
// 🔄 NODE.JS : Version simplifiée cleanLogSheet
async function cleanLogSheet() {
try {
logSh('🧹 Nettoyage logs...', 'INFO'); // Using logSh instead of console.log
// 1. Nettoyer fichiers logs locaux (garder 7 derniers jours)
await cleanLocalLogs();
// 2. Nettoyer Google Sheets si activé
if (process.env.ENABLE_SHEETS_LOGGING === 'true') {
await cleanGoogleSheetsLogs();
}
logSh('✅ Logs nettoyés', 'INFO'); // Using logSh instead of console.log
} catch (error) {
logSh('Erreur nettoyage logs: ' + error.message, 'ERROR'); // Using logSh instead of console.error
}
}
async function cleanLocalLogs() {
try {
// Note: With Pino, log files are managed differently
// This function is kept for compatibility with Google Sheets logs cleanup
// Pino log rotation should be handled by external tools like logrotate
// For now, we keep the basic cleanup for any remaining old log files
const logsDir = path.join(__dirname, '../logs');
try {
const files = await fs.readdir(logsDir);
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - 7); // Garder 7 jours
for (const file of files) {
if (file.endsWith('.log')) {
const filePath = path.join(logsDir, file);
const stats = await fs.stat(filePath);
if (stats.mtime < cutoffDate) {
await fs.unlink(filePath);
logSh(`🗑️ Supprimé log ancien: ${file}`, 'INFO');
}
}
}
} catch (error) {
// Directory might not exist, that's fine
}
} catch (error) {
// Silent fail
}
}
async function cleanGoogleSheetsLogs() {
try {
const sheetsApi = await initGoogleSheets();
// Clear + remettre headers
await sheetsApi.spreadsheets.values.clear({
spreadsheetId: SHEET_ID,
range: 'Logs!A:D'
});
await sheetsApi.spreadsheets.values.update({
spreadsheetId: SHEET_ID,
range: 'Logs!A1:D1',
valueInputOption: 'RAW',
resource: {
values: [['Timestamp', 'Level', 'Message', 'Source']]
}
});
} catch (error) {
logSh('Échec nettoyage Google Sheets: ' + error.message, 'WARNING'); // Using logSh instead of console.warn
}
}
// ============= VALIDATION PRINCIPALE - IDENTIQUE =============
function validateWorkflowIntegrity(elements, generatedContent, finalXML, csvData) {
logSh('🔍 >>> VALIDATION INTÉGRITÉ WORKFLOW <<<', 'INFO'); // Using logSh instead of console.log
const errors = [];
const warnings = [];
const stats = {
elementsExtracted: elements.length,
contentGenerated: Object.keys(generatedContent).length,
tagsReplaced: 0,
tagsRemaining: 0
};
// TEST 1: Détection tags dupliqués
const duplicateCheck = detectDuplicateTags(elements);
if (duplicateCheck.hasDuplicates) {
errors.push({
type: 'DUPLICATE_TAGS',
severity: 'HIGH',
message: `Tags dupliqués détectés: ${duplicateCheck.duplicates.join(', ')}`,
impact: 'Certains contenus ne seront pas remplacés dans le XML final',
suggestion: 'Vérifier le template XML pour corriger la structure'
});
}
// TEST 2: Cohérence éléments extraits vs générés
const missingGeneration = elements.filter(el => !generatedContent[el.originalTag]);
if (missingGeneration.length > 0) {
errors.push({
type: 'MISSING_GENERATION',
severity: 'HIGH',
message: `${missingGeneration.length} éléments extraits mais non générés`,
details: missingGeneration.map(el => el.originalTag),
impact: 'Contenu incomplet dans le XML final'
});
}
// TEST 3: Tags non remplacés dans XML final
const remainingTags = (finalXML.match(/\|[^|]*\|/g) || []);
stats.tagsRemaining = remainingTags.length;
if (remainingTags.length > 0) {
errors.push({
type: 'UNREPLACED_TAGS',
severity: 'HIGH',
message: `${remainingTags.length} tags non remplacés dans le XML final`,
details: remainingTags.slice(0, 5),
impact: 'XML final contient des placeholders non remplacés'
});
}
// TEST 4: Variables CSV manquantes
const missingVars = detectMissingCSVVariables(csvData);
if (missingVars.length > 0) {
warnings.push({
type: 'MISSING_CSV_VARIABLES',
severity: 'MEDIUM',
message: `Variables CSV manquantes: ${missingVars.join(', ')}`,
impact: 'Système de génération de mots-clés automatique activé'
});
}
// TEST 5: Qualité génération IA
const generationQuality = assessGenerationQuality(generatedContent);
if (generationQuality.errorRate > 0.1) {
warnings.push({
type: 'GENERATION_QUALITY',
severity: 'MEDIUM',
message: `${(generationQuality.errorRate * 100).toFixed(1)}% d'erreurs de génération IA`,
impact: 'Qualité du contenu potentiellement dégradée'
});
}
// CALCUL STATS FINALES
stats.tagsReplaced = elements.length - remainingTags.length;
stats.successRate = stats.elementsExtracted > 0 ?
((stats.tagsReplaced / elements.length) * 100).toFixed(1) : '100';
const report = {
timestamp: new Date().toISOString(),
csvData: { mc0: csvData.mc0, t0: csvData.t0 },
stats: stats,
errors: errors,
warnings: warnings,
status: errors.length === 0 ? 'SUCCESS' : 'ERROR'
};
const logLevel = report.status === 'SUCCESS' ? 'INFO' : 'ERROR';
logSh(`✅ Validation terminée: ${report.status} (${errors.length} erreurs, ${warnings.length} warnings)`, 'INFO'); // Using logSh instead of console.log
// ENVOYER RAPPORT SI ERREURS (async en arrière-plan)
if (errors.length > 0 || warnings.length > 2) {
sendErrorReport(report).catch(err => {
logSh('Erreur envoi rapport: ' + err.message, 'ERROR'); // Using logSh instead of console.error
});
}
return report;
}
// ============= HELPERS - IDENTIQUES =============
function detectDuplicateTags(elements) {
const tagCounts = {};
const duplicates = [];
elements.forEach(element => {
const tag = element.originalTag;
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
if (tagCounts[tag] === 2) {
duplicates.push(tag);
logSh(`❌ DUPLICATE détecté: ${tag}`, 'ERROR'); // Using logSh instead of console.error
}
});
return {
hasDuplicates: duplicates.length > 0,
duplicates: duplicates,
counts: tagCounts
};
}
function detectMissingCSVVariables(csvData) {
const missing = [];
if (!csvData.mcPlus1 || csvData.mcPlus1.split(',').length < 4) {
missing.push('MC+1 (insuffisant)');
}
if (!csvData.tPlus1 || csvData.tPlus1.split(',').length < 4) {
missing.push('T+1 (insuffisant)');
}
if (!csvData.lPlus1 || csvData.lPlus1.split(',').length < 4) {
missing.push('L+1 (insuffisant)');
}
return missing;
}
function assessGenerationQuality(generatedContent) {
let errorCount = 0;
let totalCount = Object.keys(generatedContent).length;
Object.values(generatedContent).forEach(content => {
if (content && (
content.includes('[ERREUR') ||
content.includes('ERROR') ||
content.length < 10
)) {
errorCount++;
}
});
return {
errorRate: totalCount > 0 ? errorCount / totalCount : 0,
totalGenerated: totalCount,
errorsFound: errorCount
};
}
// 🔄 NODE.JS : Email avec nodemailer (remplace MailApp)
async function sendErrorReport(report) {
try {
logSh('📧 Envoi rapport d\'erreur par email...', 'INFO'); // Using logSh instead of console.log
// Lazy load nodemailer seulement quand nécessaire
if (!nodemailer) {
nodemailer = require('nodemailer');
}
// Configuration nodemailer (Gmail par exemple)
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER, // 'your-email@gmail.com'
pass: process.env.EMAIL_APP_PASSWORD // App password Google
}
});
const subject = `Erreur Workflow SEO Node.js - ${report.status} - ${report.csvData.mc0}`;
const htmlBody = createHTMLReport(report);
const mailOptions = {
from: process.env.EMAIL_USER,
to: 'alexistrouve.pro@gmail.com',
subject: subject,
html: htmlBody,
attachments: [{
filename: `error-report-${Date.now()}.json`,
content: JSON.stringify(report, null, 2),
contentType: 'application/json'
}]
};
await transporter.sendMail(mailOptions);
logSh('✅ Rapport d\'erreur envoyé par email', 'INFO'); // Using logSh instead of console.log
} catch (error) {
logSh(`❌ Échec envoi email: ${error.message}`, 'ERROR'); // Using logSh instead of console.error
}
}
// ============= HTML REPORT - IDENTIQUE =============
function createHTMLReport(report) {
const statusColor = report.status === 'SUCCESS' ? '#28a745' : '#dc3545';
let html = `
<div style="font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto;">
<h1 style="color: ${statusColor};">Rapport Workflow SEO Automatisé (Node.js)</h1>
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Résumé Exécutif</h2>
<p><strong>Statut:</strong> <span style="color: ${statusColor};">${report.status}</span></p>
<p><strong>Article:</strong> ${report.csvData.t0}</p>
<p><strong>Mot-clé:</strong> ${report.csvData.mc0}</p>
<p><strong>Taux de réussite:</strong> ${report.stats.successRate}%</p>
<p><strong>Timestamp:</strong> ${report.timestamp}</p>
<p><strong>Plateforme:</strong> Node.js Server</p>
</div>`;
if (report.errors.length > 0) {
html += `<div style="background: #f8d7da; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Erreurs Critiques (${report.errors.length})</h2>`;
report.errors.forEach((error, i) => {
html += `
<div style="margin: 10px 0; padding: 10px; border-left: 3px solid #dc3545;">
<h4>${i + 1}. ${error.type}</h4>
<p><strong>Message:</strong> ${error.message}</p>
<p><strong>Impact:</strong> ${error.impact}</p>
${error.suggestion ? `<p><strong>Solution:</strong> ${error.suggestion}</p>` : ''}
</div>`;
});
html += `</div>`;
}
if (report.warnings.length > 0) {
html += `<div style="background: #fff3cd; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Avertissements (${report.warnings.length})</h2>`;
report.warnings.forEach((warning, i) => {
html += `
<div style="margin: 10px 0; padding: 10px; border-left: 3px solid #ffc107;">
<h4>${i + 1}. ${warning.type}</h4>
<p>${warning.message}</p>
</div>`;
});
html += `</div>`;
}
html += `
<div style="background: #e9ecef; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Statistiques Détaillées</h2>
<ul>
<li>Éléments extraits: ${report.stats.elementsExtracted}</li>
<li>Contenus générés: ${report.stats.contentGenerated}</li>
<li>Tags remplacés: ${report.stats.tagsReplaced}</li>
<li>Tags restants: ${report.stats.tagsRemaining}</li>
</ul>
</div>
<div style="background: #d1ecf1; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Informations Système</h2>
<ul>
<li>Plateforme: Node.js</li>
<li>Version: ${process.version}</li>
<li>Mémoire: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB</li>
<li>Uptime: ${Math.round(process.uptime())}s</li>
</ul>
</div>
</div>`;
return html;
}
// 🔄 NODE.JS EXPORTS
module.exports = {
logSh,
cleanLogSheet,
validateWorkflowIntegrity,
detectDuplicateTags,
detectMissingCSVVariables,
assessGenerationQuality,
sendErrorReport,
createHTMLReport,
initWebSocketServer
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/trace.js │
└────────────────────────────────────────────────────────────────────┘
*/
// lib/trace.js
const { AsyncLocalStorage } = require('node:async_hooks');
const { randomUUID } = require('node:crypto');
const { logSh } = require('./ErrorReporting');
const als = new AsyncLocalStorage();
function now() { return performance.now(); }
function dur(ms) {
if (ms < 1e3) return `${ms.toFixed(1)}ms`;
const s = ms / 1e3;
return s < 60 ? `${s.toFixed(2)}s` : `${(s/60).toFixed(2)}m`;
}
class Span {
constructor({ name, parent = null, attrs = {} }) {
this.id = randomUUID();
this.name = name;
this.parent = parent;
this.children = [];
this.attrs = attrs;
this.start = now();
this.end = null;
this.status = 'ok';
this.error = null;
}
pathNames() {
const names = [];
let cur = this;
while (cur) { names.unshift(cur.name); cur = cur.parent; }
return names.join(' > ');
}
finish() { this.end = now(); }
duration() { return (this.end ?? now()) - this.start; }
}
class Tracer {
constructor() {
this.rootSpans = [];
}
current() { return als.getStore(); }
async startSpan(name, attrs = {}) {
const parent = this.current();
const span = new Span({ name, parent, attrs });
if (parent) parent.children.push(span);
else this.rootSpans.push(span);
// Formater les paramètres pour affichage
const paramsStr = this.formatParams(attrs);
await logSh(`${name}${paramsStr}`, 'TRACE');
return span;
}
async run(name, fn, attrs = {}) {
const parent = this.current();
const span = await this.startSpan(name, attrs);
return await als.run(span, async () => {
try {
const res = await fn();
span.finish();
const paramsStr = this.formatParams(span.attrs);
await logSh(`${name}${paramsStr} (${dur(span.duration())})`, 'TRACE');
return res;
} catch (err) {
span.status = 'error';
span.error = { message: err?.message, stack: err?.stack };
span.finish();
const paramsStr = this.formatParams(span.attrs);
await logSh(`${name}${paramsStr} FAILED (${dur(span.duration())})`, 'ERROR');
await logSh(`Stack trace: ${span.error.message}`, 'ERROR');
if (span.error.stack) {
const stackLines = span.error.stack.split('\n').slice(1, 6); // Première 5 lignes du stack
for (const line of stackLines) {
await logSh(` ${line.trim()}`, 'ERROR');
}
}
throw err;
}
});
}
async event(msg, extra = {}) {
const span = this.current();
const data = { trace: true, evt: 'span.event', ...extra };
if (span) {
data.span = span.id;
data.path = span.pathNames();
data.since_ms = +( (now() - span.start).toFixed(1) );
}
await logSh(`${msg}`, 'TRACE');
}
async annotate(fields = {}) {
const span = this.current();
if (span) Object.assign(span.attrs, fields);
await logSh('… annotate', 'TRACE');
}
formatParams(attrs = {}) {
const params = Object.entries(attrs)
.filter(([key, value]) => value !== undefined && value !== null)
.map(([key, value]) => {
// Tronquer les valeurs trop longues
const strValue = String(value);
const truncated = strValue.length > 50 ? strValue.substring(0, 47) + '...' : strValue;
return `${key}=${truncated}`;
});
return params.length > 0 ? `(${params.join(', ')})` : '';
}
printSummary() {
const lines = [];
const draw = (node, depth = 0) => {
const pad = ' '.repeat(depth);
const icon = node.status === 'error' ? '✖' : '✔';
lines.push(`${pad}${icon} ${node.name} (${dur(node.duration())})`);
if (Object.keys(node.attrs ?? {}).length) {
lines.push(`${pad} attrs: ${JSON.stringify(node.attrs)}`);
}
for (const ch of node.children) draw(ch, depth + 1);
if (node.status === 'error' && node.error?.message) {
lines.push(`${pad} error: ${node.error.message}`);
if (node.error.stack) {
const stackLines = String(node.error.stack || '').split('\n').slice(1, 4).map(s => s.trim());
if (stackLines.length) {
lines.push(`${pad} stack:`);
stackLines.forEach(line => {
if (line) lines.push(`${pad} ${line}`);
});
}
}
}
};
for (const r of this.rootSpans) draw(r, 0);
const summary = lines.join('\n');
logSh(`\n—— TRACE SUMMARY ——\n${summary}\n—— END TRACE ——`, 'INFO');
return summary;
}
}
const tracer = new Tracer();
module.exports = {
Span,
Tracer,
tracer
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/BrainConfig.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: BrainConfig.js - Version Node.js
// Description: Configuration cerveau + sélection personnalité IA
// ========================================
require('dotenv').config();
const axios = require('axios');
const fs = require('fs').promises;
const path = require('path');
// Import de la fonction logSh (assumant qu'elle existe dans votre projet Node.js)
const { logSh } = require('./ErrorReporting');
// Configuration
const CONFIG = {
openai: {
apiKey: process.env.OPENAI_API_KEY || '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',
endpoint: 'https://api.openai.com/v1/chat/completions'
},
dataSource: {
type: process.env.DATA_SOURCE_TYPE || 'json', // 'json', 'csv', 'database'
instructionsPath: './data/instructions.json',
personalitiesPath: './data/personalities.json'
}
};
/**
* FONCTION PRINCIPALE - Équivalent getBrainConfig()
* @param {number|object} data - Numéro de ligne ou données directes
* @returns {object} Configuration avec données CSV + personnalité
*/
async function getBrainConfig(data) {
try {
logSh("🧠 Début getBrainConfig Node.js", "INFO");
// 1. RÉCUPÉRER LES DONNÉES CSV
let csvData;
if (typeof data === 'number') {
// Numéro de ligne fourni - lire depuis fichier
csvData = await readInstructionsData(data);
} else if (typeof data === 'object' && data.rowNumber) {
csvData = await readInstructionsData(data.rowNumber);
} else {
// Données déjà fournies
csvData = data;
}
logSh(`✅ CSV récupéré: ${csvData.mc0}`, "INFO");
// 2. RÉCUPÉRER LES PERSONNALITÉS
const personalities = await getPersonalities();
logSh(`${personalities.length} personnalités chargées`, "INFO");
// 3. SÉLECTIONNER LA MEILLEURE PERSONNALITÉ VIA IA
const selectedPersonality = await selectPersonalityWithAI(
csvData.mc0,
csvData.t0,
personalities
);
logSh(`✅ Personnalité sélectionnée: ${selectedPersonality.nom}`, "INFO");
return {
success: true,
data: {
...csvData,
personality: selectedPersonality,
timestamp: new Date().toISOString()
}
};
} catch (error) {
logSh(`❌ Erreur getBrainConfig: ${error.message}`, "ERROR");
return {
success: false,
error: error.message
};
}
}
/**
* LIRE DONNÉES INSTRUCTIONS depuis Google Sheets DIRECTEMENT
* @param {number} rowNumber - Numéro de ligne (2 = première ligne de données)
* @returns {object} Données CSV parsées
*/
async function readInstructionsData(rowNumber = 2) {
try {
logSh(`📊 Lecture Google Sheet ligne ${rowNumber}...`, 'INFO');
// NOUVEAU : Lecture directe depuis Google Sheets
const { google } = require('googleapis');
// Configuration auth Google Sheets - FORCE utilisation fichier JSON pour éviter problème TLS
const keyFilePath = path.join(__dirname, '..', 'seo-generator-470715-85d4a971c1af.json');
const auth = new google.auth.GoogleAuth({
keyFile: keyFilePath,
scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly']
});
logSh('🔑 Utilisation fichier JSON pour contourner problème TLS OAuth', 'INFO');
const sheets = google.sheets({ version: 'v4', auth });
const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
// Récupérer la ligne spécifique (A à I au minimum)
const response = await sheets.spreadsheets.values.get({
spreadsheetId: SHEET_ID,
range: `Instructions!A${rowNumber}:I${rowNumber}` // Ligne spécifique A-I
});
if (!response.data.values || response.data.values.length === 0) {
throw new Error(`Ligne ${rowNumber} non trouvée dans Google Sheet`);
}
const row = response.data.values[0];
logSh(`✅ Ligne ${rowNumber} récupérée: ${row.length} colonnes`, 'INFO');
const xmlTemplateValue = row[8] || '';
let xmlTemplate = xmlTemplateValue;
let xmlFileName = null;
// Si c'est un nom de fichier, garder le nom ET utiliser un template par défaut
if (xmlTemplateValue && xmlTemplateValue.endsWith('.xml') && xmlTemplateValue.length < 100) {
logSh(`🔧 XML filename detected (${xmlTemplateValue}), keeping filename for Digital Ocean`, 'INFO');
xmlFileName = xmlTemplateValue; // Garder le nom du fichier pour Digital Ocean
xmlTemplate = createDefaultXMLTemplate(); // Template par défaut pour le processing
}
return {
rowNumber: rowNumber,
slug: row[0] || '', // Colonne A
t0: row[1] || '', // Colonne B
mc0: row[2] || '', // Colonne C
tMinus1: row[3] || '', // Colonne D
lMinus1: row[4] || '', // Colonne E
mcPlus1: row[5] || '', // Colonne F
tPlus1: row[6] || '', // Colonne G
lPlus1: row[7] || '', // Colonne H
xmlTemplate: xmlTemplate, // XML template pour processing
xmlFileName: xmlFileName // Nom fichier pour Digital Ocean (si applicable)
};
} catch (error) {
logSh(`❌ Erreur lecture Google Sheet: ${error.message}`, "ERROR");
throw error;
}
}
/**
* RÉCUPÉRER PERSONNALITÉS depuis l'onglet "Personnalites" du Google Sheet
* @returns {Array} Liste des personnalités disponibles
*/
async function getPersonalities() {
try {
logSh('📊 Lecture personnalités depuis Google Sheet (onglet Personnalites)...', 'INFO');
// Configuration auth Google Sheets - FORCE utilisation fichier JSON pour éviter problème TLS
const { google } = require('googleapis');
const keyFilePath = path.join(__dirname, '..', 'seo-generator-470715-85d4a971c1af.json');
const auth = new google.auth.GoogleAuth({
keyFile: keyFilePath,
scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly']
});
logSh('🔑 Utilisation fichier JSON pour contourner problème TLS OAuth (personnalités)', 'INFO');
const sheets = google.sheets({ version: 'v4', auth });
const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
// Récupérer toutes les personnalités (après la ligne d'en-tête)
const response = await sheets.spreadsheets.values.get({
spreadsheetId: SHEET_ID,
range: 'Personnalites!A2:O' // Colonnes A à O pour inclure les nouvelles colonnes IA
});
if (!response.data.values || response.data.values.length === 0) {
throw new Error('Aucune personnalité trouvée dans l\'onglet Personnalites');
}
const personalities = [];
// Traiter chaque ligne de personnalité
response.data.values.forEach((row, index) => {
if (row[0] && row[0].toString().trim() !== '') { // Si nom existe (colonne A)
const personality = {
nom: row[0]?.toString().trim() || '',
description: row[1]?.toString().trim() || 'Expert généraliste',
style: row[2]?.toString().trim() || 'professionnel',
// Configuration avancée depuis colonnes Google Sheet
motsClesSecteurs: parseCSVField(row[3]),
vocabulairePref: parseCSVField(row[4]),
connecteursPref: parseCSVField(row[5]),
erreursTypiques: parseCSVField(row[6]),
longueurPhrases: row[7]?.toString().trim() || 'moyennes',
niveauTechnique: row[8]?.toString().trim() || 'moyen',
ctaStyle: parseCSVField(row[9]),
defautsSimules: parseCSVField(row[10]),
// NOUVEAU: Configuration IA par étape depuis Google Sheets (colonnes L-O)
aiEtape1Base: row[11]?.toString().trim().toLowerCase() || '',
aiEtape2Technique: row[12]?.toString().trim().toLowerCase() || '',
aiEtape3Transitions: row[13]?.toString().trim().toLowerCase() || '',
aiEtape4Style: row[14]?.toString().trim().toLowerCase() || '',
// Backward compatibility
motsCles: parseCSVField(row[3] || '') // Utilise motsClesSecteurs
};
personalities.push(personality);
logSh(`✓ Personnalité chargée: ${personality.nom} (${personality.style})`, 'DEBUG');
}
});
logSh(`📊 ${personalities.length} personnalités chargées depuis Google Sheet`, "INFO");
return personalities;
} catch (error) {
logSh(`❌ ÉCHEC: Impossible de récupérer les personnalités Google Sheets - ${error.message}`, "ERROR");
throw new Error(`FATAL: Personnalités Google Sheets inaccessibles - arrêt du workflow: ${error.message}`);
}
}
/**
* PARSER CHAMP CSV - Helper function
* @param {string} field - Champ à parser
* @returns {Array} Liste des éléments parsés
*/
function parseCSVField(field) {
if (!field || field.toString().trim() === '') return [];
return field.toString()
.split(',')
.map(item => item.trim())
.filter(item => item.length > 0);
}
/**
* Sélectionner un sous-ensemble aléatoire de personnalités
* @param {Array} allPersonalities - Liste complète des personnalités
* @param {number} percentage - Pourcentage à garder (0.6 = 60%)
* @returns {Array} Sous-ensemble aléatoire
*/
function selectRandomPersonalities(allPersonalities, percentage = 0.6) {
const count = Math.ceil(allPersonalities.length * percentage);
// Mélanger avec Fisher-Yates shuffle (meilleur que sort())
const shuffled = [...allPersonalities];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled.slice(0, count);
}
/**
* NOUVELLE FONCTION: Sélection de 4 personnalités complémentaires pour le pipeline multi-AI
* @param {string} mc0 - Mot-clé principal
* @param {string} t0 - Titre principal
* @param {Array} personalities - Liste des personnalités
* @returns {Array} 4 personnalités sélectionnées pour chaque étape
*/
async function selectMultiplePersonalitiesWithAI(mc0, t0, personalities) {
try {
logSh(`🎭 Sélection MULTI-personnalités IA pour: ${mc0}`, "INFO");
// Sélection aléatoire de 80% des personnalités (plus large pour 4 choix)
const randomPersonalities = selectRandomPersonalities(personalities, 0.8);
const totalCount = personalities.length;
const selectedCount = randomPersonalities.length;
logSh(`🎲 Pool aléatoire: ${selectedCount}/${totalCount} personnalités disponibles`, "DEBUG");
logSh(`📋 Personnalités dans le pool: ${randomPersonalities.map(p => p.nom).join(', ')}`, "DEBUG");
const prompt = `Choisis 4 personnalités COMPLÉMENTAIRES pour générer du contenu sur "${mc0}":
OBJECTIF: Créer une équipe de 4 rédacteurs avec styles différents mais cohérents
PERSONNALITÉS DISPONIBLES:
${randomPersonalities.map(p => `- ${p.nom}: ${p.description} (Style: ${p.style})`).join('\n')}
RÔLES À ATTRIBUER:
1. GÉNÉRATEUR BASE: Personnalité technique/experte pour la génération initiale
2. ENHANCER TECHNIQUE: Personnalité commerciale/précise pour améliorer les termes techniques
3. FLUIDITÉ: Personnalité créative/littéraire pour améliorer les transitions
4. STYLE FINAL: Personnalité terrain/accessible pour le style final
CRITÈRES:
- 4 personnalités aux styles DIFFÉRENTS mais complémentaires
- Adapté au secteur: ${mc0}
- Variabilité maximale pour anti-détection
- Éviter les doublons de style
FORMAT DE RÉPONSE (EXACTEMENT 4 noms séparés par des virgules):
Nom1, Nom2, Nom3, Nom4`;
const requestData = {
model: "gpt-4o-mini",
messages: [{"role": "user", "content": prompt}],
max_tokens: 100,
temperature: 1.0
};
const response = await axios.post(CONFIG.openai.endpoint, requestData, {
headers: {
'Authorization': `Bearer ${CONFIG.openai.apiKey}`,
'Content-Type': 'application/json'
},
timeout: 300000
});
const selectedNames = response.data.choices[0].message.content.trim()
.split(',')
.map(name => name.trim());
logSh(`🔍 Noms retournés par IA: ${selectedNames.join(', ')}`, "DEBUG");
// Mapper aux vraies personnalités
const selectedPersonalities = [];
selectedNames.forEach(name => {
const personality = randomPersonalities.find(p => p.nom === name);
if (personality) {
selectedPersonalities.push(personality);
}
});
// Compléter si pas assez de personnalités trouvées (sécurité)
while (selectedPersonalities.length < 4 && randomPersonalities.length > selectedPersonalities.length) {
const remaining = randomPersonalities.filter(p =>
!selectedPersonalities.some(selected => selected.nom === p.nom)
);
if (remaining.length > 0) {
const randomIndex = Math.floor(Math.random() * remaining.length);
selectedPersonalities.push(remaining[randomIndex]);
} else {
break;
}
}
// Garantir exactement 4 personnalités
const final4Personalities = selectedPersonalities.slice(0, 4);
logSh(`✅ Équipe de 4 personnalités sélectionnée:`, "INFO");
final4Personalities.forEach((p, index) => {
const roles = ['BASE', 'TECHNIQUE', 'FLUIDITÉ', 'STYLE'];
logSh(` ${index + 1}. ${roles[index]}: ${p.nom} (${p.style})`, "INFO");
});
return final4Personalities;
} catch (error) {
logSh(`❌ FATAL: Sélection multi-personnalités échouée: ${error.message}`, "ERROR");
throw new Error(`FATAL: Sélection multi-personnalités IA impossible - arrêt du workflow: ${error.message}`);
}
}
/**
* FONCTION LEGACY: Sélection personnalité unique (maintenue pour compatibilité)
* @param {string} mc0 - Mot-clé principal
* @param {string} t0 - Titre principal
* @param {Array} personalities - Liste des personnalités
* @returns {object} Personnalité sélectionnée
*/
async function selectPersonalityWithAI(mc0, t0, personalities) {
try {
logSh(`🤖 Sélection personnalité IA UNIQUE pour: ${mc0}`, "DEBUG");
// Appeler la fonction multi et prendre seulement la première
const multiPersonalities = await selectMultiplePersonalitiesWithAI(mc0, t0, personalities);
const selectedPersonality = multiPersonalities[0];
logSh(`✅ Personnalité IA sélectionnée (mode legacy): ${selectedPersonality.nom}`, "INFO");
return selectedPersonality;
} catch (error) {
logSh(`❌ FATAL: Sélection personnalité par IA échouée: ${error.message}`, "ERROR");
throw new Error(`FATAL: Sélection personnalité IA inaccessible - arrêt du workflow: ${error.message}`);
}
}
/**
* CRÉER TEMPLATE XML PAR DÉFAUT quand colonne I contient un nom de fichier
* Utilise les données CSV disponibles pour créer un template robuste
*/
function createDefaultXMLTemplate() {
return `<?xml version="1.0" encoding="UTF-8"?>
<article>
<header>
<h1>|Titre_Principal{{T0}}{Rédige un titre H1 accrocheur de maximum 10 mots pour {{MC0}}. Style {{personality.style}}}|</h1>
<intro>|Introduction{{MC0}}{Rédige une introduction engageante de 2-3 phrases sur {{MC0}}. Ton {{personality.style}}, utilise {{personality.vocabulairePref}}}|</intro>
</header>
<main>
<section class="primary">
<h2>|Titre_H2_1{{MC+1_1}}{Crée un titre H2 informatif sur {{MC+1_1}}. Style {{personality.style}}}|</h2>
<p>|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}}}|</p>
</section>
<section class="secondary">
<h2>|Titre_H2_2{{MC+1_2}}{Titre H2 pour {{MC+1_2}}. Mets en valeur les points forts. Ton {{personality.style}}}|</h2>
<p>|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}}}|</p>
</section>
<section class="benefits">
<h2>|Titre_H2_3{{MC+1_3}}{Titre H2 sur les bénéfices de {{MC+1_3}}. Accrocheur et informatif}|</h2>
<p>|Paragraphe_3{{MC+1_3}}{Explique en 4-5 phrases les avantages de {{MC+1_3}} pour {{MC0}}. Ton {{personality.style}}}|</p>
</section>
</main>
<aside class="faq">
<h2>|FAQ_Titre{Titre de section FAQ accrocheur sur {{MC0}}}|</h2>
<div class="faq-item">
<h3>|Faq_q_1{{MC+1_1}}{Question fréquente sur {{MC+1_1}} et {{MC0}}}|</h3>
<p>|Faq_a_1{{MC+1_1}}{Réponse claire et précise. 2-3 phrases. Ton {{personality.style}}}|</p>
</div>
<div class="faq-item">
<h3>|Faq_q_2{{MC+1_2}}{Question pratique sur {{MC+1_2}} en lien avec {{MC0}}}|</h3>
<p>|Faq_a_2{{MC+1_2}}{Réponse détaillée et utile. 2-3 phrases explicatives. Ton {{personality.style}}}|</p>
</div>
<div class="faq-item">
<h3>|Faq_q_3{{MC+1_3}}{Question sur {{MC+1_3}} que se posent les clients}|</h3>
<p>|Faq_a_3{{MC+1_3}}{Réponse complète qui rassure et informe. 2-3 phrases. Ton {{personality.style}}}|</p>
</div>
</aside>
<footer>
<p>|Conclusion{{MC0}}{Conclusion engageante de 2 phrases sur {{MC0}}. Appel à l'action subtil. Ton {{personality.style}}}|</p>
</footer>
</article>`;
}
/**
* CRÉER FICHIERS DE DONNÉES D'EXEMPLE
* Fonction utilitaire pour initialiser les fichiers JSON
*/
async function createSampleDataFiles() {
try {
// Créer répertoire data s'il n'existe pas
await fs.mkdir('./data', { recursive: true });
// Exemple instructions.json
const sampleInstructions = [
{
slug: "plaque-test",
t0: "Plaque test signalétique",
mc0: "plaque signalétique",
"t-1": "Signalétique",
"l-1": "/signaletique/",
"mc+1": "plaque dibond, plaque aluminium, plaque PVC",
"t+1": "Plaque dibond, Plaque alu, Plaque PVC",
"l+1": "/plaque-dibond/, /plaque-aluminium/, /plaque-pvc/",
xmlFileName: "template-plaque.xml"
}
];
// Exemple personalities.json
const samplePersonalities = [
{
nom: "Marc",
description: "Expert technique en signalétique",
style: "professionnel et précis",
motsClesSecteurs: "technique,dibond,aluminium,impression",
vocabulairePref: "précision,qualité,expertise,performance",
connecteursPref: "par ailleurs,en effet,notamment,cependant",
erreursTypiques: "accord_proximite,repetition_legere",
longueurPhrases: "moyennes",
niveauTechnique: "élevé",
ctaStyle: "découvrir,choisir,commander",
defautsSimules: "fatigue_cognitive,hesitation_technique"
},
{
nom: "Sophie",
description: "Passionnée de décoration et design",
style: "familier et chaleureux",
motsClesSecteurs: "décoration,design,esthétique,tendances",
vocabulairePref: "joli,magnifique,tendance,style",
connecteursPref: "du coup,en fait,sinon,au fait",
erreursTypiques: "familiarite_excessive,expression_populaire",
longueurPhrases: "courtes",
niveauTechnique: "moyen",
ctaStyle: "craquer,adopter,foncer",
defautsSimules: "enthousiasme_variable,anecdote_personnelle"
}
];
// Écrire les fichiers
await fs.writeFile('./data/instructions.json', JSON.stringify(sampleInstructions, null, 2));
await fs.writeFile('./data/personalities.json', JSON.stringify(samplePersonalities, null, 2));
logSh('✅ Fichiers de données d\'exemple créés dans ./data/', "INFO");
} catch (error) {
logSh(`❌ Erreur création fichiers exemple: ${error.message}`, "ERROR");
}
}
// ============= EXPORTS NODE.JS =============
module.exports = {
getBrainConfig,
getPersonalities,
selectPersonalityWithAI,
selectMultiplePersonalitiesWithAI, // NOUVEAU: Export de la fonction multi-personnalités
selectRandomPersonalities,
parseCSVField,
readInstructionsData,
createSampleDataFiles,
createDefaultXMLTemplate,
CONFIG
};
// ============= TEST RAPIDE SI LANCÉ DIRECTEMENT =============
if (require.main === module) {
(async () => {
try {
logSh('🧪 Test BrainConfig Node.js...', "INFO");
// Créer fichiers exemple si nécessaire
try {
await fs.access('./data/instructions.json');
} catch {
await createSampleDataFiles();
}
// Test de la fonction principale
const result = await getBrainConfig(2);
if (result.success) {
logSh(`✅ Test réussi: ${result.data.personality.nom} pour ${result.data.mc0}`, "INFO");
} else {
logSh(`❌ Test échoué: ${result.error}`, "ERROR");
}
} catch (error) {
logSh(`❌ Erreur test: ${error.message}`, "ERROR");
}
})();
}
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/LLMManager.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: LLMManager.js
// Description: Hub central pour tous les appels LLM (Version Node.js)
// Support: Claude, OpenAI, Gemini, Deepseek, Moonshot, Mistral
// ========================================
const fetch = globalThis.fetch.bind(globalThis);
const { logSh } = require('./ErrorReporting');
// Charger les variables d'environnement
require('dotenv').config();
// ============= CONFIGURATION CENTRALISÉE =============
const LLM_CONFIG = {
openai: {
apiKey: process.env.OPENAI_API_KEY,
endpoint: 'https://api.openai.com/v1/chat/completions',
model: 'gpt-4o-mini',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
timeout: 300000, // 5 minutes
retries: 3
},
claude: {
apiKey: process.env.ANTHROPIC_API_KEY,
endpoint: 'https://api.anthropic.com/v1/messages',
model: 'claude-sonnet-4-20250514',
headers: {
'x-api-key': '{API_KEY}',
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01'
},
temperature: 0.7,
maxTokens: 6000,
timeout: 300000, // 5 minutes
retries: 6
},
gemini: {
apiKey: process.env.GOOGLE_API_KEY,
endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent',
model: 'gemini-2.5-flash',
headers: {
'Content-Type': 'application/json'
},
temperature: 0.7,
maxTokens: 6000,
timeout: 300000, // 5 minutes
retries: 3
},
deepseek: {
apiKey: process.env.DEEPSEEK_API_KEY,
endpoint: 'https://api.deepseek.com/v1/chat/completions',
model: 'deepseek-chat',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
timeout: 300000, // 5 minutes
retries: 3
},
moonshot: {
apiKey: process.env.MOONSHOT_API_KEY,
endpoint: 'https://api.moonshot.ai/v1/chat/completions',
model: 'moonshot-v1-32k',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
timeout: 300000, // 5 minutes
retries: 3
},
mistral: {
apiKey: process.env.MISTRAL_API_KEY,
endpoint: 'https://api.mistral.ai/v1/chat/completions',
model: 'mistral-small-latest',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
max_tokens: 5000,
temperature: 0.7,
timeout: 300000, // 5 minutes
retries: 3
}
};
// Alias pour compatibilité avec le code existant
LLM_CONFIG.gpt4 = LLM_CONFIG.openai;
// ============= HELPER FUNCTIONS =============
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// ============= INTERFACE UNIVERSELLE =============
/**
* Fonction principale pour appeler n'importe quel LLM
* @param {string} llmProvider - claude|openai|gemini|deepseek|moonshot|mistral
* @param {string} prompt - Le prompt à envoyer
* @param {object} options - Options personnalisées (température, tokens, etc.)
* @param {object} personality - Personnalité pour contexte système
* @returns {Promise<string>} - Réponse générée
*/
async function callLLM(llmProvider, prompt, options = {}, personality = null) {
const startTime = Date.now();
try {
// Vérifier si le provider existe
if (!LLM_CONFIG[llmProvider]) {
throw new Error(`Provider LLM inconnu: ${llmProvider}`);
}
// Vérifier si l'API key est configurée
const config = LLM_CONFIG[llmProvider];
if (!config.apiKey || config.apiKey.startsWith('VOTRE_CLE_')) {
throw new Error(`Clé API manquante pour ${llmProvider}`);
}
logSh(`🤖 Appel LLM: ${llmProvider.toUpperCase()} (${config.model}) | Personnalité: ${personality?.nom || 'aucune'}`, 'DEBUG');
// 📢 AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA
logSh(`\n🔍 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT');
logSh(prompt, 'PROMPT');
// 📤 LOG LLM REQUEST COMPLET
logSh(`📤 LLM REQUEST [${llmProvider.toUpperCase()}] (${config.model}) | Personnalité: ${personality?.nom || 'AUCUNE'}`, 'LLM');
logSh(prompt, 'LLM');
// Préparer la requête selon le provider
const requestData = buildRequestData(llmProvider, prompt, options, personality);
// Effectuer l'appel avec retry logic
const response = await callWithRetry(llmProvider, requestData, config);
// Parser la réponse selon le format du provider
const content = parseResponse(llmProvider, response);
// 📥 LOG LLM RESPONSE COMPLET
logSh(`📥 LLM RESPONSE [${llmProvider.toUpperCase()}] (${config.model}) | Durée: ${Date.now() - startTime}ms`, 'LLM');
logSh(content, 'LLM');
const duration = Date.now() - startTime;
logSh(`${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms`, 'INFO');
// Enregistrer les stats d'usage
await recordUsageStats(llmProvider, prompt.length, content.length, duration);
return content;
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ Erreur ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}): ${error.toString()}`, 'ERROR');
// Enregistrer l'échec
await recordUsageStats(llmProvider, prompt.length, 0, duration, error.toString());
throw error;
}
}
// ============= CONSTRUCTION DES REQUÊTES =============
function buildRequestData(provider, prompt, options, personality) {
const config = LLM_CONFIG[provider];
const temperature = options.temperature || config.temperature;
const maxTokens = options.maxTokens || config.maxTokens;
// Construire le système prompt si personnalité fournie
const systemPrompt = personality ?
`Tu es ${personality.nom}. ${personality.description}. Style: ${personality.style}` :
'Tu es un assistant expert.';
switch (provider) {
case 'openai':
case 'gpt4':
case 'deepseek':
case 'moonshot':
case 'mistral':
return {
model: config.model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
],
max_tokens: maxTokens,
temperature: temperature,
stream: false
};
case 'claude':
return {
model: config.model,
max_tokens: maxTokens,
temperature: temperature,
system: systemPrompt,
messages: [
{ role: 'user', content: prompt }
]
};
case 'gemini':
return {
contents: [{
parts: [{
text: `${systemPrompt}\n\n${prompt}`
}]
}],
generationConfig: {
temperature: temperature,
maxOutputTokens: maxTokens
}
};
default:
throw new Error(`Format de requête non supporté pour ${provider}`);
}
}
// ============= APPELS AVEC RETRY =============
async function callWithRetry(provider, requestData, config) {
let lastError;
for (let attempt = 1; attempt <= config.retries; attempt++) {
try {
logSh(`🔄 Tentative ${attempt}/${config.retries} pour ${provider.toUpperCase()}`, 'DEBUG');
// Préparer les headers avec la clé API
const headers = {};
Object.keys(config.headers).forEach(key => {
headers[key] = config.headers[key].replace('{API_KEY}', config.apiKey);
});
// URL avec clé API pour Gemini (cas spécial)
let url = config.endpoint;
if (provider === 'gemini') {
url += `?key=${config.apiKey}`;
}
const options = {
method: 'POST',
headers: headers,
body: JSON.stringify(requestData),
timeout: config.timeout
};
const response = await fetch(url, options);
const responseText = await response.text();
if (response.ok) {
return JSON.parse(responseText);
} else if (response.status === 429) {
// Rate limiting - attendre plus longtemps
const waitTime = Math.pow(2, attempt) * 1000; // Exponential backoff
logSh(`⏳ Rate limit ${provider.toUpperCase()}, attente ${waitTime}ms`, 'WARNING');
await sleep(waitTime);
continue;
} else {
throw new Error(`HTTP ${response.status}: ${responseText}`);
}
} catch (error) {
lastError = error;
if (attempt < config.retries) {
const waitTime = 1000 * attempt;
logSh(`⚠ Erreur tentative ${attempt}: ${error.toString()}, retry dans ${waitTime}ms`, 'WARNING');
await sleep(waitTime);
}
}
}
throw new Error(`Échec après ${config.retries} tentatives: ${lastError.toString()}`);
}
// ============= PARSING DES RÉPONSES =============
function parseResponse(provider, responseData) {
try {
switch (provider) {
case 'openai':
case 'gpt4':
case 'deepseek':
case 'moonshot':
case 'mistral':
return responseData.choices[0].message.content.trim();
case 'claude':
return responseData.content[0].text.trim();
case 'gemini':
const candidate = responseData.candidates[0];
// Vérifications multiples pour Gemini 2.5
if (candidate && candidate.content && candidate.content.parts && candidate.content.parts[0] && candidate.content.parts[0].text) {
return candidate.content.parts[0].text.trim();
} else if (candidate && candidate.text) {
return candidate.text.trim();
} else if (candidate && candidate.content && candidate.content.text) {
return candidate.content.text.trim();
} else {
// Debug : logger la structure complète
logSh('Gemini structure complète: ' + JSON.stringify(responseData), 'DEBUG');
return '[Gemini: pas de texte généré - problème modèle]';
}
default:
throw new Error(`Parser non supporté pour ${provider}`);
}
} catch (error) {
logSh(`❌ Erreur parsing ${provider}: ${error.toString()}`, 'ERROR');
logSh(`Response brute: ${JSON.stringify(responseData)}`, 'DEBUG');
throw new Error(`Impossible de parser la réponse ${provider}: ${error.toString()}`);
}
}
// ============= GESTION DES STATISTIQUES =============
async function recordUsageStats(provider, promptTokens, responseTokens, duration, error = null) {
try {
// TODO: Adapter selon votre système de stockage Node.js
// Peut être une base de données, un fichier, MongoDB, etc.
const statsData = {
timestamp: new Date(),
provider: provider,
model: LLM_CONFIG[provider].model,
promptTokens: promptTokens,
responseTokens: responseTokens,
duration: duration,
error: error || ''
};
// Exemple: log vers console ou fichier
logSh(`📊 Stats: ${JSON.stringify(statsData)}`, 'DEBUG');
// TODO: Implémenter sauvegarde réelle (DB, fichier, etc.)
} catch (statsError) {
// Ne pas faire planter le workflow si les stats échouent
logSh(`⚠ Erreur enregistrement stats: ${statsError.toString()}`, 'WARNING');
}
}
// ============= FONCTIONS UTILITAIRES =============
/**
* Tester la connectivité de tous les LLMs
*/
async function testAllLLMs() {
const testPrompt = "Dis bonjour en 5 mots maximum.";
const results = {};
const allProviders = Object.keys(LLM_CONFIG);
for (const provider of allProviders) {
try {
logSh(`🧪 Test ${provider}...`, 'INFO');
const response = await callLLM(provider, testPrompt);
results[provider] = {
status: 'SUCCESS',
response: response,
model: LLM_CONFIG[provider].model
};
} catch (error) {
results[provider] = {
status: 'ERROR',
error: error.toString(),
model: LLM_CONFIG[provider].model
};
}
// Petit délai entre tests
await sleep(500);
}
logSh(`📊 Tests terminés: ${JSON.stringify(results, null, 2)}`, 'INFO');
return results;
}
/**
* Obtenir les providers disponibles (avec clés API valides)
*/
function getAvailableProviders() {
const available = [];
Object.keys(LLM_CONFIG).forEach(provider => {
const config = LLM_CONFIG[provider];
if (config.apiKey && !config.apiKey.startsWith('VOTRE_CLE_')) {
available.push(provider);
}
});
return available;
}
/**
* Obtenir des statistiques d'usage par provider
*/
async function getUsageStats() {
try {
// TODO: Adapter selon votre système de stockage
// Pour l'instant retourne un message par défaut
return { message: 'Statistiques non implémentées en Node.js' };
} catch (error) {
return { error: error.toString() };
}
}
// ============= MIGRATION DE L'ANCIEN CODE =============
/**
* Fonction de compatibilité pour remplacer votre ancien callOpenAI()
* Maintient la même signature pour ne pas casser votre code existant
*/
async function callOpenAI(prompt, personality) {
return await callLLM('openai', prompt, {}, personality);
}
// ============= EXPORTS POUR TESTS =============
/**
* Fonction de test rapide
*/
async function testLLMManager() {
logSh('🚀 Test du LLM Manager Node.js...', 'INFO');
// Test des providers disponibles
const available = getAvailableProviders();
logSh('Providers disponibles: ' + available.join(', ') + ' (' + available.length + '/6)', 'INFO');
// Test d'appel simple sur chaque provider disponible
for (const provider of available) {
try {
logSh(`🧪 Test ${provider}...`, 'DEBUG');
const startTime = Date.now();
const response = await callLLM(provider, 'Dis juste "Test OK"');
const duration = Date.now() - startTime;
logSh(`✅ Test ${provider} réussi: "${response}" (${duration}ms)`, 'INFO');
} catch (error) {
logSh(`❌ Test ${provider} échoué: ${error.toString()}`, 'ERROR');
}
// Petit délai pour éviter rate limits
await sleep(500);
}
// Test spécifique OpenAI (compatibilité avec ancien code)
try {
logSh('🎯 Test spécifique OpenAI (compatibilité)...', 'DEBUG');
const response = await callLLM('openai', 'Dis juste "Test OK"');
logSh('✅ Test OpenAI compatibilité: ' + response, 'INFO');
} catch (error) {
logSh('❌ Test OpenAI compatibilité échoué: ' + error.toString(), 'ERROR');
}
// Afficher les stats d'usage
try {
logSh('📊 Récupération statistiques d\'usage...', 'DEBUG');
const stats = await getUsageStats();
if (stats.error) {
logSh('⚠ Erreur récupération stats: ' + stats.error, 'WARNING');
} else if (stats.message) {
logSh('📊 Stats: ' + stats.message, 'INFO');
} else {
// Formatter les stats pour les logs
Object.keys(stats).forEach(provider => {
const s = stats[provider];
logSh(`📈 ${provider}: ${s.calls} appels, ${s.successRate}% succès, ${s.avgDuration}ms moyen`, 'INFO');
});
}
} catch (error) {
logSh('❌ Erreur lors de la récupération des stats: ' + error.toString(), 'ERROR');
}
// Résumé final
const workingCount = available.length;
const totalProviders = Object.keys(LLM_CONFIG).length;
if (workingCount === totalProviders) {
logSh(`✅ Test LLM Manager COMPLET: ${workingCount}/${totalProviders} providers opérationnels`, 'INFO');
} else if (workingCount >= 2) {
logSh(`✅ Test LLM Manager PARTIEL: ${workingCount}/${totalProviders} providers opérationnels (suffisant pour DNA Mixing)`, 'INFO');
} else {
logSh(`❌ Test LLM Manager INSUFFISANT: ${workingCount}/${totalProviders} providers opérationnels (minimum 2 requis)`, 'ERROR');
}
logSh('🏁 Test LLM Manager terminé', 'INFO');
}
/**
* Version complète avec test de tous les providers (même non configurés)
*/
async function testLLMManagerComplete() {
logSh('🚀 Test COMPLET du LLM Manager (tous providers)...', 'INFO');
const allProviders = Object.keys(LLM_CONFIG);
logSh(`Providers configurés: ${allProviders.join(', ')}`, 'INFO');
const results = {
configured: 0,
working: 0,
failed: 0
};
for (const provider of allProviders) {
const config = LLM_CONFIG[provider];
// Vérifier si configuré
if (!config.apiKey || config.apiKey.startsWith('VOTRE_CLE_')) {
logSh(`⚙️ ${provider}: NON CONFIGURÉ (clé API manquante)`, 'WARNING');
continue;
}
results.configured++;
try {
logSh(`🧪 Test ${provider} (${config.model})...`, 'DEBUG');
const startTime = Date.now();
const response = await callLLM(provider, 'Réponds "OK" seulement.', { maxTokens: 100 });
const duration = Date.now() - startTime;
results.working++;
logSh(`${provider}: "${response.trim()}" (${duration}ms)`, 'INFO');
} catch (error) {
results.failed++;
logSh(`${provider}: ${error.toString()}`, 'ERROR');
}
// Délai entre tests
await sleep(700);
}
// Résumé final complet
logSh(`📊 RÉSUMÉ FINAL:`, 'INFO');
logSh(` • Providers total: ${allProviders.length}`, 'INFO');
logSh(` • Configurés: ${results.configured}`, 'INFO');
logSh(` • Fonctionnels: ${results.working}`, 'INFO');
logSh(` • En échec: ${results.failed}`, 'INFO');
const status = results.working >= 4 ? 'EXCELLENT' :
results.working >= 2 ? 'BON' : 'INSUFFISANT';
logSh(`🏆 STATUS: ${status} (${results.working} LLMs opérationnels)`,
status === 'INSUFFISANT' ? 'ERROR' : 'INFO');
logSh('🏁 Test LLM Manager COMPLET terminé', 'INFO');
return {
total: allProviders.length,
configured: results.configured,
working: results.working,
failed: results.failed,
status: status
};
}
// ============= EXPORTS MODULE =============
module.exports = {
callLLM,
callOpenAI,
testAllLLMs,
getAvailableProviders,
getUsageStats,
testLLMManager,
testLLMManagerComplete,
LLM_CONFIG
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/ElementExtraction.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: lib/element-extraction.js - CONVERTI POUR NODE.JS
// Description: Extraction et parsing des éléments XML
// ========================================
// 🔄 NODE.JS IMPORTS
const { logSh } = require('./ErrorReporting');
// ============= EXTRACTION PRINCIPALE =============
async function extractElements(xmlTemplate, csvData) {
try {
await logSh('Extraction éléments avec séparation tag/contenu...', 'DEBUG');
const regex = /\|([^|]+)\|/g;
const elements = [];
let match;
while ((match = regex.exec(xmlTemplate)) !== null) {
const fullMatch = match[1]; // Ex: "Titre_H1_1{{T0}}" ou "Titre_H3_3{{MC+1_3}}"
// Séparer nom du tag et variables
const nameMatch = fullMatch.match(/^([^{]+)/);
const variablesMatch = fullMatch.match(/\{\{([^}]+)\}\}/g);
// FIX REGEX INSTRUCTIONS - Enlever d'abord les {{variables}} puis chercher {instructions}
const withoutVariables = fullMatch.replace(/\{\{[^}]+\}\}/g, '');
const instructionsMatch = withoutVariables.match(/\{([^}]+)\}/);
const tagName = nameMatch ? nameMatch[1].trim() : fullMatch.split('{')[0];
// TAG PUR (sans variables)
const pureTag = `|${tagName}|`;
// RÉSOUDRE le contenu des variables
const resolvedContent = resolveVariablesContent(variablesMatch, csvData);
elements.push({
originalTag: pureTag, // ← TAG PUR : |Titre_H3_3|
name: tagName, // ← Titre_H3_3
variables: variablesMatch || [], // ← [{{MC+1_3}}]
resolvedContent: resolvedContent, // ← "Plaque de rue en aluminium"
instructions: instructionsMatch ? instructionsMatch[1] : null,
type: getElementType(tagName),
originalFullMatch: fullMatch // ← Backup si besoin
});
await logSh(`Tag séparé: ${pureTag} → "${resolvedContent}"`, 'DEBUG');
}
await logSh(`${elements.length} éléments extraits avec séparation`, 'INFO');
return elements;
} catch (error) {
await logSh(`Erreur extractElements: ${error}`, 'ERROR');
return [];
}
}
// ============= RÉSOLUTION VARIABLES - IDENTIQUE =============
function resolveVariablesContent(variablesMatch, csvData) {
if (!variablesMatch || variablesMatch.length === 0) {
return ""; // Pas de variables à résoudre
}
let resolvedContent = "";
variablesMatch.forEach(variable => {
const cleanVar = variable.replace(/[{}]/g, ''); // Enlever {{ }}
switch (cleanVar) {
case 'T0':
resolvedContent += csvData.t0;
break;
case 'MC0':
resolvedContent += csvData.mc0;
break;
case 'T-1':
resolvedContent += csvData.tMinus1;
break;
case 'L-1':
resolvedContent += csvData.lMinus1;
break;
default:
// Gérer MC+1_1, MC+1_2, etc.
if (cleanVar.startsWith('MC+1_')) {
const index = parseInt(cleanVar.split('_')[1]) - 1;
const mcPlus1 = csvData.mcPlus1.split(',').map(s => s.trim());
resolvedContent += mcPlus1[index] || `[${cleanVar} non défini]`;
}
else if (cleanVar.startsWith('T+1_')) {
const index = parseInt(cleanVar.split('_')[1]) - 1;
const tPlus1 = csvData.tPlus1.split(',').map(s => s.trim());
resolvedContent += tPlus1[index] || `[${cleanVar} non défini]`;
}
else if (cleanVar.startsWith('L+1_')) {
const index = parseInt(cleanVar.split('_')[1]) - 1;
const lPlus1 = csvData.lPlus1.split(',').map(s => s.trim());
resolvedContent += lPlus1[index] || `[${cleanVar} non défini]`;
}
else {
resolvedContent += `[${cleanVar} non résolu]`;
}
break;
}
});
return resolvedContent;
}
// ============= CLASSIFICATION ÉLÉMENTS - IDENTIQUE =============
function getElementType(name) {
if (name.includes('Titre_H1')) return 'titre_h1';
if (name.includes('Titre_H2')) return 'titre_h2';
if (name.includes('Titre_H3')) return 'titre_h3';
if (name.includes('Intro_')) return 'intro';
if (name.includes('Txt_')) return 'texte';
if (name.includes('Faq_q')) return 'faq_question';
if (name.includes('Faq_a')) return 'faq_reponse';
if (name.includes('Faq_H3')) return 'faq_titre';
return 'autre';
}
// ============= GÉNÉRATION SÉQUENTIELLE - ADAPTÉE =============
async function generateAllContent(elements, csvData, xmlTemplate) {
await logSh(`Début génération pour ${elements.length} éléments`, 'INFO');
const generatedContent = {};
for (let index = 0; index < elements.length; index++) {
const element = elements[index];
try {
await logSh(`Élément ${index + 1}/${elements.length}: ${element.name}`, 'DEBUG');
const prompt = createPromptForElement(element, csvData);
await logSh(`Prompt créé: ${prompt}`, 'DEBUG');
// 🔄 NODE.JS : Import callOpenAI depuis LLM manager
const { callLLM } = require('./LLMManager');
const content = await callLLM('openai', prompt, {}, csvData.personality);
await logSh(`Contenu reçu: ${content}`, 'DEBUG');
generatedContent[element.originalTag] = content;
// 🔄 NODE.JS : Pas de Utilities.sleep(), les appels API gèrent leur rate limiting
} catch (error) {
await logSh(`ERREUR élément ${element.name}: ${error.toString()}`, 'ERROR');
generatedContent[element.originalTag] = `[Erreur génération: ${element.name}]`;
}
}
await logSh(`Génération terminée. ${Object.keys(generatedContent).length} éléments`, 'INFO');
return generatedContent;
}
// ============= PARSING STRUCTURE - IDENTIQUE =============
function parseElementStructure(element) {
// NETTOYER le nom : enlever <strong>, </strong>, {{...}}, {...}
let cleanName = element.name
.replace(/<\/?strong>/g, '') // ← ENLEVER <strong>
.replace(/\{\{[^}]*\}\}/g, '') // Enlever {{MC0}}
.replace(/\{[^}]*\}/g, ''); // Enlever {instructions}
const parts = cleanName.split('_');
return {
type: parts[0],
level: parts[1],
indices: parts.slice(2).map(Number),
hierarchyPath: parts.slice(1).join('_'),
originalElement: element,
variables: element.variables || [],
instructions: element.instructions
};
}
// ============= HIÉRARCHIE INTELLIGENTE - ADAPTÉE =============
async function buildSmartHierarchy(elements) {
const hierarchy = {};
elements.forEach(element => {
const structure = parseElementStructure(element);
const path = structure.hierarchyPath;
if (!hierarchy[path]) {
hierarchy[path] = {
title: null,
text: null,
questions: [],
children: {}
};
}
// Associer intelligemment
if (structure.type === 'Titre') {
hierarchy[path].title = structure; // Tout l'objet avec variables + instructions
} else if (structure.type === 'Txt') {
hierarchy[path].text = structure;
} else if (structure.type === 'Intro') {
hierarchy[path].text = structure;
} else if (structure.type === 'Faq') {
hierarchy[path].questions.push(structure);
}
});
// ← LIGNE COMPILÉE
const mappingSummary = Object.keys(hierarchy).map(path => {
const section = hierarchy[path];
return `${path}:[T:${section.title ? '✓' : '✗'} Txt:${section.text ? '✓' : '✗'} FAQ:${section.questions.length}]`;
}).join(' | ');
await logSh('Correspondances: ' + mappingSummary, 'DEBUG');
return hierarchy;
}
// ============= PARSERS RÉPONSES - ADAPTÉS =============
async function parseTitlesResponse(response, allTitles) {
const results = {};
// Utiliser regex pour extraire [TAG] contenu
const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
let match;
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = match[2].trim();
// Nettoyer le contenu (enlever # et balises HTML si présentes)
const cleanContent = content
.replace(/^#+\s*/, '') // Enlever # du début
.replace(/<\/?[^>]+(>|$)/g, ""); // Enlever balises HTML
results[`|${tag}|`] = cleanContent;
await logSh(`✓ Titre parsé [${tag}]: "${cleanContent}"`, 'DEBUG');
}
// Fallback si parsing échoue
if (Object.keys(results).length === 0) {
await logSh('Parsing titres échoué, fallback ligne par ligne', 'WARNING');
const lines = response.split('\n').filter(line => line.trim());
allTitles.forEach((titleInfo, index) => {
if (lines[index]) {
results[titleInfo.tag] = lines[index].trim();
}
});
}
return results;
}
async function parseTextsResponse(response, allTexts) {
const results = {};
await logSh('Parsing réponse textes avec vrais tags...', 'DEBUG');
// Utiliser regex pour extraire [TAG] contenu avec les vrais noms
const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
let match;
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = match[2].trim();
// Nettoyer le contenu
const cleanContent = content.replace(/^#+\s*/, '').replace(/<\/?[^>]+(>|$)/g, "");
results[`|${tag}|`] = cleanContent;
await logSh(`✓ Texte parsé [${tag}]: "${cleanContent}"`, 'DEBUG');
}
// Fallback si parsing échoue - mapper par position
if (Object.keys(results).length === 0) {
await logSh('Parsing textes échoué, fallback ligne par ligne', 'WARNING');
const lines = response.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0 && !line.startsWith('['));
for (let index = 0; index < allTexts.length; index++) {
const textInfo = allTexts[index];
if (index < lines.length) {
let content = lines[index];
content = content.replace(/^\d+\.\s*/, ''); // Enlever "1. " si présent
results[textInfo.tag] = content;
await logSh(`✓ Texte fallback ${index + 1}${textInfo.tag}: "${content}"`, 'DEBUG');
} else {
await logSh(`✗ Pas assez de lignes pour ${textInfo.tag}`, 'WARNING');
results[textInfo.tag] = `[Texte manquant ${index + 1}]`;
}
}
}
return results;
}
// ============= PARSER FAQ SPÉCIALISÉ - ADAPTÉ =============
async function parseFAQPairsResponse(response, faqPairs) {
const results = {};
await logSh('Parsing réponse paires FAQ...', 'DEBUG');
// Parser avec regex pour capturer question + réponse
const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = match[2].trim();
const cleanContent = content.replace(/^#+\s*/, '').replace(/<\/?[^>]+(>|$)/g, "");
parsedItems[tag] = cleanContent;
await logSh(`✓ Item FAQ parsé [${tag}]: "${cleanContent}"`, 'DEBUG');
}
// Mapper aux tags originaux avec |
Object.keys(parsedItems).forEach(cleanTag => {
const content = parsedItems[cleanTag];
results[`|${cleanTag}|`] = content;
});
// Vérification de cohérence paires
let pairsCompletes = 0;
for (const pair of faqPairs) {
const hasQuestion = results[pair.question.tag];
const hasAnswer = results[pair.answer.tag];
if (hasQuestion && hasAnswer) {
pairsCompletes++;
await logSh(`✓ Paire FAQ ${pair.number} complète: Q+R`, 'DEBUG');
} else {
await logSh(`⚠ Paire FAQ ${pair.number} incomplète: Q=${!!hasQuestion} R=${!!hasAnswer}`, 'WARNING');
}
}
await logSh(`${pairsCompletes}/${faqPairs.length} paires FAQ complètes`, 'INFO');
// FATAL si paires FAQ manquantes
if (pairsCompletes < faqPairs.length) {
const manquantes = faqPairs.length - pairsCompletes;
await logSh(`❌ FATAL: ${manquantes} paires FAQ manquantes sur ${faqPairs.length}`, 'ERROR');
throw new Error(`FATAL: Génération FAQ incomplète (${manquantes}/${faqPairs.length} manquantes) - arrêt du workflow`);
}
return results;
}
async function parseOtherElementsResponse(response, allOtherElements) {
const results = {};
await logSh('Parsing réponse autres éléments...', 'DEBUG');
const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
let match;
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = match[2].trim();
const cleanContent = content.replace(/^#+\s*/, '').replace(/<\/?[^>]+(>|$)/g, "");
results[`|${tag}|`] = cleanContent;
await logSh(`✓ Autre élément parsé [${tag}]: "${cleanContent}"`, 'DEBUG');
}
// Fallback si parsing partiel
if (Object.keys(results).length < allOtherElements.length) {
await logSh('Parsing autres éléments partiel, complétion fallback', 'WARNING');
const lines = response.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0 && !line.startsWith('['));
allOtherElements.forEach((element, index) => {
if (!results[element.tag] && lines[index]) {
results[element.tag] = lines[index];
}
});
}
return results;
}
// ============= HELPER FUNCTIONS - ADAPTÉES =============
function createPromptForElement(element, csvData) {
// Cette fonction sera probablement définie dans content-generation.js
// Pour l'instant, retour basique
return `Génère du contenu pour ${element.type}: ${element.resolvedContent}`;
}
// 🔄 NODE.JS EXPORTS
module.exports = {
extractElements,
resolveVariablesContent,
getElementType,
generateAllContent,
parseElementStructure,
buildSmartHierarchy,
parseTitlesResponse,
parseTextsResponse,
parseFAQPairsResponse,
parseOtherElementsResponse,
createPromptForElement
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/MissingKeywords.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: MissingKeywords.js - Version Node.js
// Description: Génération automatique des mots-clés manquants
// ========================================
const { logSh } = require('./ErrorReporting');
const { callLLM } = require('./LLMManager');
/**
* Génère automatiquement les mots-clés manquants pour les éléments non définis
* @param {Array} elements - Liste des éléments extraits
* @param {Object} csvData - Données CSV avec personnalité
* @returns {Object} Éléments mis à jour avec nouveaux mots-clés
*/
async function generateMissingKeywords(elements, csvData) {
logSh('>>> GÉNÉRATION MOTS-CLÉS MANQUANTS <<<', 'INFO');
// 1. IDENTIFIER tous les éléments manquants
const missingElements = [];
elements.forEach(element => {
if (element.resolvedContent.includes('non défini') ||
element.resolvedContent.includes('non résolu') ||
element.resolvedContent.trim() === '') {
missingElements.push({
tag: element.originalTag,
name: element.name,
type: element.type,
currentContent: element.resolvedContent,
context: getElementContext(element, elements, csvData)
});
}
});
if (missingElements.length === 0) {
logSh('Aucun mot-clé manquant détecté', 'INFO');
return {};
}
logSh(`${missingElements.length} mots-clés manquants détectés`, 'INFO');
// 2. ANALYSER le contexte global disponible
const contextAnalysis = analyzeAvailableContext(elements, csvData);
// 3. GÉNÉRER tous les manquants en UN SEUL appel IA
const generatedKeywords = await callOpenAIForMissingKeywords(missingElements, contextAnalysis, csvData);
// 4. METTRE À JOUR les éléments avec les nouveaux mots-clés
const updatedElements = updateElementsWithKeywords(elements, generatedKeywords);
logSh(`Mots-clés manquants générés: ${Object.keys(generatedKeywords).length}`, 'INFO');
return updatedElements;
}
/**
* Analyser le contexte disponible pour guider la génération
* @param {Array} elements - Tous les éléments
* @param {Object} csvData - Données CSV
* @returns {Object} Analyse contextuelle
*/
function analyzeAvailableContext(elements, csvData) {
const availableKeywords = [];
const availableContent = [];
// Récupérer tous les mots-clés/contenu déjà disponibles
elements.forEach(element => {
if (element.resolvedContent &&
!element.resolvedContent.includes('non défini') &&
!element.resolvedContent.includes('non résolu') &&
element.resolvedContent.trim() !== '') {
if (element.type.includes('titre')) {
availableKeywords.push(element.resolvedContent);
} else {
availableContent.push(element.resolvedContent.substring(0, 100));
}
}
});
return {
mainKeyword: csvData.mc0,
mainTitle: csvData.t0,
availableKeywords: availableKeywords,
availableContent: availableContent,
theme: csvData.mc0, // Thème principal
businessContext: "Autocollant.fr - signalétique personnalisée, plaques"
};
}
/**
* Obtenir le contexte spécifique d'un élément
* @param {Object} element - Élément à analyser
* @param {Array} allElements - Tous les éléments
* @param {Object} csvData - Données CSV
* @returns {Object} Contexte de l'élément
*/
function getElementContext(element, allElements, csvData) {
const context = {
elementType: element.type,
hierarchyLevel: element.name,
nearbyElements: []
};
// Trouver les éléments proches dans la hiérarchie
const elementParts = element.name.split('_');
if (elementParts.length >= 2) {
const baseLevel = elementParts.slice(0, 2).join('_'); // Ex: "Titre_H3"
allElements.forEach(otherElement => {
if (otherElement.name.startsWith(baseLevel) &&
otherElement.resolvedContent &&
!otherElement.resolvedContent.includes('non défini')) {
context.nearbyElements.push(otherElement.resolvedContent);
}
});
}
return context;
}
/**
* Appel IA pour générer tous les mots-clés manquants en un seul batch
* @param {Array} missingElements - Éléments manquants
* @param {Object} contextAnalysis - Analyse contextuelle
* @param {Object} csvData - Données CSV avec personnalité
* @returns {Object} Mots-clés générés
*/
async function callOpenAIForMissingKeywords(missingElements, contextAnalysis, csvData) {
const personality = csvData.personality;
let prompt = `Tu es ${personality.nom} (${personality.description}). Style: ${personality.style}
MISSION: GÉNÈRE ${missingElements.length} MOTS-CLÉS/EXPRESSIONS MANQUANTS pour ${contextAnalysis.mainKeyword}
CONTEXTE:
- Sujet: ${contextAnalysis.mainKeyword}
- Entreprise: Autocollant.fr (signalétique)
- Mots-clés existants: ${contextAnalysis.availableKeywords.slice(0, 3).join(', ')}
ÉLÉMENTS MANQUANTS:
`;
missingElements.forEach((missing, index) => {
prompt += `${index + 1}. [${missing.name}] → Mot-clé SEO\n`;
});
prompt += `\nCONSIGNES:
- Thème: ${contextAnalysis.mainKeyword}
- Mots-clés SEO naturels
- Varie les termes
- Évite répétitions
FORMAT:
[${missingElements[0].name}]
mot-clé
[${missingElements[1] ? missingElements[1].name : 'exemple'}]
mot-clé
etc...`;
try {
logSh('Génération mots-clés manquants...', 'DEBUG');
// Utilisation du LLM Manager avec fallback
const response = await callLLM('openai', prompt, {
temperature: 0.7,
maxTokens: 2000
}, personality);
// Parser la réponse
const generatedKeywords = parseMissingKeywordsResponse(response, missingElements);
return generatedKeywords;
} catch (error) {
logSh(`❌ FATAL: Génération mots-clés manquants échouée: ${error}`, 'ERROR');
throw new Error(`FATAL: Génération mots-clés LLM impossible - arrêt du workflow: ${error}`);
}
}
/**
* Parser la réponse IA pour extraire les mots-clés générés
* @param {string} response - Réponse de l'IA
* @param {Array} missingElements - Éléments manquants
* @returns {Object} Mots-clés parsés
*/
function parseMissingKeywordsResponse(response, missingElements) {
const results = {};
const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
let match;
while ((match = regex.exec(response)) !== null) {
const elementName = match[1].trim();
const generatedKeyword = match[2].trim();
results[elementName] = generatedKeyword;
logSh(`✓ Mot-clé généré [${elementName}]: "${generatedKeyword}"`, 'DEBUG');
}
// FATAL si parsing partiel
if (Object.keys(results).length < missingElements.length) {
const manquants = missingElements.length - Object.keys(results).length;
logSh(`❌ FATAL: Parsing mots-clés partiel - ${manquants}/${missingElements.length} manquants`, 'ERROR');
throw new Error(`FATAL: Parsing mots-clés incomplet (${manquants}/${missingElements.length} manquants) - arrêt du workflow`);
}
return results;
}
/**
* Mettre à jour les éléments avec les nouveaux mots-clés générés
* @param {Array} elements - Éléments originaux
* @param {Object} generatedKeywords - Nouveaux mots-clés
* @returns {Array} Éléments mis à jour
*/
function updateElementsWithKeywords(elements, generatedKeywords) {
const updatedElements = elements.map(element => {
const newKeyword = generatedKeywords[element.name];
if (newKeyword) {
return {
...element,
resolvedContent: newKeyword
};
}
return element;
});
logSh('Éléments mis à jour avec nouveaux mots-clés', 'INFO');
return updatedElements;
}
// Exports CommonJS
module.exports = {
generateMissingKeywords
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/SelectiveUtils.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// SELECTIVE UTILS - UTILITAIRES MODULAIRES
// Responsabilité: Fonctions utilitaires partagées par tous les modules selective
// Architecture: Helper functions réutilisables et composables
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* ANALYSEURS DE CONTENU SELECTIVE
*/
/**
* Analyser qualité technique d'un contenu
*/
function analyzeTechnicalQuality(content, contextualTerms = []) {
if (!content || typeof content !== 'string') return { score: 0, details: {} };
const analysis = {
score: 0,
details: {
technicalTermsFound: 0,
technicalTermsExpected: contextualTerms.length,
genericWordsCount: 0,
hasSpecifications: false,
hasDimensions: false,
contextIntegration: 0
}
};
const lowerContent = content.toLowerCase();
// 1. Compter termes techniques présents
contextualTerms.forEach(term => {
if (lowerContent.includes(term.toLowerCase())) {
analysis.details.technicalTermsFound++;
}
});
// 2. Détecter mots génériques
const genericWords = ['produit', 'solution', 'service', 'offre', 'article', 'élément'];
analysis.details.genericWordsCount = genericWords.filter(word =>
lowerContent.includes(word)
).length;
// 3. Vérifier spécifications techniques
analysis.details.hasSpecifications = /\b(norme|iso|din|ce)\b/i.test(content);
// 4. Vérifier dimensions/données techniques
analysis.details.hasDimensions = /\d+\s*(mm|cm|m|%|°|kg|g)\b/i.test(content);
// 5. Calculer score global (0-100)
const termRatio = contextualTerms.length > 0 ?
(analysis.details.technicalTermsFound / contextualTerms.length) * 40 : 20;
const genericPenalty = Math.min(20, analysis.details.genericWordsCount * 5);
const specificationBonus = analysis.details.hasSpecifications ? 15 : 0;
const dimensionBonus = analysis.details.hasDimensions ? 15 : 0;
const lengthBonus = content.length > 100 ? 10 : 0;
analysis.score = Math.max(0, Math.min(100,
termRatio + specificationBonus + dimensionBonus + lengthBonus - genericPenalty
));
return analysis;
}
/**
* Analyser fluidité des transitions
*/
function analyzeTransitionFluidity(content) {
if (!content || typeof content !== 'string') return { score: 0, details: {} };
const sentences = content.split(/[.!?]+/)
.map(s => s.trim())
.filter(s => s.length > 5);
if (sentences.length < 2) {
return { score: 100, details: { reason: 'Contenu trop court pour analyse transitions' } };
}
const analysis = {
score: 0,
details: {
sentencesCount: sentences.length,
connectorsFound: 0,
repetitiveConnectors: 0,
abruptTransitions: 0,
averageSentenceLength: 0,
lengthVariation: 0
}
};
// 1. Analyser connecteurs
const commonConnectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite'];
const connectorCounts = {};
commonConnectors.forEach(connector => {
const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
connectorCounts[connector] = matches.length;
analysis.details.connectorsFound += matches.length;
if (matches.length > 1) analysis.details.repetitiveConnectors++;
});
// 2. Détecter transitions abruptes
for (let i = 1; i < sentences.length; i++) {
const sentence = sentences[i].toLowerCase().trim();
const hasConnector = commonConnectors.some(connector =>
sentence.startsWith(connector) || sentence.includes(` ${connector} `)
);
if (!hasConnector && sentence.length > 20) {
analysis.details.abruptTransitions++;
}
}
// 3. Analyser variation de longueur
const lengths = sentences.map(s => s.split(/\s+/).length);
analysis.details.averageSentenceLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) =>
acc + Math.pow(len - analysis.details.averageSentenceLength, 2), 0
) / lengths.length;
analysis.details.lengthVariation = Math.sqrt(variance);
// 4. Calculer score fluidité (0-100)
const connectorScore = Math.min(30, (analysis.details.connectorsFound / sentences.length) * 100);
const repetitionPenalty = Math.min(20, analysis.details.repetitiveConnectors * 5);
const abruptPenalty = Math.min(30, (analysis.details.abruptTransitions / sentences.length) * 50);
const variationScore = Math.min(20, analysis.details.lengthVariation * 2);
analysis.score = Math.max(0, Math.min(100,
connectorScore + variationScore - repetitionPenalty - abruptPenalty + 50
));
return analysis;
}
/**
* Analyser cohérence de style
*/
function analyzeStyleConsistency(content, expectedPersonality = null) {
if (!content || typeof content !== 'string') return { score: 0, details: {} };
const analysis = {
score: 0,
details: {
personalityAlignment: 0,
toneConsistency: 0,
vocabularyLevel: 'standard',
formalityScore: 0,
personalityWordsFound: 0
}
};
// 1. Analyser alignement personnalité
if (expectedPersonality && expectedPersonality.vocabulairePref) {
const personalityWords = expectedPersonality.vocabulairePref.toLowerCase().split(',');
const contentLower = content.toLowerCase();
personalityWords.forEach(word => {
if (word.trim() && contentLower.includes(word.trim())) {
analysis.details.personalityWordsFound++;
}
});
analysis.details.personalityAlignment = personalityWords.length > 0 ?
(analysis.details.personalityWordsFound / personalityWords.length) * 100 : 0;
}
// 2. Analyser niveau vocabulaire
const technicalWords = content.match(/\b\w{8,}\b/g) || [];
const totalWords = content.split(/\s+/).length;
const techRatio = technicalWords.length / totalWords;
if (techRatio > 0.15) analysis.details.vocabularyLevel = 'expert';
else if (techRatio < 0.05) analysis.details.vocabularyLevel = 'accessible';
else analysis.details.vocabularyLevel = 'standard';
// 3. Analyser formalité
const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois'];
const casualIndicators = ['du coup', 'sympa', 'cool', 'nickel'];
let formalCount = formalIndicators.filter(indicator =>
content.toLowerCase().includes(indicator)
).length;
let casualCount = casualIndicators.filter(indicator =>
content.toLowerCase().includes(indicator)
).length;
analysis.details.formalityScore = formalCount - casualCount; // Positif = formel, négatif = casual
// 4. Calculer score cohérence (0-100)
let baseScore = 50;
if (expectedPersonality) {
baseScore += analysis.details.personalityAlignment * 0.3;
// Ajustements selon niveau technique attendu
const expectedLevel = expectedPersonality.niveauTechnique || 'standard';
if (expectedLevel === analysis.details.vocabularyLevel) {
baseScore += 20;
} else {
baseScore -= 10;
}
}
// Bonus cohérence tonale
const sentences = content.split(/[.!?]+/).filter(s => s.length > 10);
if (sentences.length > 1) {
baseScore += Math.min(20, analysis.details.lengthVariation || 10);
}
analysis.score = Math.max(0, Math.min(100, baseScore));
return analysis;
}
/**
* COMPARATEURS ET MÉTRIQUES
*/
/**
* Comparer deux contenus et calculer taux amélioration
*/
function compareContentImprovement(original, enhanced, analysisType = 'general') {
if (!original || !enhanced) return { improvementRate: 0, details: {} };
const comparison = {
improvementRate: 0,
details: {
lengthChange: ((enhanced.length - original.length) / original.length) * 100,
wordCountChange: 0,
structuralChanges: 0,
contentPreserved: true
}
};
// 1. Analyser changements structurels
const originalSentences = original.split(/[.!?]+/).length;
const enhancedSentences = enhanced.split(/[.!?]+/).length;
comparison.details.structuralChanges = Math.abs(enhancedSentences - originalSentences);
// 2. Analyser changements de mots
const originalWords = original.toLowerCase().split(/\s+/).filter(w => w.length > 2);
const enhancedWords = enhanced.toLowerCase().split(/\s+/).filter(w => w.length > 2);
comparison.details.wordCountChange = enhancedWords.length - originalWords.length;
// 3. Vérifier préservation du contenu principal
const originalKeyWords = originalWords.filter(w => w.length > 4);
const preservedWords = originalKeyWords.filter(w => enhanced.toLowerCase().includes(w));
comparison.details.contentPreserved = (preservedWords.length / originalKeyWords.length) > 0.7;
// 4. Calculer taux amélioration selon type d'analyse
switch (analysisType) {
case 'technical':
const originalTech = analyzeTechnicalQuality(original);
const enhancedTech = analyzeTechnicalQuality(enhanced);
comparison.improvementRate = enhancedTech.score - originalTech.score;
break;
case 'transitions':
const originalFluid = analyzeTransitionFluidity(original);
const enhancedFluid = analyzeTransitionFluidity(enhanced);
comparison.improvementRate = enhancedFluid.score - originalFluid.score;
break;
case 'style':
const originalStyle = analyzeStyleConsistency(original);
const enhancedStyle = analyzeStyleConsistency(enhanced);
comparison.improvementRate = enhancedStyle.score - originalStyle.score;
break;
default:
// Amélioration générale (moyenne pondérée)
comparison.improvementRate = Math.min(50, Math.abs(comparison.details.lengthChange) * 0.1 +
(comparison.details.contentPreserved ? 20 : -20) +
Math.min(15, Math.abs(comparison.details.wordCountChange)));
}
return comparison;
}
/**
* UTILITAIRES DE CONTENU
*/
/**
* Nettoyer contenu généré par LLM
*/
function cleanGeneratedContent(content, cleaningLevel = 'standard') {
if (!content || typeof content !== 'string') return content;
let cleaned = content.trim();
// Nettoyage de base
cleaned = cleaned.replace(/^(voici\s+)?le\s+contenu\s+(amélioré|modifié|réécrit)[:\s]*/gi, '');
cleaned = cleaned.replace(/^(bon,?\s*)?(alors,?\s*)?(voici\s+)?/gi, '');
cleaned = cleaned.replace(/^(avec\s+les?\s+)?améliorations?\s*[:\s]*/gi, '');
// Nettoyage formatage
cleaned = cleaned.replace(/\*\*([^*]+)\*\*/g, '$1'); // Gras markdown → texte normal
cleaned = cleaned.replace(/\s{2,}/g, ' '); // Espaces multiples
cleaned = cleaned.replace(/([.!?])\s*([.!?])/g, '$1 '); // Double ponctuation
if (cleaningLevel === 'intensive') {
// Nettoyage intensif
cleaned = cleaned.replace(/^\s*[-*+]\s*/gm, ''); // Puces en début de ligne
cleaned = cleaned.replace(/^(pour\s+)?(ce\s+)?(contenu\s*)?[,:]?\s*/gi, '');
cleaned = cleaned.replace(/\([^)]*\)/g, ''); // Parenthèses et contenu
}
// Nettoyage final
cleaned = cleaned.replace(/^[,.\s]+/, ''); // Début
cleaned = cleaned.replace(/[,\s]+$/, ''); // Fin
cleaned = cleaned.trim();
return cleaned;
}
/**
* Valider contenu selective
*/
function validateSelectiveContent(content, originalContent, criteria = {}) {
const validation = {
isValid: true,
score: 0,
issues: [],
suggestions: []
};
const {
minLength = 20,
maxLengthChange = 50, // % de changement maximum
preserveContent = true,
checkTechnicalTerms = true
} = criteria;
// 1. Vérifier longueur
if (!content || content.length < minLength) {
validation.isValid = false;
validation.issues.push('Contenu trop court');
validation.suggestions.push('Augmenter la longueur du contenu généré');
} else {
validation.score += 25;
}
// 2. Vérifier changements de longueur
if (originalContent) {
const lengthChange = Math.abs((content.length - originalContent.length) / originalContent.length) * 100;
if (lengthChange > maxLengthChange) {
validation.issues.push('Changement de longueur excessif');
validation.suggestions.push('Réduire l\'intensité d\'amélioration');
} else {
validation.score += 25;
}
// 3. Vérifier préservation du contenu
if (preserveContent) {
const preservation = compareContentImprovement(originalContent, content);
if (!preservation.details.contentPreserved) {
validation.isValid = false;
validation.issues.push('Contenu original non préservé');
validation.suggestions.push('Améliorer conservation du sens original');
} else {
validation.score += 25;
}
}
}
// 4. Vérifications spécifiques
if (checkTechnicalTerms) {
const technicalQuality = analyzeTechnicalQuality(content);
if (technicalQuality.score > 60) {
validation.score += 25;
} else if (technicalQuality.score < 30) {
validation.issues.push('Qualité technique insuffisante');
validation.suggestions.push('Ajouter plus de termes techniques spécialisés');
}
}
// Score final et validation
validation.score = Math.min(100, validation.score);
validation.isValid = validation.isValid && validation.score >= 60;
return validation;
}
/**
* UTILITAIRES TECHNIQUES
*/
/**
* Chunk array avec gestion intelligente
*/
function chunkArray(array, chunkSize, smartChunking = false) {
if (!Array.isArray(array)) return [];
if (array.length <= chunkSize) return [array];
const chunks = [];
if (smartChunking) {
// Chunking intelligent : éviter de séparer éléments liés
let currentChunk = [];
for (let i = 0; i < array.length; i++) {
currentChunk.push(array[i]);
// Conditions de fin de chunk intelligente
const isChunkFull = currentChunk.length >= chunkSize;
const isLastElement = i === array.length - 1;
const nextElementRelated = i < array.length - 1 &&
array[i].tag && array[i + 1].tag &&
array[i].tag.includes('FAQ') && array[i + 1].tag.includes('FAQ');
if ((isChunkFull && !nextElementRelated) || isLastElement) {
chunks.push([...currentChunk]);
currentChunk = [];
}
}
// Ajouter chunk restant si non vide
if (currentChunk.length > 0) {
if (chunks.length > 0 && chunks[chunks.length - 1].length + currentChunk.length <= chunkSize * 1.2) {
// Merger avec dernier chunk si pas trop gros
chunks[chunks.length - 1].push(...currentChunk);
} else {
chunks.push(currentChunk);
}
}
} else {
// Chunking standard
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
}
return chunks;
}
/**
* Sleep avec logging optionnel
*/
async function sleep(ms, logMessage = null) {
if (logMessage) {
logSh(`${logMessage} (${ms}ms)`, 'DEBUG');
}
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Mesurer performance d'opération
*/
function measurePerformance(operationName, startTime = Date.now()) {
const endTime = Date.now();
const duration = endTime - startTime;
const performance = {
operationName,
startTime,
endTime,
duration,
durationFormatted: formatDuration(duration)
};
return performance;
}
/**
* Formater durée en format lisible
*/
function formatDuration(ms) {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
}
/**
* GÉNÉRATION SIMPLE (REMPLACE CONTENTGENERATION.JS)
*/
/**
* Génération simple Claude uniquement (compatible avec l'ancien système)
*/
async function generateSimple(hierarchy, csvData) {
const { LLMManager } = require('../LLMManager');
logSh(`🔥 Génération simple Claude uniquement`, 'INFO');
if (!hierarchy || Object.keys(hierarchy).length === 0) {
throw new Error('Hiérarchie vide ou invalide');
}
const result = {
content: {},
stats: {
processed: 0,
enhanced: 0,
duration: 0,
llmProvider: 'claude'
}
};
const startTime = Date.now();
try {
// Générer chaque élément avec Claude
for (const [tag, instruction] of Object.entries(hierarchy)) {
try {
logSh(`🎯 Génération: ${tag}`, 'DEBUG');
const prompt = `Tu es un expert en rédaction SEO. Tu dois générer du contenu professionnel et naturel.
CONTEXTE:
- Mot-clé principal: ${csvData.mc0}
- Titre principal: ${csvData.t0}
- Personnalité: ${csvData.personality?.nom} (${csvData.personality?.style})
INSTRUCTION SPÉCIFIQUE:
${instruction}
CONSIGNES:
- Contenu naturel et engageant
- Intégration naturelle du mot-clé "${csvData.mc0}"
- Style ${csvData.personality?.style || 'professionnel'}
- Pas de formatage markdown
- Réponse directe sans préambule
RÉPONSE:`;
const response = await LLMManager.callLLM('claude', prompt, {
temperature: 0.9,
maxTokens: 300,
timeout: 30000
});
if (response && response.trim()) {
result.content[tag] = cleanGeneratedContent(response.trim());
result.stats.processed++;
result.stats.enhanced++;
} else {
logSh(`⚠️ Réponse vide pour ${tag}`, 'WARNING');
result.content[tag] = `Contenu ${tag} généré automatiquement`;
}
} catch (error) {
logSh(`❌ Erreur génération ${tag}: ${error.message}`, 'ERROR');
result.content[tag] = `Contenu ${tag} - Erreur de génération`;
}
}
result.stats.duration = Date.now() - startTime;
logSh(`✅ Génération simple terminée: ${result.stats.enhanced}/${result.stats.processed} éléments (${result.stats.duration}ms)`, 'INFO');
return result;
} catch (error) {
result.stats.duration = Date.now() - startTime;
logSh(`❌ Échec génération simple: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* STATISTIQUES ET RAPPORTS
*/
/**
* Générer rapport amélioration
*/
function generateImprovementReport(originalContent, enhancedContent, layerType = 'general') {
const report = {
layerType,
timestamp: new Date().toISOString(),
summary: {
elementsProcessed: 0,
elementsImproved: 0,
averageImprovement: 0,
totalExecutionTime: 0
},
details: {
byElement: [],
qualityMetrics: {},
recommendations: []
}
};
// Analyser chaque élément
Object.keys(originalContent).forEach(tag => {
const original = originalContent[tag];
const enhanced = enhancedContent[tag];
if (original && enhanced) {
report.summary.elementsProcessed++;
const improvement = compareContentImprovement(original, enhanced, layerType);
if (improvement.improvementRate > 0) {
report.summary.elementsImproved++;
}
report.summary.averageImprovement += improvement.improvementRate;
report.details.byElement.push({
tag,
improvementRate: improvement.improvementRate,
lengthChange: improvement.details.lengthChange,
contentPreserved: improvement.details.contentPreserved
});
}
});
// Calculer moyennes
if (report.summary.elementsProcessed > 0) {
report.summary.averageImprovement = report.summary.averageImprovement / report.summary.elementsProcessed;
}
// Métriques qualité globales
const fullOriginal = Object.values(originalContent).join(' ');
const fullEnhanced = Object.values(enhancedContent).join(' ');
report.details.qualityMetrics = {
technical: analyzeTechnicalQuality(fullEnhanced),
transitions: analyzeTransitionFluidity(fullEnhanced),
style: analyzeStyleConsistency(fullEnhanced)
};
// Recommandations
if (report.summary.averageImprovement < 10) {
report.details.recommendations.push('Augmenter l\'intensité d\'amélioration');
}
if (report.details.byElement.some(e => !e.contentPreserved)) {
report.details.recommendations.push('Améliorer préservation du contenu original');
}
return report;
}
module.exports = {
// Analyseurs
analyzeTechnicalQuality,
analyzeTransitionFluidity,
analyzeStyleConsistency,
// Comparateurs
compareContentImprovement,
// Utilitaires contenu
cleanGeneratedContent,
validateSelectiveContent,
// Utilitaires techniques
chunkArray,
sleep,
measurePerformance,
formatDuration,
// Génération simple (remplace ContentGeneration.js)
generateSimple,
// Rapports
generateImprovementReport
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/ContentAssembly.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: ContentAssembly.js
// Description: Assemblage et nettoyage du contenu XML
// ========================================
const { logSh } = require('./ErrorReporting'); // Using unified logSh from ErrorReporting
/**
* Nettoie les balises <strong> du template XML
* @param {string} xmlString - Le contenu XML à nettoyer
* @returns {string} - XML nettoyé
*/
function cleanStrongTags(xmlString) {
logSh('Nettoyage balises <strong> du template...', 'DEBUG');
// Enlever toutes les balises <strong> et </strong>
let cleaned = xmlString.replace(/<\/?strong>/g, '');
// Log du nettoyage
const strongCount = (xmlString.match(/<\/?strong>/g) || []).length;
if (strongCount > 0) {
logSh(`${strongCount} balises <strong> supprimées`, 'INFO');
}
return cleaned;
}
/**
* Remplace toutes les variables CSV dans le XML
* @param {string} xmlString - Le contenu XML
* @param {object} csvData - Les données CSV
* @returns {string} - XML avec variables remplacées
*/
function replaceAllCSVVariables(xmlString, csvData) {
logSh('Remplacement variables CSV...', 'DEBUG');
let result = xmlString;
// Variables simples
result = result.replace(/\{\{T0\}\}/g, csvData.t0 || '');
result = result.replace(/\{\{MC0\}\}/g, csvData.mc0 || '');
result = result.replace(/\{\{T-1\}\}/g, csvData.tMinus1 || '');
result = result.replace(/\{\{L-1\}\}/g, csvData.lMinus1 || '');
logSh(`Variables simples remplacées: T0="${csvData.t0}", MC0="${csvData.mc0}"`, 'DEBUG');
// Variables multiples
const mcPlus1 = (csvData.mcPlus1 || '').split(',').map(s => s.trim());
const tPlus1 = (csvData.tPlus1 || '').split(',').map(s => s.trim());
const lPlus1 = (csvData.lPlus1 || '').split(',').map(s => s.trim());
logSh(`Variables multiples: MC+1[${mcPlus1.length}], T+1[${tPlus1.length}], L+1[${lPlus1.length}]`, 'DEBUG');
// Remplacer MC+1_1, MC+1_2, etc.
for (let i = 1; i <= 6; i++) {
const mcValue = mcPlus1[i-1] || `[MC+1_${i} non défini]`;
const tValue = tPlus1[i-1] || `[T+1_${i} non défini]`;
const lValue = lPlus1[i-1] || `[L+1_${i} non défini]`;
result = result.replace(new RegExp(`\\{\\{MC\\+1_${i}\\}\\}`, 'g'), mcValue);
result = result.replace(new RegExp(`\\{\\{T\\+1_${i}\\}\\}`, 'g'), tValue);
result = result.replace(new RegExp(`\\{\\{L\\+1_${i}\\}\\}`, 'g'), lValue);
if (mcPlus1[i-1]) {
logSh(`MC+1_${i} = "${mcValue}"`, 'DEBUG');
}
}
// Vérifier qu'il ne reste pas de variables non remplacées
const remainingVars = (result.match(/\{\{[^}]+\}\}/g) || []);
if (remainingVars.length > 0) {
logSh(`ATTENTION: Variables non remplacées: ${remainingVars.join(', ')}`, 'WARNING');
}
logSh('Toutes les variables CSV remplacées', 'INFO');
return result;
}
/**
* Injecte le contenu généré dans le XML final
* @param {string} cleanXML - XML nettoyé
* @param {object} generatedContent - Contenu généré par tag
* @param {array} elements - Éléments extraits
* @returns {string} - XML final avec contenu injecté
*/
function injectGeneratedContent(cleanXML, generatedContent, elements) {
logSh('🔍 === DEBUG INJECTION MAPPING ===', 'DEBUG');
logSh(`XML reçu: ${cleanXML.length} caractères`, 'DEBUG');
logSh(`Contenu généré: ${Object.keys(generatedContent).length} éléments`, 'DEBUG');
logSh(`Éléments fournis: ${elements.length} éléments`, 'DEBUG');
// Debug: montrer le XML
logSh(`🔍 XML début: ${cleanXML}`, 'DEBUG');
// Debug: montrer le contenu généré
Object.keys(generatedContent).forEach(key => {
logSh(`🔍 Généré [${key}]: "${generatedContent[key]}"`, 'DEBUG');
});
// Debug: montrer les éléments
elements.forEach((element, i) => {
logSh(`🔍 Element ${i+1}: originalTag="${element.originalTag}", originalFullMatch="${element.originalFullMatch}"`, 'DEBUG');
});
let finalXML = cleanXML;
// Créer un mapping tag pur → tag original complet
const tagMapping = {};
elements.forEach(element => {
tagMapping[element.originalTag] = element.originalFullMatch || element.originalTag;
});
logSh(`🔍 TagMapping créé: ${JSON.stringify(tagMapping, null, 2)}`, 'DEBUG');
// Remplacer en utilisant les tags originaux complets
Object.keys(generatedContent).forEach(pureTag => {
const content = generatedContent[pureTag];
logSh(`🔍 === TRAITEMENT TAG: ${pureTag} ===`, 'DEBUG');
logSh(`🔍 Contenu à injecter: "${content}"`, 'DEBUG');
// Trouver le tag original complet dans le XML
const originalTag = findOriginalTagInXML(finalXML, pureTag);
logSh(`🔍 Tag original trouvé: ${originalTag ? originalTag : 'AUCUN'}`, 'DEBUG');
if (originalTag) {
const beforeLength = finalXML.length;
finalXML = finalXML.replace(originalTag, content);
const afterLength = finalXML.length;
if (beforeLength !== afterLength) {
logSh(`✅ SUCCÈS: Remplacé ${originalTag} par contenu (${afterLength - beforeLength + originalTag.length} chars)`, 'DEBUG');
} else {
logSh(`❌ ÉCHEC: Replace n'a pas fonctionné pour ${originalTag}`, 'DEBUG');
}
} else {
// Fallback : essayer avec le tag pur
const beforeLength = finalXML.length;
finalXML = finalXML.replace(pureTag, content);
const afterLength = finalXML.length;
logSh(`⚠ FALLBACK ${pureTag}: remplacement ${beforeLength !== afterLength ? 'RÉUSSI' : 'ÉCHOUÉ'}`, 'DEBUG');
logSh(`⚠ Contenu fallback: "${content}"`, 'DEBUG');
}
});
// Vérifier les tags restants
const remainingTags = (finalXML.match(/\|[^|]*\|/g) || []);
if (remainingTags.length > 0) {
logSh(`ATTENTION: ${remainingTags.length} tags non remplacés: ${remainingTags.slice(0, 3).join(', ')}...`, 'WARNING');
}
logSh('Injection terminée', 'INFO');
return finalXML;
}
/**
* Helper pour trouver le tag original complet dans le XML
* @param {string} xmlString - Contenu XML
* @param {string} pureTag - Tag pur à rechercher
* @returns {string|null} - Tag original trouvé ou null
*/
function findOriginalTagInXML(xmlString, pureTag) {
logSh(`🔍 === RECHERCHE TAG DANS XML ===`, 'DEBUG');
logSh(`🔍 Tag pur recherché: "${pureTag}"`, 'DEBUG');
// Extraire le nom du tag pur : |Titre_H1_1| → Titre_H1_1
const tagName = pureTag.replace(/\|/g, '');
logSh(`🔍 Nom tag extrait: "${tagName}"`, 'DEBUG');
// Chercher tous les tags qui commencent par ce nom (avec espaces optionnels)
const regex = new RegExp(`\\|\\s*${tagName}[^|]*\\|`, 'g');
logSh(`🔍 Regex utilisée: ${regex}`, 'DEBUG');
// Debug: montrer tous les tags présents dans le XML
const allTags = xmlString.match(/\|[^|]*\|/g) || [];
logSh(`🔍 Tags présents dans XML: ${allTags.length}`, 'DEBUG');
allTags.forEach((tag, i) => {
logSh(`🔍 ${i+1}. "${tag}"`, 'DEBUG');
});
const matches = xmlString.match(regex);
logSh(`🔍 Matches trouvés: ${matches ? matches.length : 0}`, 'DEBUG');
if (matches && matches.length > 0) {
logSh(`🔍 Premier match: "${matches[0]}"`, 'DEBUG');
logSh(`✅ Tag original trouvé pour ${pureTag}: ${matches[0]}`, 'DEBUG');
return matches[0];
}
logSh(`❌ Aucun tag original trouvé pour ${pureTag}`, 'DEBUG');
return null;
}
// ============= EXPORTS =============
module.exports = {
cleanStrongTags,
replaceAllCSVVariables,
injectGeneratedContent,
findOriginalTagInXML
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/ArticleStorage.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: ArticleStorage.js
// Description: Système de sauvegarde articles avec texte compilé uniquement
// ========================================
require('dotenv').config();
const { google } = require('googleapis');
const { logSh } = require('./ErrorReporting');
// Configuration Google Sheets
const SHEET_CONFIG = {
sheetId: '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c'
};
/**
* NOUVELLE FONCTION : Compiler le contenu de manière organique
* Respecte la hiérarchie et les associations naturelles
*/
async function compileGeneratedTextsOrganic(generatedTexts, elements) {
if (!generatedTexts || Object.keys(generatedTexts).length === 0) {
return '';
}
logSh(`🌱 Compilation ORGANIQUE de ${Object.keys(generatedTexts).length} éléments...`, 'DEBUG');
let compiledParts = [];
// 1. DÉTECTER et GROUPER les sections organiques
const organicSections = buildOrganicSections(generatedTexts, elements);
// 2. COMPILER dans l'ordre naturel
organicSections.forEach(section => {
if (section.type === 'header_with_content') {
// H1, H2, H3 avec leur contenu associé
if (section.title) {
compiledParts.push(cleanIndividualContent(section.title));
}
if (section.content) {
compiledParts.push(cleanIndividualContent(section.content));
}
}
else if (section.type === 'standalone_content') {
// Contenu sans titre associé
compiledParts.push(cleanIndividualContent(section.content));
}
else if (section.type === 'faq_pair') {
// Paire question + réponse
if (section.question && section.answer) {
compiledParts.push(cleanIndividualContent(section.question));
compiledParts.push(cleanIndividualContent(section.answer));
}
}
});
// 3. Joindre avec espacement naturel
const finalText = compiledParts.join('\n\n');
logSh(`✅ Compilation organique terminée: ${finalText.length} caractères`, 'INFO');
return finalText.trim();
}
/**
* Construire les sections organiques en analysant les associations
*/
function buildOrganicSections(generatedTexts, elements) {
const sections = [];
const usedTags = new Set();
// 🔧 FIX: Gérer le cas où elements est null/undefined
if (!elements) {
logSh('⚠️ Elements null, utilisation compilation simple', 'DEBUG');
// Compilation simple : tout le contenu dans l'ordre des clés
Object.keys(generatedTexts).forEach(tag => {
sections.push({
type: 'standalone_content',
content: generatedTexts[tag],
tag: tag
});
});
return sections;
}
// 1. ANALYSER l'ordre original des éléments
const originalOrder = elements.map(el => el.originalTag);
logSh(`📋 Analyse de ${originalOrder.length} éléments dans l'ordre original...`, 'DEBUG');
// 2. DÉTECTER les associations naturelles
for (let i = 0; i < originalOrder.length; i++) {
const currentTag = originalOrder[i];
const currentContent = generatedTexts[currentTag];
if (!currentContent || usedTags.has(currentTag)) continue;
const currentType = identifyElementType(currentTag);
if (currentType === 'titre_h1' || currentType === 'titre_h2' || currentType === 'titre_h3') {
// CHERCHER le contenu associé qui suit
const associatedContent = findAssociatedContent(originalOrder, i, generatedTexts, usedTags);
sections.push({
type: 'header_with_content',
title: currentContent,
content: associatedContent.content,
titleTag: currentTag,
contentTag: associatedContent.tag
});
usedTags.add(currentTag);
if (associatedContent.tag) {
usedTags.add(associatedContent.tag);
}
logSh(` ✓ Section: ${currentType} + contenu associé`, 'DEBUG');
}
else if (currentType === 'faq_question') {
// CHERCHER la réponse correspondante
const matchingAnswer = findMatchingFAQAnswer(currentTag, generatedTexts);
if (matchingAnswer) {
sections.push({
type: 'faq_pair',
question: currentContent,
answer: matchingAnswer.content,
questionTag: currentTag,
answerTag: matchingAnswer.tag
});
usedTags.add(currentTag);
usedTags.add(matchingAnswer.tag);
logSh(` ✓ Paire FAQ: ${currentTag} + ${matchingAnswer.tag}`, 'DEBUG');
}
}
else if (currentType !== 'faq_reponse') {
// CONTENU STANDALONE (pas une réponse FAQ déjà traitée)
sections.push({
type: 'standalone_content',
content: currentContent,
contentTag: currentTag
});
usedTags.add(currentTag);
logSh(` ✓ Contenu standalone: ${currentType}`, 'DEBUG');
}
}
logSh(`🏗️ ${sections.length} sections organiques construites`, 'INFO');
return sections;
}
/**
* Trouver le contenu associé à un titre (paragraphe qui suit)
*/
function findAssociatedContent(originalOrder, titleIndex, generatedTexts, usedTags) {
// Chercher dans les éléments suivants
for (let j = titleIndex + 1; j < originalOrder.length; j++) {
const nextTag = originalOrder[j];
const nextContent = generatedTexts[nextTag];
if (!nextContent || usedTags.has(nextTag)) continue;
const nextType = identifyElementType(nextTag);
// Si on trouve un autre titre, on s'arrête
if (nextType === 'titre_h1' || nextType === 'titre_h2' || nextType === 'titre_h3') {
break;
}
// Si on trouve du contenu (texte, intro), c'est probablement associé
if (nextType === 'texte' || nextType === 'intro') {
return {
content: nextContent,
tag: nextTag
};
}
}
return { content: null, tag: null };
}
/**
* Extraire le numéro d'une FAQ : |Faq_q_1| ou |Faq_a_2| → "1" ou "2"
*/
function extractFAQNumber(tag) {
const match = tag.match(/(\d+)/);
return match ? match[1] : null;
}
/**
* Trouver la réponse FAQ correspondant à une question
*/
function findMatchingFAQAnswer(questionTag, generatedTexts) {
// Extraire le numéro : |Faq_q_1| → 1
const questionNumber = extractFAQNumber(questionTag);
if (!questionNumber) return null;
// Chercher la réponse correspondante
for (const tag in generatedTexts) {
const tagType = identifyElementType(tag);
if (tagType === 'faq_reponse') {
const answerNumber = extractFAQNumber(tag);
if (answerNumber === questionNumber) {
return {
content: generatedTexts[tag],
tag: tag
};
}
}
}
return null;
}
/**
* Nouvelle fonction de sauvegarde avec compilation organique
*/
async function saveGeneratedArticleOrganic(articleData, csvData, config = {}) {
try {
logSh('💾 Sauvegarde article avec compilation organique...', 'INFO');
const sheets = await getSheetsClient();
// 🆕 Choisir la sheet selon le flag useVersionedSheet
const targetSheetName = config.useVersionedSheet ? 'Generated_Articles_Versioned' : 'Generated_Articles';
let articlesSheet = await getOrCreateSheet(sheets, targetSheetName);
// ===== COMPILATION ORGANIQUE =====
const compiledText = await compileGeneratedTextsOrganic(
articleData.generatedTexts,
articleData.originalElements // Passer les éléments originaux si disponibles
);
logSh(`📝 Texte compilé organiquement: ${compiledText.length} caractères`, 'INFO');
// Métadonnées avec format français
const now = new Date();
const frenchTimestamp = formatDateToFrench(now);
// UTILISER le slug du CSV (colonne A du Google Sheet source)
// Le slug doit venir de csvData.slug (récupéré via getBrainConfig)
const slug = csvData.slug || generateSlugFromContent(csvData.mc0, csvData.t0);
const metadata = {
timestamp: frenchTimestamp,
slug: slug,
mc0: csvData.mc0,
t0: csvData.t0,
personality: csvData.personality?.nom || 'Unknown',
antiDetectionLevel: config.antiDetectionLevel || 'MVP',
elementsCount: Object.keys(articleData.generatedTexts || {}).length,
textLength: compiledText.length,
wordCount: countWords(compiledText),
llmUsed: config.llmUsed || 'openai',
validationStatus: articleData.validationReport?.status || 'unknown',
// 🆕 Métadonnées de versioning
version: config.version || '1.0',
stage: config.stage || 'final_version',
stageDescription: config.stageDescription || 'Version finale',
parentArticleId: config.parentArticleId || null,
versionHistory: config.versionHistory || null
};
// Préparer la ligne de données avec versioning
const row = [
metadata.timestamp,
metadata.slug,
metadata.mc0,
metadata.t0,
metadata.personality,
metadata.antiDetectionLevel,
compiledText, // ← TEXTE ORGANIQUE
metadata.textLength,
metadata.wordCount,
metadata.elementsCount,
metadata.llmUsed,
metadata.validationStatus,
// 🆕 Colonnes de versioning
metadata.version,
metadata.stage,
metadata.stageDescription,
metadata.parentArticleId || '',
'', '', '', '', // Colonnes de scores détecteurs (réservées)
JSON.stringify({
csvData: csvData,
config: config,
stats: metadata,
versionHistory: metadata.versionHistory // Inclure l'historique
})
];
// DEBUG: Vérifier le slug généré
logSh(`💾 Sauvegarde avec slug: "${metadata.slug}" (colonne B)`, 'DEBUG');
// Ajouter la ligne aux données dans la bonne sheet
const targetRange = config.useVersionedSheet ? 'Generated_Articles_Versioned!A:U' : 'Generated_Articles!A:U';
await sheets.spreadsheets.values.append({
spreadsheetId: SHEET_CONFIG.sheetId,
range: targetRange,
valueInputOption: 'USER_ENTERED',
resource: {
values: [row]
}
});
// Récupérer le numéro de ligne pour l'ID article
const targetRangeForId = config.useVersionedSheet ? 'Generated_Articles_Versioned!A:A' : 'Generated_Articles!A:A';
const response = await sheets.spreadsheets.values.get({
spreadsheetId: SHEET_CONFIG.sheetId,
range: targetRangeForId
});
const articleId = response.data.values ? response.data.values.length - 1 : 1;
logSh(`✅ Article organique sauvé: ID ${articleId}, ${metadata.wordCount} mots`, 'INFO');
return {
articleId: articleId,
textLength: metadata.textLength,
wordCount: metadata.wordCount,
sheetRow: response.data.values ? response.data.values.length : 2
};
} catch (error) {
logSh(`❌ Erreur sauvegarde organique: ${error.toString()}`, 'ERROR');
throw error;
}
}
/**
* Générer un slug à partir du contenu MC0 et T0
*/
function generateSlugFromContent(mc0, t0) {
if (!mc0 && !t0) return 'article-generated';
const source = mc0 || t0;
return source
.toString()
.toLowerCase()
.replace(/[àáâäã]/g, 'a')
.replace(/[èéêë]/g, 'e')
.replace(/[ìíîï]/g, 'i')
.replace(/[òóôöõ]/g, 'o')
.replace(/[ùúûü]/g, 'u')
.replace(/[ç]/g, 'c')
.replace(/[ñ]/g, 'n')
.replace(/[^a-z0-9\s-]/g, '') // Enlever caractères spéciaux
.replace(/\s+/g, '-') // Espaces -> tirets
.replace(/-+/g, '-') // Éviter doubles tirets
.replace(/^-+|-+$/g, '') // Enlever tirets début/fin
.substring(0, 50); // Limiter longueur
}
/**
* Identifier le type d'élément par son tag
*/
function identifyElementType(tag) {
const cleanTag = tag.toLowerCase().replace(/[|{}]/g, '');
if (cleanTag.includes('titre_h1') || cleanTag.includes('h1')) return 'titre_h1';
if (cleanTag.includes('titre_h2') || cleanTag.includes('h2')) return 'titre_h2';
if (cleanTag.includes('titre_h3') || cleanTag.includes('h3')) return 'titre_h3';
if (cleanTag.includes('intro')) return 'intro';
if (cleanTag.includes('faq_q') || cleanTag.includes('faq_question')) return 'faq_question';
if (cleanTag.includes('faq_a') || cleanTag.includes('faq_reponse')) return 'faq_reponse';
return 'texte'; // Par défaut
}
/**
* Nettoyer un contenu individuel
*/
function cleanIndividualContent(content) {
if (!content) return '';
let cleaned = content.toString();
// 1. Supprimer les balises HTML
cleaned = cleaned.replace(/<[^>]*>/g, '');
// 2. Décoder les entités HTML
cleaned = cleaned.replace(/&lt;/g, '<');
cleaned = cleaned.replace(/&gt;/g, '>');
cleaned = cleaned.replace(/&amp;/g, '&');
cleaned = cleaned.replace(/&quot;/g, '"');
cleaned = cleaned.replace(/&#039;/g, "'");
cleaned = cleaned.replace(/&nbsp;/g, ' ');
// 3. Nettoyer les espaces
cleaned = cleaned.replace(/\s+/g, ' ');
cleaned = cleaned.replace(/\n\s+/g, '\n');
// 4. Supprimer les caractères de contrôle étranges
cleaned = cleaned.replace(/[\x00-\x1F\x7F-\x9F]/g, '');
return cleaned.trim();
}
/**
* Créer la sheet de stockage avec headers appropriés
*/
async function createArticlesStorageSheet(sheets, sheetName = 'Generated_Articles') {
logSh(`🗄️ Création sheet ${sheetName}...`, 'INFO');
try {
// Créer la nouvelle sheet
await sheets.spreadsheets.batchUpdate({
spreadsheetId: SHEET_CONFIG.sheetId,
resource: {
requests: [{
addSheet: {
properties: {
title: sheetName
}
}
}]
}
});
// Headers avec versioning
const headers = [
'Timestamp',
'Slug',
'MC0',
'T0',
'Personality',
'AntiDetection_Level',
'Compiled_Text', // ← COLONNE PRINCIPALE
'Text_Length',
'Word_Count',
'Elements_Count',
'LLM_Used',
'Validation_Status',
// 🆕 Colonnes de versioning
'Version', // v1.0, v1.1, v1.2, v2.0
'Stage', // initial_generation, selective_enhancement, etc.
'Stage_Description', // Description détaillée de l'étape
'Parent_Article_ID', // ID de l'article parent (pour linkage)
'GPTZero_Score', // Scores détecteurs (à remplir)
'Originality_Score',
'CopyLeaks_Score',
'Human_Quality_Score',
'Full_Metadata_JSON' // Backup complet avec historique
];
// Ajouter les headers
await sheets.spreadsheets.values.update({
spreadsheetId: SHEET_CONFIG.sheetId,
range: `${sheetName}!A1:U1`,
valueInputOption: 'USER_ENTERED',
resource: {
values: [headers]
}
});
// Formatter les headers
await sheets.spreadsheets.batchUpdate({
spreadsheetId: SHEET_CONFIG.sheetId,
resource: {
requests: [{
repeatCell: {
range: {
sheetId: await getSheetIdByName(sheets, sheetName),
startRowIndex: 0,
endRowIndex: 1,
startColumnIndex: 0,
endColumnIndex: headers.length
},
cell: {
userEnteredFormat: {
textFormat: {
bold: true
},
backgroundColor: {
red: 0.878,
green: 0.878,
blue: 0.878
},
horizontalAlignment: 'CENTER'
}
},
fields: 'userEnteredFormat(textFormat,backgroundColor,horizontalAlignment)'
}
}]
}
});
logSh(`✅ Sheet ${sheetName} créée avec succès`, 'INFO');
return true;
} catch (error) {
logSh(`❌ Erreur création sheet: ${error.toString()}`, 'ERROR');
throw error;
}
}
/**
* Formater date au format français DD/MM/YYYY HH:mm:ss
*/
function formatDateToFrench(date) {
// Utiliser toLocaleString avec le format français
return date.toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZone: 'Europe/Paris'
}).replace(',', '');
}
/**
* Compter les mots dans un texte
*/
function countWords(text) {
if (!text || text.trim() === '') return 0;
return text.trim().split(/\s+/).length;
}
/**
* Récupérer un article sauvé par ID
*/
async function getStoredArticle(articleId) {
try {
const sheets = await getSheetsClient();
const rowNumber = articleId + 2; // +2 car header + 0-indexing
const response = await sheets.spreadsheets.values.get({
spreadsheetId: SHEET_CONFIG.sheetId,
range: `Generated_Articles!A${rowNumber}:Q${rowNumber}`
});
if (!response.data.values || response.data.values.length === 0) {
throw new Error(`Article ${articleId} non trouvé`);
}
const data = response.data.values[0];
return {
articleId: articleId,
timestamp: data[0],
slug: data[1],
mc0: data[2],
t0: data[3],
personality: data[4],
antiDetectionLevel: data[5],
compiledText: data[6], // ← TEXTE PUR
textLength: data[7],
wordCount: data[8],
elementsCount: data[9],
llmUsed: data[10],
validationStatus: data[11],
gptZeroScore: data[12],
originalityScore: data[13],
copyLeaksScore: data[14],
humanScore: data[15],
fullMetadata: data[16] ? JSON.parse(data[16]) : null
};
} catch (error) {
logSh(`❌ Erreur récupération article ${articleId}: ${error.toString()}`, 'ERROR');
throw error;
}
}
/**
* Lister les derniers articles générés
*/
async function getRecentArticles(limit = 10) {
try {
const sheets = await getSheetsClient();
const response = await sheets.spreadsheets.values.get({
spreadsheetId: SHEET_CONFIG.sheetId,
range: 'Generated_Articles!A:L'
});
if (!response.data.values || response.data.values.length <= 1) {
return []; // Pas de données ou seulement headers
}
const data = response.data.values.slice(1); // Exclure headers
const startIndex = Math.max(0, data.length - limit);
const recentData = data.slice(startIndex);
return recentData.map((row, index) => ({
articleId: startIndex + index,
timestamp: row[0],
slug: row[1],
mc0: row[2],
personality: row[4],
antiDetectionLevel: row[5],
wordCount: row[8],
validationStatus: row[11]
})).reverse(); // Plus récents en premier
} catch (error) {
logSh(`❌ Erreur liste articles récents: ${error.toString()}`, 'ERROR');
return [];
}
}
/**
* Mettre à jour les scores de détection d'un article
*/
async function updateDetectionScores(articleId, scores) {
try {
const sheets = await getSheetsClient();
const rowNumber = articleId + 2;
const updates = [];
// Colonnes des scores : M, N, O (GPTZero, Originality, CopyLeaks)
if (scores.gptzero !== undefined) {
updates.push({
range: `Generated_Articles!M${rowNumber}`,
values: [[scores.gptzero]]
});
}
if (scores.originality !== undefined) {
updates.push({
range: `Generated_Articles!N${rowNumber}`,
values: [[scores.originality]]
});
}
if (scores.copyleaks !== undefined) {
updates.push({
range: `Generated_Articles!O${rowNumber}`,
values: [[scores.copyleaks]]
});
}
if (updates.length > 0) {
await sheets.spreadsheets.values.batchUpdate({
spreadsheetId: SHEET_CONFIG.sheetId,
resource: {
valueInputOption: 'USER_ENTERED',
data: updates
}
});
}
logSh(`✅ Scores détection mis à jour pour article ${articleId}`, 'INFO');
} catch (error) {
logSh(`❌ Erreur maj scores article ${articleId}: ${error.toString()}`, 'ERROR');
throw error;
}
}
// ============= HELPERS GOOGLE SHEETS =============
/**
* Obtenir le client Google Sheets authentifié
*/
async function getSheetsClient() {
const auth = new google.auth.GoogleAuth({
credentials: {
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n')
},
scopes: ['https://www.googleapis.com/auth/spreadsheets']
});
const authClient = await auth.getClient();
const sheets = google.sheets({ version: 'v4', auth: authClient });
return sheets;
}
/**
* Obtenir ou créer une sheet
*/
async function getOrCreateSheet(sheets, sheetName) {
try {
// Vérifier si la sheet existe
const response = await sheets.spreadsheets.get({
spreadsheetId: SHEET_CONFIG.sheetId
});
const existingSheet = response.data.sheets.find(
sheet => sheet.properties.title === sheetName
);
if (existingSheet) {
return existingSheet;
} else {
// Créer la sheet si elle n'existe pas
if (sheetName === 'Generated_Articles' || sheetName === 'Generated_Articles_Versioned') {
await createArticlesStorageSheet(sheets, sheetName);
return await getOrCreateSheet(sheets, sheetName); // Récursif pour récupérer la sheet créée
}
throw new Error(`Sheet ${sheetName} non supportée pour création automatique`);
}
} catch (error) {
logSh(`❌ Erreur accès/création sheet ${sheetName}: ${error.toString()}`, 'ERROR');
throw error;
}
}
/**
* Obtenir l'ID d'une sheet par son nom
*/
async function getSheetIdByName(sheets, sheetName) {
const response = await sheets.spreadsheets.get({
spreadsheetId: SHEET_CONFIG.sheetId
});
const sheet = response.data.sheets.find(
s => s.properties.title === sheetName
);
return sheet ? sheet.properties.sheetId : null;
}
// ============= EXPORTS =============
module.exports = {
compileGeneratedTextsOrganic,
buildOrganicSections,
findAssociatedContent,
extractFAQNumber,
findMatchingFAQAnswer,
saveGeneratedArticleOrganic,
identifyElementType,
cleanIndividualContent,
createArticlesStorageSheet,
formatDateToFrench,
countWords,
getStoredArticle,
getRecentArticles,
updateDetectionScores,
getSheetsClient,
getOrCreateSheet,
getSheetIdByName,
generateSlugFromContent
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/TechnicalLayer.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// TECHNICAL LAYER - COUCHE TECHNIQUE MODULAIRE
// Responsabilité: Amélioration technique modulaire réutilisable
// LLM: GPT-4o-mini (précision technique optimale)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { chunkArray, sleep } = require('./SelectiveUtils');
/**
* COUCHE TECHNIQUE MODULAIRE
*/
class TechnicalLayer {
constructor() {
this.name = 'TechnicalEnhancement';
this.defaultLLM = 'openai';
this.priority = 1; // Haute priorité - appliqué en premier généralement
}
/**
* MAIN METHOD - Appliquer amélioration technique
*/
async apply(content, config = {}) {
return await tracer.run('TechnicalLayer.apply()', async () => {
const {
llmProvider = this.defaultLLM,
intensity = 1.0, // 0.0-2.0 intensité d'amélioration
analysisMode = true, // Analyser avant d'appliquer
csvData = null,
preserveStructure = true,
targetTerms = null // Termes techniques ciblés
} = config;
await tracer.annotate({
technicalLayer: true,
llmProvider,
intensity,
elementsCount: Object.keys(content).length,
mc0: csvData?.mc0
});
const startTime = Date.now();
logSh(`⚙️ TECHNICAL LAYER: Amélioration technique (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Intensité: ${intensity}`, 'INFO');
try {
let enhancedContent = {};
let elementsProcessed = 0;
let elementsEnhanced = 0;
if (analysisMode) {
// 1. Analyser éléments nécessitant amélioration technique
const analysis = await this.analyzeTechnicalNeeds(content, csvData, targetTerms);
logSh(` 📋 Analyse: ${analysis.candidates.length}/${Object.keys(content).length} éléments candidats`, 'DEBUG');
if (analysis.candidates.length === 0) {
logSh(`✅ TECHNICAL LAYER: Aucune amélioration nécessaire`, 'INFO');
return {
content,
stats: {
processed: Object.keys(content).length,
enhanced: 0,
analysisSkipped: true,
duration: Date.now() - startTime
}
};
}
// 2. Améliorer les éléments sélectionnés
const improvedResults = await this.enhanceTechnicalElements(
analysis.candidates,
csvData,
{ llmProvider, intensity, preserveStructure }
);
// 3. Merger avec contenu original
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = analysis.candidates.length;
} else {
// Mode direct : améliorer tous les éléments
enhancedContent = await this.enhanceAllElementsDirect(
content,
csvData,
{ llmProvider, intensity, preserveStructure }
);
elementsProcessed = Object.keys(content).length;
elementsEnhanced = this.countDifferences(content, enhancedContent);
}
const duration = Date.now() - startTime;
const stats = {
processed: elementsProcessed,
enhanced: elementsEnhanced,
total: Object.keys(content).length,
enhancementRate: (elementsEnhanced / Math.max(elementsProcessed, 1)) * 100,
duration,
llmProvider,
intensity
};
logSh(`✅ TECHNICAL LAYER TERMINÉE: ${elementsEnhanced}/${elementsProcessed} améliorés (${duration}ms)`, 'INFO');
await tracer.event('Technical layer appliquée', stats);
return { content: enhancedContent, stats };
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ TECHNICAL LAYER ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw error;
}
}, { content: Object.keys(content), config });
}
/**
* ANALYSER BESOINS TECHNIQUES
*/
async analyzeTechnicalNeeds(content, csvData, targetTerms = null) {
logSh(`🔍 Analyse besoins techniques`, 'DEBUG');
const analysis = {
candidates: [],
technicalTermsFound: [],
missingTerms: [],
globalScore: 0
};
// Définir termes techniques selon contexte
const contextualTerms = this.getContextualTechnicalTerms(csvData?.mc0, targetTerms);
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
const elementAnalysis = this.analyzeTechnicalElement(text, contextualTerms, csvData);
if (elementAnalysis.needsImprovement) {
analysis.candidates.push({
tag,
content: text,
technicalTerms: elementAnalysis.foundTerms,
missingTerms: elementAnalysis.missingTerms,
score: elementAnalysis.score,
improvements: elementAnalysis.improvements
});
analysis.globalScore += elementAnalysis.score;
}
analysis.technicalTermsFound.push(...elementAnalysis.foundTerms);
});
analysis.globalScore = analysis.globalScore / Math.max(Object.keys(content).length, 1);
analysis.technicalTermsFound = [...new Set(analysis.technicalTermsFound)];
logSh(` 📊 Score global technique: ${analysis.globalScore.toFixed(2)}`, 'DEBUG');
return analysis;
}
/**
* AMÉLIORER ÉLÉMENTS TECHNIQUES SÉLECTIONNÉS
*/
async enhanceTechnicalElements(candidates, csvData, config) {
logSh(`🛠️ Amélioration ${candidates.length} éléments techniques`, 'DEBUG');
logSh(`🔍 Candidates reçus: ${JSON.stringify(candidates.map(c => c.tag))}`, 'DEBUG');
const results = {};
const chunks = chunkArray(candidates, 4); // Chunks de 4 pour GPT-4
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk technique ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const enhancementPrompt = this.createTechnicalEnhancementPrompt(chunk, csvData, config);
const response = await callLLM(config.llmProvider, enhancementPrompt, {
temperature: 0.4, // Précision technique
maxTokens: 3000
}, csvData?.personality);
const chunkResults = this.parseTechnicalResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk technique ${chunkIndex + 1}: ${Object.keys(chunkResults).length} améliorés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk technique ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: conserver contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* AMÉLIORER TOUS ÉLÉMENTS MODE DIRECT
*/
async enhanceAllElementsDirect(content, csvData, config) {
const allElements = Object.entries(content).map(([tag, text]) => ({
tag,
content: text,
technicalTerms: [],
improvements: ['amélioration_générale_technique'],
missingTerms: [] // Ajout de la propriété manquante
}));
return await this.enhanceTechnicalElements(allElements, csvData, config);
}
// ============= HELPER METHODS =============
/**
* Analyser élément technique individuel
*/
analyzeTechnicalElement(text, contextualTerms, csvData) {
let score = 0;
const foundTerms = [];
const missingTerms = [];
const improvements = [];
// 1. Détecter termes techniques présents
contextualTerms.forEach(term => {
if (text.toLowerCase().includes(term.toLowerCase())) {
foundTerms.push(term);
} else if (text.length > 100) { // Seulement pour textes longs
missingTerms.push(term);
}
});
// 2. Évaluer manque de précision technique
if (foundTerms.length === 0 && text.length > 80) {
score += 0.4;
improvements.push('ajout_termes_techniques');
}
// 3. Détecter vocabulaire trop générique
const genericWords = ['produit', 'solution', 'service', 'offre', 'article'];
const genericCount = genericWords.filter(word =>
text.toLowerCase().includes(word)
).length;
if (genericCount > 1) {
score += 0.3;
improvements.push('spécialisation_vocabulaire');
}
// 4. Manque de données techniques (dimensions, etc.)
if (text.length > 50 && !(/\d+\s*(mm|cm|m|%|°|kg|g)/.test(text))) {
score += 0.2;
improvements.push('ajout_données_techniques');
}
// 5. Contexte métier spécifique
if (csvData?.mc0 && !text.toLowerCase().includes(csvData.mc0.toLowerCase().split(' ')[0])) {
score += 0.1;
improvements.push('intégration_contexte_métier');
}
return {
needsImprovement: score > 0.3,
score,
foundTerms,
missingTerms: missingTerms.slice(0, 3), // Limiter à 3 termes manquants
improvements
};
}
/**
* Obtenir termes techniques contextuels
*/
getContextualTechnicalTerms(mc0, targetTerms) {
// Termes de base signalétique
const baseTerms = [
'dibond', 'aluminium', 'PMMA', 'acrylique', 'plexiglas',
'impression', 'gravure', 'découpe', 'fraisage', 'perçage',
'adhésif', 'fixation', 'visserie', 'support'
];
// Termes spécifiques selon contexte
const contextualTerms = [];
if (mc0) {
const mc0Lower = mc0.toLowerCase();
if (mc0Lower.includes('plaque')) {
contextualTerms.push('épaisseur 3mm', 'format standard', 'finition brossée', 'anodisation');
}
if (mc0Lower.includes('signalétique')) {
contextualTerms.push('norme ISO', 'pictogramme', 'contraste visuel', 'lisibilité');
}
if (mc0Lower.includes('personnalisée')) {
contextualTerms.push('découpe forme', 'impression numérique', 'quadrichromie', 'pantone');
}
}
// Ajouter termes ciblés si fournis
if (targetTerms && Array.isArray(targetTerms)) {
contextualTerms.push(...targetTerms);
}
return [...baseTerms, ...contextualTerms];
}
/**
* Créer prompt amélioration technique
*/
createTechnicalEnhancementPrompt(chunk, csvData, config) {
const personality = csvData?.personality;
let prompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces contenus.
CONTEXTE: ${csvData?.mc0 || 'Signalétique personnalisée'} - Secteur: impression/signalétique
${personality ? `PERSONNALITÉ: ${personality.nom} (${personality.style})` : ''}
INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif)
ÉLÉMENTS À AMÉLIORER TECHNIQUEMENT:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
CONTENU: "${item.content}"
AMÉLIORATIONS: ${item.improvements.join(', ')}
${item.missingTerms.length > 0 ? `TERMES À INTÉGRER: ${item.missingTerms.join(', ')}` : ''}`).join('\n\n')}
CONSIGNES TECHNIQUES:
- GARDE exactement le même message et ton${personality ? ` ${personality.style}` : ''}
- AJOUTE précision technique naturelle et vocabulaire spécialisé
- INTÈGRE termes métier : matériaux, procédés, normes, dimensions
- REMPLACE vocabulaire générique par termes techniques appropriés
- ÉVITE jargon incompréhensible, reste accessible
- PRESERVE longueur approximative (±15%)
VOCABULAIRE TECHNIQUE RECOMMANDÉ:
- Matériaux: dibond, aluminium anodisé, PMMA coulé, PVC expansé
- Procédés: impression UV, gravure laser, découpe numérique, fraisage CNC
- Finitions: brossé, poli, texturé, laqué
- Fixations: perçage, adhésif double face, vis inox, plots de fixation
FORMAT RÉPONSE:
[1] Contenu avec amélioration technique précise
[2] Contenu avec amélioration technique précise
etc...
IMPORTANT: Réponse DIRECTE par les contenus améliorés, pas d'explication.`;
return prompt;
}
/**
* Parser réponse technique
*/
parseTechnicalResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let technicalContent = match[2].trim();
const element = chunk[index];
// Nettoyer contenu technique
technicalContent = this.cleanTechnicalContent(technicalContent);
if (technicalContent && technicalContent.length > 10) {
results[element.tag] = technicalContent;
logSh(`✅ Amélioré technique [${element.tag}]: "${technicalContent.substring(0, 60)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content; // Fallback
logSh(`⚠️ Fallback technique [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu technique généré
*/
cleanTechnicalContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(voici\s+)?le\s+contenu\s+amélioré\s*[:.]?\s*/gi, '');
content = content.replace(/^(avec\s+)?amélioration\s+technique\s*[:.]?\s*/gi, '');
content = content.replace(/^(bon,?\s*)?(alors,?\s*)?pour\s+/gi, '');
// Nettoyer formatage
content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
content = content.replace(/\s{2,}/g, ' '); // Espaces multiples
content = content.trim();
return content;
}
/**
* Compter différences entre contenus
*/
countDifferences(original, enhanced) {
let count = 0;
Object.keys(original).forEach(tag => {
if (enhanced[tag] && enhanced[tag] !== original[tag]) {
count++;
}
});
return count;
}
}
module.exports = { TechnicalLayer };
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/TransitionLayer.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// TRANSITION LAYER - COUCHE TRANSITIONS MODULAIRE
// Responsabilité: Amélioration fluidité modulaire réutilisable
// LLM: Gemini (fluidité linguistique optimale)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { chunkArray, sleep } = require('./SelectiveUtils');
/**
* COUCHE TRANSITIONS MODULAIRE
*/
class TransitionLayer {
constructor() {
this.name = 'TransitionEnhancement';
this.defaultLLM = 'gemini';
this.priority = 2; // Priorité moyenne - appliqué après technique
}
/**
* MAIN METHOD - Appliquer amélioration transitions
*/
async apply(content, config = {}) {
return await tracer.run('TransitionLayer.apply()', async () => {
const {
llmProvider = this.defaultLLM,
intensity = 1.0, // 0.0-2.0 intensité d'amélioration
analysisMode = true, // Analyser avant d'appliquer
csvData = null,
preserveStructure = true,
targetIssues = null // Issues spécifiques à corriger
} = config;
await tracer.annotate({
transitionLayer: true,
llmProvider,
intensity,
elementsCount: Object.keys(content).length,
mc0: csvData?.mc0
});
const startTime = Date.now();
logSh(`🔗 TRANSITION LAYER: Amélioration fluidité (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Intensité: ${intensity}`, 'INFO');
try {
let enhancedContent = {};
let elementsProcessed = 0;
let elementsEnhanced = 0;
if (analysisMode) {
// 1. Analyser éléments nécessitant amélioration transitions
const analysis = await this.analyzeTransitionNeeds(content, csvData, targetIssues);
logSh(` 📋 Analyse: ${analysis.candidates.length}/${Object.keys(content).length} éléments candidats`, 'DEBUG');
if (analysis.candidates.length === 0) {
logSh(`✅ TRANSITION LAYER: Fluidité déjà optimale`, 'INFO');
return {
content,
stats: {
processed: Object.keys(content).length,
enhanced: 0,
analysisSkipped: true,
duration: Date.now() - startTime
}
};
}
// 2. Améliorer les éléments sélectionnés
const improvedResults = await this.enhanceTransitionElements(
analysis.candidates,
csvData,
{ llmProvider, intensity, preserveStructure }
);
// 3. Merger avec contenu original
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = analysis.candidates.length;
} else {
// Mode direct : améliorer tous les éléments longs
const longElements = Object.entries(content)
.filter(([tag, text]) => text.length > 150)
.map(([tag, text]) => ({ tag, content: text, issues: ['amélioration_générale'] }));
if (longElements.length === 0) {
return { content, stats: { processed: 0, enhanced: 0, duration: Date.now() - startTime } };
}
const improvedResults = await this.enhanceTransitionElements(
longElements,
csvData,
{ llmProvider, intensity, preserveStructure }
);
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = longElements.length;
}
const duration = Date.now() - startTime;
const stats = {
processed: elementsProcessed,
enhanced: elementsEnhanced,
total: Object.keys(content).length,
enhancementRate: (elementsEnhanced / Math.max(elementsProcessed, 1)) * 100,
duration,
llmProvider,
intensity
};
logSh(`✅ TRANSITION LAYER TERMINÉE: ${elementsEnhanced}/${elementsProcessed} fluidifiés (${duration}ms)`, 'INFO');
await tracer.event('Transition layer appliquée', stats);
return { content: enhancedContent, stats };
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ TRANSITION LAYER ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback gracieux : retourner contenu original
logSh(`🔄 Fallback: contenu original préservé`, 'WARNING');
return {
content,
stats: { fallback: true, duration },
error: error.message
};
}
}, { content: Object.keys(content), config });
}
/**
* ANALYSER BESOINS TRANSITIONS
*/
async analyzeTransitionNeeds(content, csvData, targetIssues = null) {
logSh(`🔍 Analyse besoins transitions`, 'DEBUG');
const analysis = {
candidates: [],
globalScore: 0,
issuesFound: {
repetitiveConnectors: 0,
abruptTransitions: 0,
uniformSentences: 0,
formalityImbalance: 0
}
};
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
const elementAnalysis = this.analyzeTransitionElement(text, csvData);
if (elementAnalysis.needsImprovement) {
analysis.candidates.push({
tag,
content: text,
issues: elementAnalysis.issues,
score: elementAnalysis.score,
improvements: elementAnalysis.improvements
});
analysis.globalScore += elementAnalysis.score;
// Compter types d'issues
elementAnalysis.issues.forEach(issue => {
if (analysis.issuesFound.hasOwnProperty(issue)) {
analysis.issuesFound[issue]++;
}
});
}
});
analysis.globalScore = analysis.globalScore / Math.max(Object.keys(content).length, 1);
logSh(` 📊 Score global transitions: ${analysis.globalScore.toFixed(2)}`, 'DEBUG');
logSh(` 🔍 Issues trouvées: ${JSON.stringify(analysis.issuesFound)}`, 'DEBUG');
return analysis;
}
/**
* AMÉLIORER ÉLÉMENTS TRANSITIONS SÉLECTIONNÉS
*/
async enhanceTransitionElements(candidates, csvData, config) {
logSh(`🔄 Amélioration ${candidates.length} éléments transitions`, 'DEBUG');
const results = {};
const chunks = chunkArray(candidates, 6); // Chunks plus petits pour Gemini
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk transitions ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const enhancementPrompt = this.createTransitionEnhancementPrompt(chunk, csvData, config);
const response = await callLLM(config.llmProvider, enhancementPrompt, {
temperature: 0.6, // Créativité modérée pour fluidité
maxTokens: 2500
}, csvData?.personality);
const chunkResults = this.parseTransitionResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk transitions ${chunkIndex + 1}: ${Object.keys(chunkResults).length} fluidifiés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk transitions ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: conserver contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
// ============= HELPER METHODS =============
/**
* Analyser élément transition individuel
*/
analyzeTransitionElement(text, csvData) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) {
return { needsImprovement: false, score: 0, issues: [], improvements: [] };
}
let score = 0;
const issues = [];
const improvements = [];
// 1. Analyser connecteurs répétitifs
const repetitiveScore = this.analyzeRepetitiveConnectors(text);
if (repetitiveScore > 0.3) {
score += 0.3;
issues.push('repetitiveConnectors');
improvements.push('varier_connecteurs');
}
// 2. Analyser transitions abruptes
const abruptScore = this.analyzeAbruptTransitions(sentences);
if (abruptScore > 0.4) {
score += 0.4;
issues.push('abruptTransitions');
improvements.push('ajouter_transitions_fluides');
}
// 3. Analyser uniformité des phrases
const uniformityScore = this.analyzeSentenceUniformity(sentences);
if (uniformityScore < 0.3) {
score += 0.2;
issues.push('uniformSentences');
improvements.push('varier_longueurs_phrases');
}
// 4. Analyser équilibre formalité
const formalityScore = this.analyzeFormalityBalance(text);
if (formalityScore > 0.5) {
score += 0.1;
issues.push('formalityImbalance');
improvements.push('équilibrer_registre_langue');
}
return {
needsImprovement: score > 0.3,
score,
issues,
improvements
};
}
/**
* Analyser connecteurs répétitifs
*/
analyzeRepetitiveConnectors(text) {
const commonConnectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc'];
let totalConnectors = 0;
let repetitions = 0;
commonConnectors.forEach(connector => {
const matches = (text.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
totalConnectors += matches.length;
if (matches.length > 1) repetitions += matches.length - 1;
});
return totalConnectors > 0 ? repetitions / totalConnectors : 0;
}
/**
* Analyser transitions abruptes
*/
analyzeAbruptTransitions(sentences) {
if (sentences.length < 2) return 0;
let abruptCount = 0;
for (let i = 1; i < sentences.length; i++) {
const current = sentences[i].trim().toLowerCase();
const hasConnector = this.hasTransitionWord(current);
if (!hasConnector && current.length > 30) {
abruptCount++;
}
}
return abruptCount / (sentences.length - 1);
}
/**
* Analyser uniformité des phrases
*/
analyzeSentenceUniformity(sentences) {
if (sentences.length < 2) return 1;
const lengths = sentences.map(s => s.trim().length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
return Math.min(1, stdDev / avgLength);
}
/**
* Analyser équilibre formalité
*/
analyzeFormalityBalance(text) {
const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois', 'cependant'];
const casualIndicators = ['du coup', 'bon', 'franchement', 'nickel', 'sympa'];
let formalCount = 0;
let casualCount = 0;
formalIndicators.forEach(indicator => {
if (text.toLowerCase().includes(indicator)) formalCount++;
});
casualIndicators.forEach(indicator => {
if (text.toLowerCase().includes(indicator)) casualCount++;
});
const total = formalCount + casualCount;
if (total === 0) return 0;
// Déséquilibre si trop d'un côté
return Math.abs(formalCount - casualCount) / total;
}
/**
* Vérifier présence mots de transition
*/
hasTransitionWord(sentence) {
const transitionWords = [
'par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc',
'ensuite', 'puis', 'également', 'aussi', 'néanmoins', 'toutefois',
'd\'ailleurs', 'en outre', 'par contre', 'en revanche'
];
return transitionWords.some(word => sentence.includes(word));
}
/**
* Créer prompt amélioration transitions
*/
createTransitionEnhancementPrompt(chunk, csvData, config) {
const personality = csvData?.personality;
let prompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.
CONTEXTE: Article SEO ${csvData?.mc0 || 'signalétique personnalisée'}
${personality ? `PERSONNALITÉ: ${personality.nom} (${personality.style} web professionnel)` : ''}
${personality?.connecteursPref ? `CONNECTEURS PRÉFÉRÉS: ${personality.connecteursPref}` : ''}
INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif)
CONTENUS À FLUIDIFIER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
PROBLÈMES: ${item.issues.join(', ')}
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS FLUIDITÉ:
- Connecteurs plus naturels et variés${personality?.connecteursPref ? `: ${personality.connecteursPref}` : ''}
- Transitions fluides entre idées et paragraphes
- Variation naturelle longueurs phrases
- ÉVITE répétitions excessives ("du coup", "par ailleurs", "en effet")
- Style ${personality?.style || 'professionnel'} mais naturel web
CONSIGNES STRICTES:
- NE CHANGE PAS le fond du message ni les informations
- GARDE même structure générale et longueur approximative (±20%)
- Améliore SEULEMENT la fluidité et les enchaînements
- RESPECTE le style ${personality?.nom || 'professionnel'}${personality?.style ? ` (${personality.style})` : ''}
- ÉVITE sur-correction qui rendrait artificiel
TECHNIQUES FLUIDITÉ:
- Varier connecteurs logiques sans répétition
- Alterner phrases courtes (8-12 mots) et moyennes (15-20 mots)
- Utiliser pronoms et reprises pour cohésion
- Ajouter transitions implicites par reformulation
- Équilibrer registre soutenu/accessible
FORMAT RÉPONSE:
[1] Contenu avec transitions améliorées
[2] Contenu avec transitions améliorées
etc...
IMPORTANT: Réponse DIRECTE par les contenus fluidifiés, pas d'explication.`;
return prompt;
}
/**
* Parser réponse transitions
*/
parseTransitionResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let fluidContent = match[2].trim();
const element = chunk[index];
// Nettoyer contenu fluidifié
fluidContent = this.cleanTransitionContent(fluidContent);
if (fluidContent && fluidContent.length > 10) {
results[element.tag] = fluidContent;
logSh(`✅ Fluidifié [${element.tag}]: "${fluidContent.substring(0, 60)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content; // Fallback
logSh(`⚠️ Fallback transitions [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu transitions généré
*/
cleanTransitionContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(voici\s+)?le\s+contenu\s+(fluidifié|amélioré)\s*[:.]?\s*/gi, '');
content = content.replace(/^(avec\s+)?transitions\s+améliorées\s*[:.]?\s*/gi, '');
content = content.replace(/^(bon,?\s*)?(alors,?\s*)?/, '');
// Nettoyer formatage
content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
content = content.replace(/\s{2,}/g, ' '); // Espaces multiples
content = content.trim();
return content;
}
}
module.exports = { TransitionLayer };
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/StyleLayer.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// STYLE LAYER - COUCHE STYLE MODULAIRE
// Responsabilité: Adaptation personnalité modulaire réutilisable
// LLM: Mistral (excellence style et personnalité)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { chunkArray, sleep } = require('./SelectiveUtils');
/**
* COUCHE STYLE MODULAIRE
*/
class StyleLayer {
constructor() {
this.name = 'StyleEnhancement';
this.defaultLLM = 'mistral';
this.priority = 3; // Priorité basse - appliqué en dernier
}
/**
* MAIN METHOD - Appliquer amélioration style
*/
async apply(content, config = {}) {
return await tracer.run('StyleLayer.apply()', async () => {
const {
llmProvider = this.defaultLLM,
intensity = 1.0, // 0.0-2.0 intensité d'amélioration
analysisMode = true, // Analyser avant d'appliquer
csvData = null,
preserveStructure = true,
targetStyle = null // Style spécifique à appliquer
} = config;
await tracer.annotate({
styleLayer: true,
llmProvider,
intensity,
elementsCount: Object.keys(content).length,
personality: csvData?.personality?.nom
});
const startTime = Date.now();
logSh(`🎨 STYLE LAYER: Amélioration personnalité (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Style: ${csvData?.personality?.nom || 'standard'}`, 'INFO');
try {
let enhancedContent = {};
let elementsProcessed = 0;
let elementsEnhanced = 0;
// Vérifier présence personnalité
if (!csvData?.personality && !targetStyle) {
logSh(`⚠️ STYLE LAYER: Pas de personnalité définie, style générique appliqué`, 'WARNING');
}
if (analysisMode) {
// 1. Analyser éléments nécessitant amélioration style
const analysis = await this.analyzeStyleNeeds(content, csvData, targetStyle);
logSh(` 📋 Analyse: ${analysis.candidates.length}/${Object.keys(content).length} éléments candidats`, 'DEBUG');
if (analysis.candidates.length === 0) {
logSh(`✅ STYLE LAYER: Style déjà cohérent`, 'INFO');
return {
content,
stats: {
processed: Object.keys(content).length,
enhanced: 0,
analysisSkipped: true,
duration: Date.now() - startTime
}
};
}
// 2. Améliorer les éléments sélectionnés
const improvedResults = await this.enhanceStyleElements(
analysis.candidates,
csvData,
{ llmProvider, intensity, preserveStructure, targetStyle }
);
// 3. Merger avec contenu original
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = analysis.candidates.length;
} else {
// Mode direct : améliorer tous les éléments textuels
const textualElements = Object.entries(content)
.filter(([tag, text]) => text.length > 50 && !tag.includes('FAQ_Question'))
.map(([tag, text]) => ({ tag, content: text, styleIssues: ['adaptation_générale'] }));
if (textualElements.length === 0) {
return { content, stats: { processed: 0, enhanced: 0, duration: Date.now() - startTime } };
}
const improvedResults = await this.enhanceStyleElements(
textualElements,
csvData,
{ llmProvider, intensity, preserveStructure, targetStyle }
);
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = textualElements.length;
}
const duration = Date.now() - startTime;
const stats = {
processed: elementsProcessed,
enhanced: elementsEnhanced,
total: Object.keys(content).length,
enhancementRate: (elementsEnhanced / Math.max(elementsProcessed, 1)) * 100,
duration,
llmProvider,
intensity,
personalityApplied: csvData?.personality?.nom || targetStyle || 'générique'
};
logSh(`✅ STYLE LAYER TERMINÉE: ${elementsEnhanced}/${elementsProcessed} stylisés (${duration}ms)`, 'INFO');
await tracer.event('Style layer appliquée', stats);
return { content: enhancedContent, stats };
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ STYLE LAYER ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback gracieux : retourner contenu original
logSh(`🔄 Fallback: style original préservé`, 'WARNING');
return {
content,
stats: { fallback: true, duration },
error: error.message
};
}
}, { content: Object.keys(content), config });
}
/**
* ANALYSER BESOINS STYLE
*/
async analyzeStyleNeeds(content, csvData, targetStyle = null) {
logSh(`🎨 Analyse besoins style`, 'DEBUG');
const analysis = {
candidates: [],
globalScore: 0,
styleIssues: {
genericLanguage: 0,
personalityMismatch: 0,
inconsistentTone: 0,
missingVocabulary: 0
}
};
const personality = csvData?.personality;
const expectedStyle = targetStyle || personality;
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
const elementAnalysis = this.analyzeStyleElement(text, expectedStyle, csvData);
if (elementAnalysis.needsImprovement) {
analysis.candidates.push({
tag,
content: text,
styleIssues: elementAnalysis.issues,
score: elementAnalysis.score,
improvements: elementAnalysis.improvements
});
analysis.globalScore += elementAnalysis.score;
// Compter types d'issues
elementAnalysis.issues.forEach(issue => {
if (analysis.styleIssues.hasOwnProperty(issue)) {
analysis.styleIssues[issue]++;
}
});
}
});
analysis.globalScore = analysis.globalScore / Math.max(Object.keys(content).length, 1);
logSh(` 📊 Score global style: ${analysis.globalScore.toFixed(2)}`, 'DEBUG');
logSh(` 🎭 Issues style: ${JSON.stringify(analysis.styleIssues)}`, 'DEBUG');
return analysis;
}
/**
* AMÉLIORER ÉLÉMENTS STYLE SÉLECTIONNÉS
*/
async enhanceStyleElements(candidates, csvData, config) {
logSh(`🎨 Amélioration ${candidates.length} éléments style`, 'DEBUG');
const results = {};
const chunks = chunkArray(candidates, 5); // Chunks optimisés pour Mistral
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk style ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const enhancementPrompt = this.createStyleEnhancementPrompt(chunk, csvData, config);
const response = await callLLM(config.llmProvider, enhancementPrompt, {
temperature: 0.8, // Créativité élevée pour style
maxTokens: 3000
}, csvData?.personality);
const chunkResults = this.parseStyleResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk style ${chunkIndex + 1}: ${Object.keys(chunkResults).length} stylisés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1800);
}
} catch (error) {
logSh(` ❌ Chunk style ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: conserver contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
// ============= HELPER METHODS =============
/**
* Analyser élément style individuel
*/
analyzeStyleElement(text, expectedStyle, csvData) {
let score = 0;
const issues = [];
const improvements = [];
// Si pas de style attendu, score faible
if (!expectedStyle) {
return { needsImprovement: false, score: 0.1, issues: ['pas_style_défini'], improvements: [] };
}
// 1. Analyser langage générique
const genericScore = this.analyzeGenericLanguage(text);
if (genericScore > 0.4) {
score += 0.3;
issues.push('genericLanguage');
improvements.push('personnaliser_vocabulaire');
}
// 2. Analyser adéquation personnalité
if (expectedStyle.vocabulairePref) {
const personalityScore = this.analyzePersonalityAlignment(text, expectedStyle);
if (personalityScore < 0.3) {
score += 0.4;
issues.push('personalityMismatch');
improvements.push('appliquer_style_personnalité');
}
}
// 3. Analyser cohérence de ton
const toneScore = this.analyzeToneConsistency(text, expectedStyle);
if (toneScore > 0.5) {
score += 0.2;
issues.push('inconsistentTone');
improvements.push('unifier_ton');
}
// 4. Analyser vocabulaire spécialisé
if (expectedStyle.niveauTechnique) {
const vocabScore = this.analyzeVocabularyLevel(text, expectedStyle);
if (vocabScore > 0.4) {
score += 0.1;
issues.push('missingVocabulary');
improvements.push('ajuster_niveau_vocabulaire');
}
}
return {
needsImprovement: score > 0.3,
score,
issues,
improvements
};
}
/**
* Analyser langage générique
*/
analyzeGenericLanguage(text) {
const genericPhrases = [
'nos solutions', 'notre expertise', 'notre savoir-faire',
'nous vous proposons', 'nous mettons à votre disposition',
'qualité optimale', 'service de qualité', 'expertise reconnue'
];
let genericCount = 0;
genericPhrases.forEach(phrase => {
if (text.toLowerCase().includes(phrase)) genericCount++;
});
const wordCount = text.split(/\s+/).length;
return Math.min(1, (genericCount / Math.max(wordCount / 50, 1)));
}
/**
* Analyser alignement personnalité
*/
analyzePersonalityAlignment(text, personality) {
if (!personality.vocabulairePref) return 1;
const preferredWords = personality.vocabulairePref.toLowerCase().split(',');
const contentLower = text.toLowerCase();
let alignmentScore = 0;
preferredWords.forEach(word => {
if (word.trim() && contentLower.includes(word.trim())) {
alignmentScore++;
}
});
return Math.min(1, alignmentScore / Math.max(preferredWords.length, 1));
}
/**
* Analyser cohérence de ton
*/
analyzeToneConsistency(text, expectedStyle) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) return 0;
const tones = sentences.map(sentence => this.detectSentenceTone(sentence));
const expectedTone = this.getExpectedTone(expectedStyle);
let inconsistencies = 0;
tones.forEach(tone => {
if (tone !== expectedTone && tone !== 'neutral') {
inconsistencies++;
}
});
return inconsistencies / tones.length;
}
/**
* Analyser niveau vocabulaire
*/
analyzeVocabularyLevel(text, expectedStyle) {
const technicalWords = text.match(/\b\w{8,}\b/g) || [];
const expectedLevel = expectedStyle.niveauTechnique || 'standard';
const techRatio = technicalWords.length / text.split(/\s+/).length;
switch (expectedLevel) {
case 'accessible':
return techRatio > 0.1 ? techRatio : 0; // Trop technique
case 'expert':
return techRatio < 0.05 ? 1 - techRatio : 0; // Pas assez technique
default:
return techRatio > 0.15 || techRatio < 0.02 ? Math.abs(0.08 - techRatio) : 0;
}
}
/**
* Détecter ton de phrase
*/
detectSentenceTone(sentence) {
const lowerSentence = sentence.toLowerCase();
if (/\b(excellent|remarquable|exceptionnel|parfait)\b/.test(lowerSentence)) return 'enthusiastic';
if (/\b(il convient|nous recommandons|il est conseillé)\b/.test(lowerSentence)) return 'formal';
if (/\b(sympa|cool|nickel|top)\b/.test(lowerSentence)) return 'casual';
if (/\b(technique|précision|spécification)\b/.test(lowerSentence)) return 'technical';
return 'neutral';
}
/**
* Obtenir ton attendu selon personnalité
*/
getExpectedTone(personality) {
if (!personality || !personality.style) return 'neutral';
const style = personality.style.toLowerCase();
if (style.includes('technique') || style.includes('expert')) return 'technical';
if (style.includes('commercial') || style.includes('vente')) return 'enthusiastic';
if (style.includes('décontracté') || style.includes('moderne')) return 'casual';
if (style.includes('professionnel') || style.includes('formel')) return 'formal';
return 'neutral';
}
/**
* Créer prompt amélioration style
*/
createStyleEnhancementPrompt(chunk, csvData, config) {
const personality = csvData?.personality || config.targetStyle;
let prompt = `MISSION: Adapte UNIQUEMENT le style et la personnalité de ces contenus.
CONTEXTE: Article SEO ${csvData?.mc0 || 'signalétique personnalisée'}
${personality ? `PERSONNALITÉ CIBLE: ${personality.nom} (${personality.style})` : 'STYLE: Professionnel standard'}
${personality?.description ? `DESCRIPTION: ${personality.description}` : ''}
INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif)
CONTENUS À STYLISER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
PROBLÈMES: ${item.styleIssues.join(', ')}
CONTENU: "${item.content}"`).join('\n\n')}
PROFIL PERSONNALITÉ ${personality?.nom || 'Standard'}:
${personality ? `- Style: ${personality.style}
- Niveau: ${personality.niveauTechnique || 'standard'}
- Vocabulaire préféré: ${personality.vocabulairePref || 'professionnel'}
- Connecteurs: ${personality.connecteursPref || 'variés'}
${personality.specificites ? `- Spécificités: ${personality.specificites}` : ''}` : '- Style professionnel web standard'}
OBJECTIFS STYLE:
- Appliquer personnalité ${personality?.nom || 'standard'} de façon naturelle
- Utiliser vocabulaire et expressions caractéristiques
- Maintenir cohérence de ton sur tout le contenu
- Adapter niveau technique selon profil (${personality?.niveauTechnique || 'standard'})
- Style web ${personality?.style || 'professionnel'} mais authentique
CONSIGNES STRICTES:
- NE CHANGE PAS le fond du message ni les informations factuelles
- GARDE même structure et longueur approximative (±15%)
- Applique SEULEMENT style et personnalité sur la forme
- RESPECTE impérativement le niveau ${personality?.niveauTechnique || 'standard'}
- ÉVITE exagération qui rendrait artificiel
TECHNIQUES STYLE:
${personality?.vocabulairePref ? `- Intégrer naturellement: ${personality.vocabulairePref}` : '- Vocabulaire professionnel équilibré'}
- Adapter registre de langue selon ${personality?.style || 'professionnel'}
- Expressions et tournures caractéristiques personnalité
- Ton cohérent: ${this.getExpectedTone(personality)} mais naturel
- Connecteurs préférés: ${personality?.connecteursPref || 'variés et naturels'}
FORMAT RÉPONSE:
[1] Contenu avec style personnalisé
[2] Contenu avec style personnalisé
etc...
IMPORTANT: Réponse DIRECTE par les contenus stylisés, pas d'explication.`;
return prompt;
}
/**
* Parser réponse style
*/
parseStyleResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let styledContent = match[2].trim();
const element = chunk[index];
// Nettoyer contenu stylisé
styledContent = this.cleanStyleContent(styledContent);
if (styledContent && styledContent.length > 10) {
results[element.tag] = styledContent;
logSh(`✅ Stylisé [${element.tag}]: "${styledContent.substring(0, 60)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content; // Fallback
logSh(`⚠️ Fallback style [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu style généré
*/
cleanStyleContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(voici\s+)?le\s+contenu\s+(stylisé|adapté|personnalisé)\s*[:.]?\s*/gi, '');
content = content.replace(/^(avec\s+)?style\s+[^:]*\s*[:.]?\s*/gi, '');
content = content.replace(/^(dans\s+le\s+style\s+de\s+)[^:]*[:.]?\s*/gi, '');
// Nettoyer formatage
content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
content = content.replace(/\s{2,}/g, ' '); // Espaces multiples
content = content.trim();
return content;
}
}
module.exports = { StyleLayer };
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/SelectiveCore.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// SELECTIVE CORE - MOTEUR MODULAIRE
// Responsabilité: Moteur selective enhancement réutilisable sur tout contenu
// Architecture: Couches applicables à la demande
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - APPLICATION COUCHE SELECTIVE ENHANCEMENT
* Input: contenu existant + configuration selective
* Output: contenu avec couche selective appliquée
*/
async function applySelectiveLayer(existingContent, config = {}) {
return await tracer.run('SelectiveCore.applySelectiveLayer()', async () => {
const {
layerType = 'technical', // 'technical' | 'transitions' | 'style' | 'all'
llmProvider = 'auto', // 'claude' | 'gpt4' | 'gemini' | 'mistral' | 'auto'
analysisMode = true, // Analyser avant d'appliquer
preserveStructure = true,
csvData = null,
context = {}
} = config;
await tracer.annotate({
selectiveLayer: true,
layerType,
llmProvider,
analysisMode,
elementsCount: Object.keys(existingContent).length
});
const startTime = Date.now();
logSh(`🔧 APPLICATION COUCHE SELECTIVE: ${layerType} (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(existingContent).length} éléments | Mode: ${analysisMode ? 'analysé' : 'direct'}`, 'INFO');
try {
let enhancedContent = {};
let layerStats = {};
// Sélection automatique du LLM si 'auto'
const selectedLLM = selectOptimalLLM(layerType, llmProvider);
// Application selon type de couche
switch (layerType) {
case 'technical':
const technicalResult = await applyTechnicalEnhancement(existingContent, { ...config, llmProvider: selectedLLM });
enhancedContent = technicalResult.content;
layerStats = technicalResult.stats;
break;
case 'transitions':
const transitionResult = await applyTransitionEnhancement(existingContent, { ...config, llmProvider: selectedLLM });
enhancedContent = transitionResult.content;
layerStats = transitionResult.stats;
break;
case 'style':
const styleResult = await applyStyleEnhancement(existingContent, { ...config, llmProvider: selectedLLM });
enhancedContent = styleResult.content;
layerStats = styleResult.stats;
break;
case 'all':
const allResult = await applyAllSelectiveLayers(existingContent, config);
enhancedContent = allResult.content;
layerStats = allResult.stats;
break;
default:
throw new Error(`Type de couche selective inconnue: ${layerType}`);
}
const duration = Date.now() - startTime;
const stats = {
layerType,
llmProvider: selectedLLM,
elementsProcessed: Object.keys(existingContent).length,
elementsEnhanced: countEnhancedElements(existingContent, enhancedContent),
duration,
...layerStats
};
logSh(`✅ COUCHE SELECTIVE APPLIQUÉE: ${stats.elementsEnhanced}/${stats.elementsProcessed} améliorés (${duration}ms)`, 'INFO');
await tracer.event('Couche selective appliquée', stats);
return {
content: enhancedContent,
stats,
original: existingContent,
config: { ...config, llmProvider: selectedLLM }
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ COUCHE SELECTIVE ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content: existingContent,
stats: { fallback: true, duration },
original: existingContent,
config,
error: error.message
};
}
}, { existingContent: Object.keys(existingContent), config });
}
/**
* APPLICATION TECHNIQUE MODULAIRE
*/
async function applyTechnicalEnhancement(content, config = {}) {
const { TechnicalLayer } = require('./TechnicalLayer');
const layer = new TechnicalLayer();
return await layer.apply(content, config);
}
/**
* APPLICATION TRANSITIONS MODULAIRE
*/
async function applyTransitionEnhancement(content, config = {}) {
const { TransitionLayer } = require('./TransitionLayer');
const layer = new TransitionLayer();
return await layer.apply(content, config);
}
/**
* APPLICATION STYLE MODULAIRE
*/
async function applyStyleEnhancement(content, config = {}) {
const { StyleLayer } = require('./StyleLayer');
const layer = new StyleLayer();
return await layer.apply(content, config);
}
/**
* APPLICATION TOUTES COUCHES SÉQUENTIELLES
*/
async function applyAllSelectiveLayers(content, config = {}) {
logSh(`🔄 Application séquentielle toutes couches selective`, 'DEBUG');
let currentContent = content;
const allStats = {
steps: [],
totalDuration: 0,
totalEnhancements: 0
};
const steps = [
{ name: 'technical', llm: 'gpt4' },
{ name: 'transitions', llm: 'gemini' },
{ name: 'style', llm: 'mistral' }
];
for (const step of steps) {
try {
logSh(` 🔧 Étape: ${step.name} (${step.llm})`, 'DEBUG');
const stepResult = await applySelectiveLayer(currentContent, {
...config,
layerType: step.name,
llmProvider: step.llm
});
currentContent = stepResult.content;
allStats.steps.push({
name: step.name,
llm: step.llm,
...stepResult.stats
});
allStats.totalDuration += stepResult.stats.duration;
allStats.totalEnhancements += stepResult.stats.elementsEnhanced;
} catch (error) {
logSh(` ❌ Étape ${step.name} échouée: ${error.message}`, 'ERROR');
allStats.steps.push({
name: step.name,
llm: step.llm,
error: error.message,
duration: 0,
elementsEnhanced: 0
});
}
}
return {
content: currentContent,
stats: allStats
};
}
/**
* ANALYSE BESOIN D'ENHANCEMENT
*/
async function analyzeEnhancementNeeds(content, config = {}) {
logSh(`🔍 Analyse besoins selective enhancement`, 'DEBUG');
const analysis = {
technical: { needed: false, score: 0, elements: [] },
transitions: { needed: false, score: 0, elements: [] },
style: { needed: false, score: 0, elements: [] },
recommendation: 'none'
};
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
// Analyse technique (termes techniques manquants)
const technicalNeed = assessTechnicalNeed(text, config.csvData);
if (technicalNeed.score > 0.3) {
analysis.technical.needed = true;
analysis.technical.score += technicalNeed.score;
analysis.technical.elements.push({ tag, score: technicalNeed.score, reason: technicalNeed.reason });
}
// Analyse transitions (fluidité)
const transitionNeed = assessTransitionNeed(text);
if (transitionNeed.score > 0.4) {
analysis.transitions.needed = true;
analysis.transitions.score += transitionNeed.score;
analysis.transitions.elements.push({ tag, score: transitionNeed.score, reason: transitionNeed.reason });
}
// Analyse style (personnalité)
const styleNeed = assessStyleNeed(text, config.csvData?.personality);
if (styleNeed.score > 0.3) {
analysis.style.needed = true;
analysis.style.score += styleNeed.score;
analysis.style.elements.push({ tag, score: styleNeed.score, reason: styleNeed.reason });
}
});
// Normaliser scores
const elementCount = Object.keys(content).length;
analysis.technical.score = analysis.technical.score / elementCount;
analysis.transitions.score = analysis.transitions.score / elementCount;
analysis.style.score = analysis.style.score / elementCount;
// Recommandation
const scores = [
{ type: 'technical', score: analysis.technical.score },
{ type: 'transitions', score: analysis.transitions.score },
{ type: 'style', score: analysis.style.score }
].sort((a, b) => b.score - a.score);
if (scores[0].score > 0.6) {
analysis.recommendation = scores[0].type;
} else if (scores[0].score > 0.4) {
analysis.recommendation = 'light_' + scores[0].type;
}
logSh(` 📊 Analyse: Tech=${analysis.technical.score.toFixed(2)} | Trans=${analysis.transitions.score.toFixed(2)} | Style=${analysis.style.score.toFixed(2)}`, 'DEBUG');
logSh(` 💡 Recommandation: ${analysis.recommendation}`, 'DEBUG');
return analysis;
}
// ============= HELPER FUNCTIONS =============
/**
* Sélectionner LLM optimal selon type de couche
*/
function selectOptimalLLM(layerType, llmProvider) {
if (llmProvider !== 'auto') return llmProvider;
const optimalMapping = {
'technical': 'openai', // OpenAI GPT-4 excellent pour précision technique
'transitions': 'gemini', // Gemini bon pour fluidité
'style': 'mistral', // Mistral excellent pour style personnalité
'all': 'claude' // Claude polyvalent pour tout
};
return optimalMapping[layerType] || 'claude';
}
/**
* Compter éléments améliorés
*/
function countEnhancedElements(original, enhanced) {
let count = 0;
Object.keys(original).forEach(tag => {
if (enhanced[tag] && enhanced[tag] !== original[tag]) {
count++;
}
});
return count;
}
/**
* Évaluer besoin technique
*/
function assessTechnicalNeed(content, csvData) {
let score = 0;
let reason = [];
// Manque de termes techniques spécifiques
if (csvData?.mc0) {
const technicalTerms = ['dibond', 'pmma', 'aluminium', 'fraisage', 'impression', 'gravure', 'découpe'];
const contentLower = content.toLowerCase();
const foundTerms = technicalTerms.filter(term => contentLower.includes(term));
if (foundTerms.length === 0 && content.length > 100) {
score += 0.4;
reason.push('manque_termes_techniques');
}
}
// Vocabulaire trop générique
const genericWords = ['produit', 'solution', 'service', 'qualité', 'offre'];
const genericCount = genericWords.filter(word => content.toLowerCase().includes(word)).length;
if (genericCount > 2) {
score += 0.3;
reason.push('vocabulaire_générique');
}
// Manque de précision dimensionnelle/technique
if (content.length > 50 && !(/\d+\s*(mm|cm|m|%|°)/.test(content))) {
score += 0.2;
reason.push('manque_précision_technique');
}
return { score: Math.min(1, score), reason: reason.join(',') };
}
/**
* Évaluer besoin transitions
*/
function assessTransitionNeed(content) {
let score = 0;
let reason = [];
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) return { score: 0, reason: '' };
// Connecteurs répétitifs
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant'];
let repetitiveConnectors = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(connector, 'gi')) || []);
if (matches.length > 1) repetitiveConnectors++;
});
if (repetitiveConnectors > 1) {
score += 0.4;
reason.push('connecteurs_répétitifs');
}
// Transitions abruptes (phrases sans connecteurs logiques)
let abruptTransitions = 0;
for (let i = 1; i < sentences.length; i++) {
const sentence = sentences[i].trim().toLowerCase();
const hasConnector = connectors.some(conn => sentence.startsWith(conn)) ||
/^(puis|ensuite|également|aussi|donc|ainsi)/.test(sentence);
if (!hasConnector && sentence.length > 30) {
abruptTransitions++;
}
}
if (abruptTransitions / sentences.length > 0.6) {
score += 0.3;
reason.push('transitions_abruptes');
}
return { score: Math.min(1, score), reason: reason.join(',') };
}
/**
* Évaluer besoin style
*/
function assessStyleNeed(content, personality) {
let score = 0;
let reason = [];
if (!personality) {
score += 0.2;
reason.push('pas_personnalité');
return { score, reason: reason.join(',') };
}
// Style générique (pas de personnalité visible)
const personalityWords = (personality.vocabulairePref || '').toLowerCase().split(',');
const contentLower = content.toLowerCase();
const personalityFound = personalityWords.some(word =>
word.trim() && contentLower.includes(word.trim())
);
if (!personalityFound && content.length > 50) {
score += 0.4;
reason.push('style_générique');
}
// Niveau technique inadapté
if (personality.niveauTechnique === 'accessible' && /\b(optimisation|implémentation|méthodologie)\b/i.test(content)) {
score += 0.3;
reason.push('trop_technique');
}
return { score: Math.min(1, score), reason: reason.join(',') };
}
module.exports = {
applySelectiveLayer, // ← MAIN ENTRY POINT MODULAIRE
applyTechnicalEnhancement,
applyTransitionEnhancement,
applyStyleEnhancement,
applyAllSelectiveLayers,
analyzeEnhancementNeeds,
selectOptimalLLM
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/SelectiveLayers.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// SELECTIVE LAYERS - COUCHES COMPOSABLES
// Responsabilité: Stacks prédéfinis et couches adaptatives pour selective enhancement
// Architecture: Composable layers avec orchestration intelligente
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { applySelectiveLayer } = require('./SelectiveCore');
/**
* STACKS PRÉDÉFINIS SELECTIVE ENHANCEMENT
*/
const PREDEFINED_STACKS = {
// Stack léger - Amélioration technique uniquement
lightEnhancement: {
name: 'lightEnhancement',
description: 'Amélioration technique légère avec OpenAI',
layers: [
{ type: 'technical', llm: 'openai', intensity: 0.7 }
],
layersCount: 1
},
// Stack standard - Technique + Transitions
standardEnhancement: {
name: 'standardEnhancement',
description: 'Amélioration technique et fluidité (OpenAI + Gemini)',
layers: [
{ type: 'technical', llm: 'openai', intensity: 0.9 },
{ type: 'transitions', llm: 'gemini', intensity: 0.8 }
],
layersCount: 2
},
// Stack complet - Toutes couches séquentielles
fullEnhancement: {
name: 'fullEnhancement',
description: 'Enhancement complet multi-LLM (OpenAI + Gemini + Mistral)',
layers: [
{ type: 'technical', llm: 'openai', intensity: 1.0 },
{ type: 'transitions', llm: 'gemini', intensity: 0.9 },
{ type: 'style', llm: 'mistral', intensity: 0.8 }
],
layersCount: 3
},
// Stack personnalité - Style prioritaire
personalityFocus: {
name: 'personalityFocus',
description: 'Focus personnalité et style avec Mistral + technique légère',
layers: [
{ type: 'style', llm: 'mistral', intensity: 1.2 },
{ type: 'technical', llm: 'openai', intensity: 0.6 }
],
layersCount: 2
},
// Stack fluidité - Transitions prioritaires
fluidityFocus: {
name: 'fluidityFocus',
description: 'Focus fluidité avec Gemini + enhancements légers',
layers: [
{ type: 'transitions', llm: 'gemini', intensity: 1.1 },
{ type: 'technical', llm: 'openai', intensity: 0.7 },
{ type: 'style', llm: 'mistral', intensity: 0.6 }
],
layersCount: 3
}
};
/**
* APPLIQUER STACK PRÉDÉFINI
*/
async function applyPredefinedStack(content, stackName, config = {}) {
return await tracer.run('SelectiveLayers.applyPredefinedStack()', async () => {
const stack = PREDEFINED_STACKS[stackName];
if (!stack) {
throw new Error(`Stack selective prédéfini inconnu: ${stackName}. Disponibles: ${Object.keys(PREDEFINED_STACKS).join(', ')}`);
}
await tracer.annotate({
selectivePredefinedStack: true,
stackName,
layersCount: stack.layersCount,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`📦 APPLICATION STACK SELECTIVE: ${stack.name} (${stack.layersCount} couches)`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Description: ${stack.description}`, 'INFO');
try {
let currentContent = content;
const stackStats = {
stackName,
layers: [],
totalModifications: 0,
totalDuration: 0,
success: true
};
// Appliquer chaque couche séquentiellement
for (let i = 0; i < stack.layers.length; i++) {
const layer = stack.layers[i];
try {
logSh(` 🔧 Couche ${i + 1}/${stack.layersCount}: ${layer.type} (${layer.llm})`, 'DEBUG');
const layerResult = await applySelectiveLayer(currentContent, {
...config,
layerType: layer.type,
llmProvider: layer.llm,
intensity: layer.intensity,
analysisMode: true
});
currentContent = layerResult.content;
stackStats.layers.push({
order: i + 1,
type: layer.type,
llm: layer.llm,
intensity: layer.intensity,
elementsEnhanced: layerResult.stats.elementsEnhanced,
duration: layerResult.stats.duration,
success: !layerResult.stats.fallback
});
stackStats.totalModifications += layerResult.stats.elementsEnhanced;
stackStats.totalDuration += layerResult.stats.duration;
logSh(` ✅ Couche ${layer.type}: ${layerResult.stats.elementsEnhanced} améliorations`, 'DEBUG');
} catch (layerError) {
logSh(` ❌ Couche ${layer.type} échouée: ${layerError.message}`, 'ERROR');
stackStats.layers.push({
order: i + 1,
type: layer.type,
llm: layer.llm,
error: layerError.message,
duration: 0,
success: false
});
// Continuer avec les autres couches
}
}
const duration = Date.now() - startTime;
const successfulLayers = stackStats.layers.filter(l => l.success).length;
logSh(`✅ STACK SELECTIVE ${stackName}: ${successfulLayers}/${stack.layersCount} couches | ${stackStats.totalModifications} modifications (${duration}ms)`, 'INFO');
await tracer.event('Stack selective appliqué', { ...stackStats, totalDuration: duration });
return {
content: currentContent,
stats: { ...stackStats, totalDuration: duration },
original: content,
stackApplied: stackName
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ STACK SELECTIVE ${stackName} ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
return {
content,
stats: { stackName, error: error.message, duration, success: false },
original: content,
fallback: true
};
}
}, { content: Object.keys(content), stackName, config });
}
/**
* APPLIQUER COUCHES ADAPTATIVES
*/
async function applyAdaptiveLayers(content, config = {}) {
return await tracer.run('SelectiveLayers.applyAdaptiveLayers()', async () => {
const {
maxIntensity = 1.0,
analysisThreshold = 0.4,
csvData = null
} = config;
await tracer.annotate({
selectiveAdaptiveLayers: true,
maxIntensity,
analysisThreshold,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🧠 APPLICATION COUCHES ADAPTATIVES SELECTIVE`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Seuil: ${analysisThreshold}`, 'INFO');
try {
// 1. Analyser besoins de chaque type de couche
const needsAnalysis = await analyzeSelectiveNeeds(content, csvData);
logSh(` 📋 Analyse besoins: Tech=${needsAnalysis.technical.score.toFixed(2)} | Trans=${needsAnalysis.transitions.score.toFixed(2)} | Style=${needsAnalysis.style.score.toFixed(2)}`, 'DEBUG');
// 2. Déterminer couches à appliquer selon scores
const layersToApply = [];
if (needsAnalysis.technical.needed && needsAnalysis.technical.score > analysisThreshold) {
layersToApply.push({
type: 'technical',
llm: 'openai',
intensity: Math.min(maxIntensity, needsAnalysis.technical.score * 1.2),
priority: 1
});
}
if (needsAnalysis.transitions.needed && needsAnalysis.transitions.score > analysisThreshold) {
layersToApply.push({
type: 'transitions',
llm: 'gemini',
intensity: Math.min(maxIntensity, needsAnalysis.transitions.score * 1.1),
priority: 2
});
}
if (needsAnalysis.style.needed && needsAnalysis.style.score > analysisThreshold) {
layersToApply.push({
type: 'style',
llm: 'mistral',
intensity: Math.min(maxIntensity, needsAnalysis.style.score),
priority: 3
});
}
if (layersToApply.length === 0) {
logSh(`✅ COUCHES ADAPTATIVES: Aucune amélioration nécessaire`, 'INFO');
return {
content,
stats: {
adaptive: true,
layersApplied: 0,
analysisOnly: true,
duration: Date.now() - startTime
}
};
}
// 3. Appliquer couches par ordre de priorité
layersToApply.sort((a, b) => a.priority - b.priority);
logSh(` 🎯 Couches sélectionnées: ${layersToApply.map(l => `${l.type}(${l.intensity.toFixed(1)})`).join(' → ')}`, 'INFO');
let currentContent = content;
const adaptiveStats = {
layersAnalyzed: 3,
layersApplied: layersToApply.length,
layers: [],
totalModifications: 0,
adaptive: true
};
for (const layer of layersToApply) {
try {
logSh(` 🔧 Couche adaptative: ${layer.type} (intensité: ${layer.intensity.toFixed(1)})`, 'DEBUG');
const layerResult = await applySelectiveLayer(currentContent, {
...config,
layerType: layer.type,
llmProvider: layer.llm,
intensity: layer.intensity,
analysisMode: true
});
currentContent = layerResult.content;
adaptiveStats.layers.push({
type: layer.type,
llm: layer.llm,
intensity: layer.intensity,
elementsEnhanced: layerResult.stats.elementsEnhanced,
duration: layerResult.stats.duration,
success: !layerResult.stats.fallback
});
adaptiveStats.totalModifications += layerResult.stats.elementsEnhanced;
} catch (layerError) {
logSh(` ❌ Couche adaptative ${layer.type} échouée: ${layerError.message}`, 'ERROR');
adaptiveStats.layers.push({
type: layer.type,
error: layerError.message,
success: false
});
}
}
const duration = Date.now() - startTime;
const successfulLayers = adaptiveStats.layers.filter(l => l.success).length;
logSh(`✅ COUCHES ADAPTATIVES: ${successfulLayers}/${layersToApply.length} appliquées | ${adaptiveStats.totalModifications} modifications (${duration}ms)`, 'INFO');
await tracer.event('Couches adaptatives appliquées', { ...adaptiveStats, totalDuration: duration });
return {
content: currentContent,
stats: { ...adaptiveStats, totalDuration: duration },
original: content
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ COUCHES ADAPTATIVES ÉCHOUÉES après ${duration}ms: ${error.message}`, 'ERROR');
return {
content,
stats: { adaptive: true, error: error.message, duration },
original: content,
fallback: true
};
}
}, { content: Object.keys(content), config });
}
/**
* PIPELINE COUCHES PERSONNALISÉ
*/
async function applyLayerPipeline(content, layerSequence, config = {}) {
return await tracer.run('SelectiveLayers.applyLayerPipeline()', async () => {
if (!Array.isArray(layerSequence) || layerSequence.length === 0) {
throw new Error('Séquence de couches invalide ou vide');
}
await tracer.annotate({
selectiveLayerPipeline: true,
pipelineLength: layerSequence.length,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🔄 PIPELINE COUCHES SELECTIVE PERSONNALISÉ: ${layerSequence.length} étapes`, 'INFO');
try {
let currentContent = content;
const pipelineStats = {
pipelineLength: layerSequence.length,
steps: [],
totalModifications: 0,
success: true
};
for (let i = 0; i < layerSequence.length; i++) {
const step = layerSequence[i];
try {
logSh(` 📍 Étape ${i + 1}/${layerSequence.length}: ${step.type} (${step.llm || 'auto'})`, 'DEBUG');
const stepResult = await applySelectiveLayer(currentContent, {
...config,
...step
});
currentContent = stepResult.content;
pipelineStats.steps.push({
order: i + 1,
...step,
elementsEnhanced: stepResult.stats.elementsEnhanced,
duration: stepResult.stats.duration,
success: !stepResult.stats.fallback
});
pipelineStats.totalModifications += stepResult.stats.elementsEnhanced;
} catch (stepError) {
logSh(` ❌ Étape ${i + 1} échouée: ${stepError.message}`, 'ERROR');
pipelineStats.steps.push({
order: i + 1,
...step,
error: stepError.message,
success: false
});
}
}
const duration = Date.now() - startTime;
const successfulSteps = pipelineStats.steps.filter(s => s.success).length;
logSh(`✅ PIPELINE SELECTIVE: ${successfulSteps}/${layerSequence.length} étapes | ${pipelineStats.totalModifications} modifications (${duration}ms)`, 'INFO');
await tracer.event('Pipeline selective appliqué', { ...pipelineStats, totalDuration: duration });
return {
content: currentContent,
stats: { ...pipelineStats, totalDuration: duration },
original: content
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ PIPELINE SELECTIVE ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
return {
content,
stats: { error: error.message, duration, success: false },
original: content,
fallback: true
};
}
}, { content: Object.keys(content), layerSequence, config });
}
// ============= HELPER FUNCTIONS =============
/**
* Analyser besoins selective enhancement
*/
async function analyzeSelectiveNeeds(content, csvData) {
const analysis = {
technical: { needed: false, score: 0, elements: [] },
transitions: { needed: false, score: 0, elements: [] },
style: { needed: false, score: 0, elements: [] }
};
// Analyser chaque élément pour tous types de besoins
Object.entries(content).forEach(([tag, text]) => {
// Analyse technique (import depuis SelectiveCore logic)
const technicalNeed = assessTechnicalNeed(text, csvData);
if (technicalNeed.score > 0.3) {
analysis.technical.needed = true;
analysis.technical.score += technicalNeed.score;
analysis.technical.elements.push({ tag, score: technicalNeed.score });
}
// Analyse transitions
const transitionNeed = assessTransitionNeed(text);
if (transitionNeed.score > 0.3) {
analysis.transitions.needed = true;
analysis.transitions.score += transitionNeed.score;
analysis.transitions.elements.push({ tag, score: transitionNeed.score });
}
// Analyse style
const styleNeed = assessStyleNeed(text, csvData?.personality);
if (styleNeed.score > 0.3) {
analysis.style.needed = true;
analysis.style.score += styleNeed.score;
analysis.style.elements.push({ tag, score: styleNeed.score });
}
});
// Normaliser scores
const elementCount = Object.keys(content).length;
analysis.technical.score = analysis.technical.score / elementCount;
analysis.transitions.score = analysis.transitions.score / elementCount;
analysis.style.score = analysis.style.score / elementCount;
return analysis;
}
/**
* Évaluer besoin technique (simplifié de SelectiveCore)
*/
function assessTechnicalNeed(content, csvData) {
let score = 0;
// Manque de termes techniques spécifiques
if (csvData?.mc0) {
const technicalTerms = ['dibond', 'pmma', 'aluminium', 'fraisage', 'impression', 'gravure'];
const foundTerms = technicalTerms.filter(term => content.toLowerCase().includes(term));
if (foundTerms.length === 0 && content.length > 100) {
score += 0.4;
}
}
// Vocabulaire générique
const genericWords = ['produit', 'solution', 'service', 'qualité'];
const genericCount = genericWords.filter(word => content.toLowerCase().includes(word)).length;
if (genericCount > 2) score += 0.3;
return { score: Math.min(1, score) };
}
/**
* Évaluer besoin transitions (simplifié)
*/
function assessTransitionNeed(content) {
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) return { score: 0 };
let score = 0;
// Connecteurs répétitifs
const connectors = ['par ailleurs', 'en effet', 'de plus'];
let repetitions = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(connector, 'gi')) || []);
if (matches.length > 1) repetitions++;
});
if (repetitions > 1) score += 0.4;
return { score: Math.min(1, score) };
}
/**
* Évaluer besoin style (simplifié)
*/
function assessStyleNeed(content, personality) {
let score = 0;
if (!personality) {
score += 0.2;
return { score };
}
// Style générique
const personalityWords = (personality.vocabulairePref || '').toLowerCase().split(',');
const personalityFound = personalityWords.some(word =>
word.trim() && content.toLowerCase().includes(word.trim())
);
if (!personalityFound && content.length > 50) score += 0.4;
return { score: Math.min(1, score) };
}
/**
* Obtenir stacks disponibles
*/
function getAvailableStacks() {
return Object.values(PREDEFINED_STACKS);
}
module.exports = {
// Main functions
applyPredefinedStack,
applyAdaptiveLayers,
applyLayerPipeline,
// Utils
getAvailableStacks,
analyzeSelectiveNeeds,
// Constants
PREDEFINED_STACKS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/DetectorStrategies.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// DETECTOR STRATEGIES - NIVEAU 3
// Responsabilité: Stratégies spécialisées par détecteur IA
// Anti-détection: Techniques ciblées contre chaque analyseur
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* STRATÉGIES DÉTECTEUR PAR DÉTECTEUR
* Chaque classe implémente une approche spécialisée
*/
class BaseDetectorStrategy {
constructor(name) {
this.name = name;
this.effectiveness = 0.8;
this.targetMetrics = [];
}
/**
* Générer instructions spécifiques pour ce détecteur
*/
generateInstructions(elementType, personality, csvData) {
throw new Error('generateInstructions must be implemented by subclass');
}
/**
* Obtenir instructions anti-détection (NOUVEAU pour modularité)
*/
getInstructions(intensity = 1.0) {
throw new Error('getInstructions must be implemented by subclass');
}
/**
* Obtenir conseils d'amélioration (NOUVEAU pour modularité)
*/
getEnhancementTips(intensity = 1.0) {
throw new Error('getEnhancementTips must be implemented by subclass');
}
/**
* Analyser efficacité contre ce détecteur
*/
analyzeEffectiveness(content) {
return {
detector: this.name,
effectiveness: this.effectiveness,
metrics: this.analyzeContent(content)
};
}
/**
* Analyser contenu selon métriques de ce détecteur
*/
analyzeContent(content) {
return {
wordCount: content.split(/\s+/).length,
sentenceCount: content.split(/[.!?]+/).length
};
}
}
/**
* STRATÉGIE ANTI-GPTZERO
* Focus: Imprévisibilité et variation syntaxique
*/
class GPTZeroStrategy extends BaseDetectorStrategy {
constructor() {
super('GPTZero');
this.effectiveness = 0.9;
this.targetMetrics = ['perplexity', 'burstiness', 'unpredictability'];
this.techniques = {
syntaxVariation: {
name: 'Variation syntaxique drastique',
rules: [
'Alterne phrases simples (5-8 mots) et complexes (25-35 mots)',
'Utilise structures atypiques : inversion, anacoluthe, ellipse',
'Intercale incises, parenthèses, tirets pour briser linéarité',
'Varie types : déclarative → interrogative → exclamative'
]
},
lexicalUnpredictability: {
name: 'Imprévisibilité lexicale',
rules: [
'Synonymes rares ou détournés : "performant" → "qui tient la route"',
'Néologismes justifiés : "sur-mesurable" pour "très mesurable"',
'Registres mélangés : soutenu puis familier dans même paragraphe',
'Évite collocations attendues : "haute qualité" → "qualité qui dépote"'
]
},
narrativeDisruption: {
name: 'Disruption narrative',
rules: [
'Apartés personnels : "(j\'ai testé, c\'est bluffant)"',
'Questions rhétoriques inattendues au milieu des faits',
'Changements de perspective soudains : technique → humain → commercial',
'Digressions courtes puis retour au sujet'
]
}
};
}
generateInstructions(elementType, personality, csvData) {
const instructions = [];
instructions.push(`ANTI-GPTZERO - MAXIMUM IMPRÉVISIBILITÉ:`);
// Techniques syntaxiques
instructions.push(`\nSYNTAXE VARIABLE:`);
this.techniques.syntaxVariation.rules.forEach(rule => {
instructions.push(`${rule}`);
});
// Imprévisibilité lexicale
instructions.push(`\nLEXIQUE IMPRÉVISIBLE:`);
this.techniques.lexicalUnpredictability.rules.forEach(rule => {
instructions.push(`${rule}`);
});
// Adaptation selon type élément
if (elementType === 'titre_h1' || elementType === 'titre_h2') {
instructions.push(`\nTITRES ANTI-GPTZERO:`);
instructions.push(`• Évite formules attendues : "Guide complet" → "Le vrai topo sur"`);
instructions.push(`• Structure atypique : question, exclamation, fragment`);
} else if (elementType === 'texte') {
instructions.push(`\nTEXTES ANTI-GPTZERO:`);
this.techniques.narrativeDisruption.rules.forEach(rule => {
instructions.push(`${rule}`);
});
}
// Adaptation personnalité
if (personality) {
instructions.push(`\nAVEC STYLE ${personality.nom.toUpperCase()}:`);
instructions.push(`• Pousse ${personality.style} vers extrêmes : plus marqué, moins lisse`);
instructions.push(`• Accentue tics de langage : répétitions, expressions favorites`);
}
return instructions.join('\n');
}
/**
* Instructions modulaires pour AdversarialCore
*/
getInstructions(intensity = 1.0) {
const baseRules = [
'Surprends avec tournures inattendues et constructions atypiques',
'Évite phrases trop prévisibles ou patterns répétitifs',
'Utilise synonymes moins évidents et vocabulaire moins commun',
'Varie drastiquement la complexité syntaxique entre phrases'
];
const intensiveRules = [
'Intercale observations personnelles ou détours narratifs',
'Alterne registres de langue (soutenu → familier → technique)',
'Brise la logique linéaire avec des parenthèses, incises, apartés',
'Créé ruptures narratives et changements de perspective soudains'
];
return intensity >= 1.0 ? [...baseRules, ...intensiveRules] : baseRules;
}
/**
* Conseils d'amélioration pour enhancement method
*/
getEnhancementTips(intensity = 1.0) {
return [
'Remplace connecteurs prévisibles par variations inattendues',
'Ajoute incises courtes pour briser la linéarité',
'Varie longueurs phrases dans même paragraphe',
'Utilise synonymes moins courants mais naturels',
...(intensity > 0.8 ? [
'Insère questions rhétoriques ponctuelles',
'Ajoute nuances et hésitations authentiques'
] : [])
];
}
analyzeContent(content) {
const baseMetrics = super.analyzeContent(content);
// Analyse perplexité approximative
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 5);
const sentenceLengths = sentences.map(s => s.split(/\s+/).length);
// Variance longueur (proxy pour burstiness)
const avgLength = sentenceLengths.reduce((a, b) => a + b, 0) / sentenceLengths.length;
const variance = sentenceLengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / sentenceLengths.length;
const burstiness = Math.sqrt(variance) / avgLength;
// Diversité lexicale (proxy pour imprévisibilité)
const words = content.toLowerCase().split(/\s+/).filter(w => w.length > 2);
const uniqueWords = [...new Set(words)];
const lexicalDiversity = uniqueWords.length / words.length;
return {
...baseMetrics,
burstiness: Math.round(burstiness * 100) / 100,
lexicalDiversity: Math.round(lexicalDiversity * 100) / 100,
avgSentenceLength: Math.round(avgLength),
gptZeroRiskLevel: this.calculateGPTZeroRisk(burstiness, lexicalDiversity)
};
}
calculateGPTZeroRisk(burstiness, lexicalDiversity) {
// Heuristique : GPTZero détecte uniformité faible + diversité faible
const uniformityScore = Math.min(burstiness, 1) * 100;
const diversityScore = lexicalDiversity * 100;
const combinedScore = (uniformityScore + diversityScore) / 2;
if (combinedScore > 70) return 'low';
if (combinedScore > 40) return 'medium';
return 'high';
}
}
/**
* STRATÉGIE ANTI-ORIGINALITY
* Focus: Diversité sémantique et originalité
*/
class OriginalityStrategy extends BaseDetectorStrategy {
constructor() {
super('Originality');
this.effectiveness = 0.85;
this.targetMetrics = ['semantic_diversity', 'originality_score', 'vocabulary_range'];
this.techniques = {
semanticCreativity: {
name: 'Créativité sémantique',
rules: [
'Métaphores inattendues : "cette plaque, c\'est le passeport de votre façade"',
'Comparaisons originales : évite clichés, invente analogies',
'Reformulations créatives : "résistant aux intempéries" → "qui brave les saisons"',
'Néologismes justifiés et expressifs'
]
},
perspectiveShifting: {
name: 'Changements de perspective',
rules: [
'Angles multiples sur même info : technique → esthétique → pratique',
'Points de vue variés : fabricant, utilisateur, installateur, voisin',
'Temporalités mélangées : présent, futur proche, retour d\'expérience',
'Niveaux d\'abstraction : détail précis puis vue d\'ensemble'
]
},
linguisticInventiveness: {
name: 'Inventivité linguistique',
rules: [
'Jeux de mots subtils et expressions détournées',
'Régionalismes et références culturelles précises',
'Vocabulaire technique humanisé avec créativité',
'Rythmes et sonorités travaillés : allitérations, assonances'
]
}
};
}
generateInstructions(elementType, personality, csvData) {
const instructions = [];
instructions.push(`ANTI-ORIGINALITY - MAXIMUM CRÉATIVITÉ SÉMANTIQUE:`);
// Créativité sémantique
instructions.push(`\nCRÉATIVITÉ SÉMANTIQUE:`);
this.techniques.semanticCreativity.rules.forEach(rule => {
instructions.push(`${rule}`);
});
// Changements de perspective
instructions.push(`\nPERSPECTIVES MULTIPLES:`);
this.techniques.perspectiveShifting.rules.forEach(rule => {
instructions.push(`${rule}`);
});
// Spécialisation par élément
if (elementType === 'intro') {
instructions.push(`\nINTROS ANTI-ORIGINALITY:`);
instructions.push(`• Commence par angle totalement inattendu pour le sujet`);
instructions.push(`• Évite intro-types, réinvente présentation du sujet`);
instructions.push(`• Crée surprise puis retour naturel au cœur du sujet`);
} else if (elementType.includes('faq')) {
instructions.push(`\nFAQ ANTI-ORIGINALITY:`);
instructions.push(`• Questions vraiment originales, pas standard secteur`);
instructions.push(`• Réponses avec angles créatifs et exemples inédits`);
}
// Contexte métier créatif
if (csvData && csvData.mc0) {
instructions.push(`\nCRÉATIVITÉ CONTEXTUELLE ${csvData.mc0.toUpperCase()}:`);
instructions.push(`• Réinvente façon de parler de ${csvData.mc0}`);
instructions.push(`• Évite vocabulaire convenu du secteur, invente expressions`);
instructions.push(`• Trouve analogies originales spécifiques à ${csvData.mc0}`);
}
// Inventivité linguistique
instructions.push(`\nINVENTIVITÉ LINGUISTIQUE:`);
this.techniques.linguisticInventiveness.rules.forEach(rule => {
instructions.push(`${rule}`);
});
return instructions.join('\n');
}
/**
* Instructions modulaires pour AdversarialCore
*/
getInstructions(intensity = 1.0) {
const baseRules = [
'Vocabulaire TRÈS varié : évite répétitions même de synonymes',
'Structures phrases délibérément irrégulières et asymétriques',
'Changements angles fréquents : technique → personnel → général',
'Créativité sémantique : métaphores, comparaisons inattendues'
];
const intensiveRules = [
'Évite formulations académiques ou trop structurées',
'Intègre références culturelles, expressions régionales',
'Subvertis les attentes : commence par la fin, questionne l\'évidence',
'Réinvente façon de présenter informations basiques'
];
return intensity >= 1.0 ? [...baseRules, ...intensiveRules] : baseRules;
}
/**
* Conseils d'amélioration pour enhancement method
*/
getEnhancementTips(intensity = 1.0) {
return [
'Trouve synonymes créatifs et expressions détournées',
'Ajoute métaphores subtiles et comparaisons originales',
'Varie angles d\'approche dans même contenu',
'Utilise vocabulaire technique humanisé',
...(intensity > 0.8 ? [
'Insère références culturelles ou régionalismes',
'Crée néologismes justifiés et expressifs'
] : [])
];
}
analyzeContent(content) {
const baseMetrics = super.analyzeContent(content);
// Analyse diversité sémantique
const words = content.toLowerCase().split(/\s+/).filter(w => w.length > 3);
const uniqueWords = [...new Set(words)];
const semanticDiversity = uniqueWords.length / words.length;
// Détection créativité (heuristique)
const creativityIndicators = [
'comme', 'tel', 'sorte de', 'façon de', 'manière de', // métaphores
'(', ')', '"', // originalité structure
'?', '!', // variation tonale
];
const creativityCount = creativityIndicators.reduce((count, indicator) => {
return count + (content.match(new RegExp(indicator.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
}, 0);
const creativityScore = Math.min(100, (creativityCount / words.length) * 1000);
return {
...baseMetrics,
semanticDiversity: Math.round(semanticDiversity * 100) / 100,
creativityScore: Math.round(creativityScore),
uniqueWordRatio: Math.round((uniqueWords.length / words.length) * 100),
originalityRiskLevel: this.calculateOriginalityRisk(semanticDiversity, creativityScore)
};
}
calculateOriginalityRisk(semanticDiversity, creativityScore) {
const diversityScore = semanticDiversity * 100;
const combinedScore = (diversityScore + creativityScore) / 2;
if (combinedScore > 60) return 'low';
if (combinedScore > 35) return 'medium';
return 'high';
}
}
/**
* STRATÉGIE GÉNÉRALE
* Équilibre entre toutes les techniques
*/
class GeneralStrategy extends BaseDetectorStrategy {
constructor() {
super('General');
this.effectiveness = 0.75;
this.targetMetrics = ['overall_naturalness', 'human_likeness'];
}
generateInstructions(elementType, personality, csvData) {
const instructions = [];
instructions.push(`ANTI-DÉTECTION GÉNÉRALE - ÉQUILIBRE OPTIMAL:`);
// Instructions générales équilibrées
instructions.push(`\nNATUREL ET VARIÉ:`);
instructions.push(`• Évite mots typiques IA : "optimal", "comprehensive", "seamless", "robust"`);
instructions.push(`• Mélange phrases courtes (5-10 mots) et longues (15-25 mots) naturellement`);
instructions.push(`• Utilise expressions idiomatiques françaises authentiques`);
instructions.push(`• Intègre imperfections humaines : répétitions légères, hésitations`);
instructions.push(`• Change angle ou ton 1-2 fois selon longueur du contenu`);
// Adaptation personnalité équilibrée
if (personality) {
instructions.push(`\nSTYLE ${personality.nom} NATUREL:`);
instructions.push(`• Applique style ${personality.style} de façon authentique, pas forcée`);
instructions.push(`• Respecte vocabulaire naturel : ${personality.vocabulairePref || 'varié'}`);
instructions.push(`• Utilise connecteurs variés : ${personality.connecteursPref || 'naturels'}`);
}
return instructions.join('\n');
}
/**
* Instructions modulaires pour AdversarialCore
*/
getInstructions(intensity = 1.0) {
const baseRules = [
'Évite ces mots typiques IA : "optimal", "comprehensive", "seamless", "robust", "leverage"',
'Varie longueurs phrases : mélange phrases courtes (5-10 mots) ET longues (20-30 mots)',
'Utilise expressions françaises familières et tournures idiomatiques',
'Ajoute imperfections naturelles : répétitions légères, hésitations, reformulations'
];
const intensiveRules = [
'Change angle ou perspective 1-2 fois dans le texte',
'Préfère synonymes moins évidents : "bon" plutôt qu\'excellent", "solide" plutôt qu\'optimisé"',
'Intègre connecteurs variés et naturels selon le contexte',
'Simule variation naturelle d\'humeur et d\'énergie rédactionnelle'
];
return intensity >= 0.8 ? [...baseRules, ...intensiveRules] : baseRules;
}
/**
* Conseils d'amélioration pour enhancement method
*/
getEnhancementTips(intensity = 1.0) {
return [
'Remplace mots typiques IA par synonymes plus naturels',
'Ajoute nuances et hésitations : "peut-être", "généralement", "souvent"',
'Varie connecteurs pour éviter répétitions mécaniques',
'Personnalise avec observations subjectives légères',
...(intensity > 0.7 ? [
'Intègre "erreurs" humaines : corrections, précisions',
'Simule changement léger de ton ou d\'énergie'
] : [])
];
}
analyzeContent(content) {
const baseMetrics = super.analyzeContent(content);
// Métrique naturalité générale
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 5);
const avgWordsPerSentence = baseMetrics.wordCount / baseMetrics.sentenceCount;
// Détection mots typiques IA
const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage'];
const aiWordCount = aiWords.reduce((count, word) => {
return count + (content.toLowerCase().match(new RegExp(`\\b${word}\\b`, 'g')) || []).length;
}, 0);
const aiWordDensity = aiWordCount / baseMetrics.wordCount * 100;
const naturalness = Math.max(0, 100 - (aiWordDensity * 10) - Math.abs(avgWordsPerSentence - 15));
return {
...baseMetrics,
avgWordsPerSentence: Math.round(avgWordsPerSentence),
aiWordCount,
aiWordDensity: Math.round(aiWordDensity * 100) / 100,
naturalnessScore: Math.round(naturalness),
generalRiskLevel: naturalness > 70 ? 'low' : naturalness > 40 ? 'medium' : 'high'
};
}
}
/**
* FACTORY POUR CRÉER STRATÉGIES
*/
class DetectorStrategyFactory {
static strategies = {
'general': GeneralStrategy,
'gptZero': GPTZeroStrategy,
'originality': OriginalityStrategy
};
static createStrategy(detectorName) {
const StrategyClass = this.strategies[detectorName];
if (!StrategyClass) {
logSh(`⚠️ Stratégie inconnue: ${detectorName}, fallback vers général`, 'WARNING');
return new GeneralStrategy();
}
return new StrategyClass();
}
static getSupportedDetectors() {
return Object.keys(this.strategies).map(name => {
const strategy = this.createStrategy(name);
return {
name,
displayName: strategy.name,
effectiveness: strategy.effectiveness,
targetMetrics: strategy.targetMetrics
};
});
}
static analyzeContentAgainstAllDetectors(content) {
const results = {};
Object.keys(this.strategies).forEach(detectorName => {
const strategy = this.createStrategy(detectorName);
results[detectorName] = strategy.analyzeEffectiveness(content);
});
return results;
}
}
/**
* FONCTION UTILITAIRE - SÉLECTION STRATÉGIE OPTIMALE
*/
function selectOptimalStrategy(elementType, personality, previousResults = {}) {
// Logique de sélection intelligente
// Si résultats précédents disponibles, adapter
if (previousResults.gptZero && previousResults.gptZero.effectiveness < 0.6) {
return 'gptZero'; // Renforcer anti-GPTZero
}
if (previousResults.originality && previousResults.originality.effectiveness < 0.6) {
return 'originality'; // Renforcer anti-Originality
}
// Sélection par type d'élément
if (elementType === 'titre_h1' || elementType === 'titre_h2') {
return 'gptZero'; // Titres bénéficient imprévisibilité
}
if (elementType === 'intro' || elementType === 'texte') {
return 'originality'; // Corps bénéficie créativité sémantique
}
if (elementType.includes('faq')) {
return 'general'; // FAQ équilibre naturalité
}
// Par personnalité
if (personality) {
if (personality.style === 'créatif' || personality.style === 'original') {
return 'originality';
}
if (personality.style === 'technique' || personality.style === 'expert') {
return 'gptZero';
}
}
return 'general'; // Fallback
}
module.exports = {
DetectorStrategyFactory,
GPTZeroStrategy,
OriginalityStrategy,
GeneralStrategy,
selectOptimalStrategy,
BaseDetectorStrategy
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialCore.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ADVERSARIAL CORE - MOTEUR MODULAIRE
// Responsabilité: Moteur adversarial réutilisable sur tout contenu
// Architecture: Couches applicables à la demande
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { callLLM } = require('../LLMManager');
// Import stratégies et utilitaires
const { DetectorStrategyFactory, selectOptimalStrategy } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - APPLICATION COUCHE ADVERSARIALE
* Input: contenu existant + configuration adversariale
* Output: contenu avec couche adversariale appliquée
*/
async function applyAdversarialLayer(existingContent, config = {}) {
return await tracer.run('AdversarialCore.applyAdversarialLayer()', async () => {
const {
detectorTarget = 'general',
intensity = 1.0,
method = 'regeneration', // 'regeneration' | 'enhancement' | 'hybrid'
preserveStructure = true,
csvData = null,
context = {}
} = config;
await tracer.annotate({
adversarialLayer: true,
detectorTarget,
intensity,
method,
elementsCount: Object.keys(existingContent).length
});
const startTime = Date.now();
logSh(`🎯 APPLICATION COUCHE ADVERSARIALE: ${detectorTarget} (${method})`, 'INFO');
logSh(` 📊 ${Object.keys(existingContent).length} éléments | Intensité: ${intensity}`, 'INFO');
try {
// Initialiser stratégie détecteur
const strategy = DetectorStrategyFactory.createStrategy(detectorTarget);
// Appliquer méthode adversariale choisie
let adversarialContent = {};
switch (method) {
case 'regeneration':
adversarialContent = await applyRegenerationMethod(existingContent, config, strategy);
break;
case 'enhancement':
adversarialContent = await applyEnhancementMethod(existingContent, config, strategy);
break;
case 'hybrid':
adversarialContent = await applyHybridMethod(existingContent, config, strategy);
break;
default:
throw new Error(`Méthode adversariale inconnue: ${method}`);
}
const duration = Date.now() - startTime;
const stats = {
elementsProcessed: Object.keys(existingContent).length,
elementsModified: countModifiedElements(existingContent, adversarialContent),
detectorTarget,
intensity,
method,
duration
};
logSh(`✅ COUCHE ADVERSARIALE APPLIQUÉE: ${stats.elementsModified}/${stats.elementsProcessed} modifiés (${duration}ms)`, 'INFO');
await tracer.event('Couche adversariale appliquée', stats);
return {
content: adversarialContent,
stats,
original: existingContent,
config
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ COUCHE ADVERSARIALE ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content: existingContent,
stats: { fallback: true, duration },
original: existingContent,
config,
error: error.message
};
}
}, { existingContent: Object.keys(existingContent), config });
}
/**
* MÉTHODE RÉGÉNÉRATION - Réécrire complètement avec prompts adversariaux
*/
async function applyRegenerationMethod(existingContent, config, strategy) {
logSh(`🔄 Méthode régénération adversariale`, 'DEBUG');
const results = {};
const contentEntries = Object.entries(existingContent);
// Traiter en chunks pour éviter timeouts
const chunks = chunkArray(contentEntries, 4);
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
logSh(` 📦 Régénération chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
try {
const regenerationPrompt = createRegenerationPrompt(chunk, config, strategy);
const response = await callLLM('claude', regenerationPrompt, {
temperature: 0.7 + (config.intensity * 0.2), // Température variable selon intensité
maxTokens: 2000 * chunk.length
}, config.csvData?.personality);
const chunkResults = parseRegenerationResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} éléments régénérés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: garder contenu original pour ce chunk
chunk.forEach(([tag, content]) => {
results[tag] = content;
});
}
}
return results;
}
/**
* MÉTHODE ENHANCEMENT - Améliorer sans réécrire complètement
*/
async function applyEnhancementMethod(existingContent, config, strategy) {
logSh(`🔧 Méthode enhancement adversarial`, 'DEBUG');
const results = { ...existingContent }; // Base: contenu original
const elementsToEnhance = selectElementsForEnhancement(existingContent, config);
if (elementsToEnhance.length === 0) {
logSh(` ⏭️ Aucun élément nécessite enhancement`, 'DEBUG');
return results;
}
logSh(` 📋 ${elementsToEnhance.length} éléments sélectionnés pour enhancement`, 'DEBUG');
const enhancementPrompt = createEnhancementPrompt(elementsToEnhance, config, strategy);
try {
const response = await callLLM('gpt4', enhancementPrompt, {
temperature: 0.5 + (config.intensity * 0.3),
maxTokens: 3000
}, config.csvData?.personality);
const enhancedResults = parseEnhancementResponse(response, elementsToEnhance);
// Appliquer améliorations
Object.keys(enhancedResults).forEach(tag => {
if (enhancedResults[tag] !== existingContent[tag]) {
results[tag] = enhancedResults[tag];
}
});
return results;
} catch (error) {
logSh(`❌ Enhancement échoué: ${error.message}`, 'ERROR');
return results; // Fallback: contenu original
}
}
/**
* MÉTHODE HYBRIDE - Combinaison régénération + enhancement
*/
async function applyHybridMethod(existingContent, config, strategy) {
logSh(`⚡ Méthode hybride adversariale`, 'DEBUG');
// 1. Enhancement léger sur tout le contenu
const enhancedContent = await applyEnhancementMethod(existingContent, {
...config,
intensity: config.intensity * 0.6 // Intensité réduite pour enhancement
}, strategy);
// 2. Régénération ciblée sur éléments clés
const keyElements = selectKeyElementsForRegeneration(enhancedContent, config);
if (keyElements.length === 0) {
return enhancedContent;
}
const keyElementsContent = {};
keyElements.forEach(tag => {
keyElementsContent[tag] = enhancedContent[tag];
});
const regeneratedElements = await applyRegenerationMethod(keyElementsContent, {
...config,
intensity: config.intensity * 1.2 // Intensité augmentée pour régénération
}, strategy);
// 3. Merger résultats
const hybridContent = { ...enhancedContent };
Object.keys(regeneratedElements).forEach(tag => {
hybridContent[tag] = regeneratedElements[tag];
});
return hybridContent;
}
// ============= HELPER FUNCTIONS =============
/**
* Créer prompt de régénération adversariale
*/
function createRegenerationPrompt(chunk, config, strategy) {
const { detectorTarget, intensity, csvData } = config;
let prompt = `MISSION: Réécris ces contenus pour éviter détection par ${detectorTarget}.
TECHNIQUE ANTI-${detectorTarget.toUpperCase()}:
${strategy.getInstructions(intensity).join('\n')}
CONTENUS À RÉÉCRIRE:
${chunk.map(([tag, content], i) => `[${i + 1}] TAG: ${tag}
ORIGINAL: "${content}"`).join('\n\n')}
CONSIGNES:
- GARDE exactement le même message et informations factuelles
- CHANGE structure, vocabulaire, style pour éviter détection ${detectorTarget}
- Intensité adversariale: ${intensity.toFixed(2)}
${csvData?.personality ? `- Style: ${csvData.personality.nom} (${csvData.personality.style})` : ''}
IMPORTANT: Réponse DIRECTE par les contenus réécrits, pas d'explication.
FORMAT:
[1] Contenu réécrit anti-${detectorTarget}
[2] Contenu réécrit anti-${detectorTarget}
etc...`;
return prompt;
}
/**
* Créer prompt d'enhancement adversarial
*/
function createEnhancementPrompt(elementsToEnhance, config, strategy) {
const { detectorTarget, intensity } = config;
let prompt = `MISSION: Améliore subtilement ces contenus pour réduire détection ${detectorTarget}.
AMÉLIORATIONS CIBLÉES:
${strategy.getEnhancementTips(intensity).join('\n')}
ÉLÉMENTS À AMÉLIORER:
${elementsToEnhance.map((element, i) => `[${i + 1}] TAG: ${element.tag}
CONTENU: "${element.content}"
PROBLÈME: ${element.detectionRisk}`).join('\n\n')}
CONSIGNES:
- Modifications LÉGÈRES et naturelles
- GARDE le fond du message intact
- Focus sur réduction détection ${detectorTarget}
- Intensité: ${intensity.toFixed(2)}
FORMAT:
[1] Contenu légèrement amélioré
[2] Contenu légèrement amélioré
etc...`;
return prompt;
}
/**
* Parser réponse régénération
*/
function parseRegenerationResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const index = parseInt(match[1]) - 1;
const content = cleanAdversarialContent(match[2].trim());
if (index >= 0 && index < chunk.length) {
parsedItems[index] = content;
}
}
// Mapper aux vrais tags
chunk.forEach(([tag, originalContent], index) => {
if (parsedItems[index] && parsedItems[index].length > 10) {
results[tag] = parsedItems[index];
} else {
results[tag] = originalContent; // Fallback
logSh(`⚠️ Fallback régénération pour [${tag}]`, 'WARNING');
}
});
return results;
}
/**
* Parser réponse enhancement
*/
function parseEnhancementResponse(response, elementsToEnhance) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < elementsToEnhance.length) {
let enhancedContent = cleanAdversarialContent(match[2].trim());
const element = elementsToEnhance[index];
if (enhancedContent && enhancedContent.length > 10) {
results[element.tag] = enhancedContent;
} else {
results[element.tag] = element.content; // Fallback
}
index++;
}
return results;
}
/**
* Sélectionner éléments pour enhancement
*/
function selectElementsForEnhancement(existingContent, config) {
const elements = [];
Object.entries(existingContent).forEach(([tag, content]) => {
const detectionRisk = assessDetectionRisk(content, config.detectorTarget);
if (detectionRisk.score > 0.6) { // Risque élevé
elements.push({
tag,
content,
detectionRisk: detectionRisk.reasons.join(', '),
priority: detectionRisk.score
});
}
});
// Trier par priorité (risque élevé en premier)
elements.sort((a, b) => b.priority - a.priority);
return elements;
}
/**
* Sélectionner éléments clés pour régénération (hybride)
*/
function selectKeyElementsForRegeneration(content, config) {
const keyTags = [];
Object.keys(content).forEach(tag => {
// Éléments clés: titres, intro, premiers paragraphes
if (tag.includes('Titre') || tag.includes('H1') || tag.includes('intro') ||
tag.includes('Introduction') || tag.includes('1')) {
keyTags.push(tag);
}
});
return keyTags.slice(0, 3); // Maximum 3 éléments clés
}
/**
* Évaluer risque de détection
*/
function assessDetectionRisk(content, detectorTarget) {
let score = 0;
const reasons = [];
// Indicateurs génériques de contenu IA
const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge'];
const aiCount = aiWords.reduce((count, word) => {
return count + (content.toLowerCase().includes(word) ? 1 : 0);
}, 0);
if (aiCount > 2) {
score += 0.4;
reasons.push('mots_typiques_ia');
}
// Structure trop parfaite
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length > 2) {
const avgLength = sentences.reduce((sum, s) => sum + s.length, 0) / sentences.length;
const variance = sentences.reduce((sum, s) => sum + Math.pow(s.length - avgLength, 2), 0) / sentences.length;
const uniformity = 1 - (Math.sqrt(variance) / avgLength);
if (uniformity > 0.8) {
score += 0.3;
reasons.push('structure_uniforme');
}
}
// Spécifique selon détecteur
if (detectorTarget === 'gptZero') {
// GPTZero détecte la prévisibilité
if (content.includes('par ailleurs') && content.includes('en effet')) {
score += 0.3;
reasons.push('connecteurs_prévisibles');
}
}
return { score: Math.min(1, score), reasons };
}
/**
* Nettoyer contenu adversarial généré
*/
function cleanAdversarialContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(voici\s+)?le\s+contenu\s+(réécrit|amélioré)[:\s]*/gi, '');
content = content.replace(/^(bon,?\s*)?(alors,?\s*)?/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
/**
* Compter éléments modifiés
*/
function countModifiedElements(original, modified) {
let count = 0;
Object.keys(original).forEach(tag => {
if (modified[tag] && modified[tag] !== original[tag]) {
count++;
}
});
return count;
}
/**
* Chunk array utility
*/
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
/**
* Sleep utility
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
applyAdversarialLayer, // ← MAIN ENTRY POINT MODULAIRE
applyRegenerationMethod,
applyEnhancementMethod,
applyHybridMethod,
assessDetectionRisk,
selectElementsForEnhancement
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialLayers.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ADVERSARIAL LAYERS - COUCHES MODULAIRES
// Responsabilité: Couches adversariales composables et réutilisables
// Architecture: Fonction pipeline |> layer1 |> layer2 |> layer3
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { applyAdversarialLayer } = require('./AdversarialCore');
/**
* COUCHE ANTI-GPTZEERO - Spécialisée contre GPTZero
*/
async function applyAntiGPTZeroLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: 'gptZero',
intensity: options.intensity || 1.0,
method: options.method || 'regeneration',
...options
});
}
/**
* COUCHE ANTI-ORIGINALITY - Spécialisée contre Originality.ai
*/
async function applyAntiOriginalityLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: 'originality',
intensity: options.intensity || 1.1,
method: options.method || 'hybrid',
...options
});
}
/**
* COUCHE ANTI-WINSTON - Spécialisée contre Winston AI
*/
async function applyAntiWinstonLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: 'winston',
intensity: options.intensity || 0.9,
method: options.method || 'enhancement',
...options
});
}
/**
* COUCHE GÉNÉRALE - Protection généraliste multi-détecteurs
*/
async function applyGeneralAdversarialLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: 'general',
intensity: options.intensity || 0.8,
method: options.method || 'hybrid',
...options
});
}
/**
* COUCHE LÉGÈRE - Modifications subtiles pour préserver qualité
*/
async function applyLightAdversarialLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: options.detectorTarget || 'general',
intensity: 0.5,
method: 'enhancement',
preserveStructure: true,
...options
});
}
/**
* COUCHE INTENSIVE - Maximum anti-détection
*/
async function applyIntensiveAdversarialLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: options.detectorTarget || 'gptZero',
intensity: 1.5,
method: 'regeneration',
preserveStructure: false,
...options
});
}
/**
* PIPELINE COMPOSABLE - Application séquentielle de couches
*/
async function applyLayerPipeline(content, layers = [], globalOptions = {}) {
return await tracer.run('AdversarialLayers.applyLayerPipeline()', async () => {
await tracer.annotate({
layersPipeline: true,
layersCount: layers.length,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🔄 PIPELINE COUCHES ADVERSARIALES: ${layers.length} couches`, 'INFO');
let currentContent = content;
const pipelineStats = {
layers: [],
totalDuration: 0,
totalModifications: 0,
success: true
};
try {
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
const layerStartTime = Date.now();
logSh(` 🎯 Couche ${i + 1}/${layers.length}: ${layer.name || layer.type || 'anonyme'}`, 'DEBUG');
try {
const layerResult = await applyLayerByConfig(currentContent, layer, globalOptions);
currentContent = layerResult.content;
const layerStats = {
name: layer.name || `layer_${i + 1}`,
type: layer.type,
duration: Date.now() - layerStartTime,
modificationsCount: layerResult.stats?.elementsModified || 0,
success: true
};
pipelineStats.layers.push(layerStats);
pipelineStats.totalModifications += layerStats.modificationsCount;
logSh(`${layerStats.name}: ${layerStats.modificationsCount} modifs (${layerStats.duration}ms)`, 'DEBUG');
} catch (error) {
logSh(` ❌ Couche ${i + 1} échouée: ${error.message}`, 'ERROR');
pipelineStats.layers.push({
name: layer.name || `layer_${i + 1}`,
type: layer.type,
duration: Date.now() - layerStartTime,
success: false,
error: error.message
});
// Continuer avec le contenu précédent si une couche échoue
if (!globalOptions.stopOnError) {
continue;
} else {
throw error;
}
}
}
pipelineStats.totalDuration = Date.now() - startTime;
pipelineStats.success = pipelineStats.layers.every(layer => layer.success);
logSh(`🔄 PIPELINE TERMINÉ: ${pipelineStats.totalModifications} modifs totales (${pipelineStats.totalDuration}ms)`, 'INFO');
await tracer.event('Pipeline couches terminé', pipelineStats);
return {
content: currentContent,
stats: pipelineStats,
original: content
};
} catch (error) {
pipelineStats.totalDuration = Date.now() - startTime;
pipelineStats.success = false;
logSh(`❌ PIPELINE COUCHES ÉCHOUÉ après ${pipelineStats.totalDuration}ms: ${error.message}`, 'ERROR');
throw error;
}
}, { layers: layers.map(l => l.name || l.type), content: Object.keys(content) });
}
/**
* COUCHES PRÉDÉFINIES - Configurations courantes
*/
const PREDEFINED_LAYERS = {
// Stack défensif léger
lightDefense: [
{ type: 'general', name: 'General Light', intensity: 0.6, method: 'enhancement' },
{ type: 'anti-gptZero', name: 'GPTZero Light', intensity: 0.5, method: 'enhancement' }
],
// Stack défensif standard
standardDefense: [
{ type: 'general', name: 'General Standard', intensity: 0.8, method: 'hybrid' },
{ type: 'anti-gptZero', name: 'GPTZero Standard', intensity: 0.9, method: 'enhancement' },
{ type: 'anti-originality', name: 'Originality Standard', intensity: 0.8, method: 'enhancement' }
],
// Stack défensif intensif
heavyDefense: [
{ type: 'general', name: 'General Heavy', intensity: 1.0, method: 'regeneration' },
{ type: 'anti-gptZero', name: 'GPTZero Heavy', intensity: 1.2, method: 'regeneration' },
{ type: 'anti-originality', name: 'Originality Heavy', intensity: 1.1, method: 'hybrid' },
{ type: 'anti-winston', name: 'Winston Heavy', intensity: 1.0, method: 'enhancement' }
],
// Stack ciblé GPTZero
gptZeroFocused: [
{ type: 'anti-gptZero', name: 'GPTZero Primary', intensity: 1.3, method: 'regeneration' },
{ type: 'general', name: 'General Support', intensity: 0.7, method: 'enhancement' }
],
// Stack ciblé Originality
originalityFocused: [
{ type: 'anti-originality', name: 'Originality Primary', intensity: 1.4, method: 'hybrid' },
{ type: 'general', name: 'General Support', intensity: 0.8, method: 'enhancement' }
]
};
/**
* APPLIQUER STACK PRÉDÉFINI
*/
async function applyPredefinedStack(content, stackName, options = {}) {
const stack = PREDEFINED_LAYERS[stackName];
if (!stack) {
throw new Error(`Stack prédéfini inconnu: ${stackName}. Disponibles: ${Object.keys(PREDEFINED_LAYERS).join(', ')}`);
}
logSh(`📦 APPLICATION STACK PRÉDÉFINI: ${stackName}`, 'INFO');
return await applyLayerPipeline(content, stack, options);
}
/**
* COUCHES ADAPTATIVES - S'adaptent selon le contenu
*/
async function applyAdaptiveLayers(content, options = {}) {
const {
targetDetectors = ['gptZero', 'originality'],
maxIntensity = 1.0,
analysisMode = true
} = options;
logSh(`🧠 COUCHES ADAPTATIVES: Analyse + adaptation auto`, 'INFO');
// 1. Analyser le contenu pour détecter les risques
const contentAnalysis = analyzeContentRisks(content);
// 2. Construire pipeline adaptatif selon l'analyse
const adaptiveLayers = [];
// Niveau de base selon risque global
const baseIntensity = Math.min(maxIntensity, contentAnalysis.globalRisk * 1.2);
if (baseIntensity > 0.3) {
adaptiveLayers.push({
type: 'general',
name: 'Adaptive Base',
intensity: baseIntensity,
method: baseIntensity > 0.7 ? 'hybrid' : 'enhancement'
});
}
// Couches spécifiques selon détecteurs ciblés
targetDetectors.forEach(detector => {
const detectorRisk = contentAnalysis.detectorRisks[detector] || 0;
if (detectorRisk > 0.4) {
const intensity = Math.min(maxIntensity * 1.1, detectorRisk * 1.5);
adaptiveLayers.push({
type: `anti-${detector}`,
name: `Adaptive ${detector}`,
intensity,
method: intensity > 0.8 ? 'regeneration' : 'enhancement'
});
}
});
logSh(` 🎯 ${adaptiveLayers.length} couches adaptatives générées`, 'DEBUG');
if (adaptiveLayers.length === 0) {
logSh(` ✅ Contenu déjà optimal, aucune couche nécessaire`, 'INFO');
return { content, stats: { adaptive: true, layersApplied: 0 }, original: content };
}
return await applyLayerPipeline(content, adaptiveLayers, options);
}
// ============= HELPER FUNCTIONS =============
/**
* Appliquer couche selon configuration
*/
async function applyLayerByConfig(content, layerConfig, globalOptions = {}) {
const { type, intensity, method, ...layerOptions } = layerConfig;
const options = { ...globalOptions, ...layerOptions, intensity, method };
switch (type) {
case 'general':
return await applyGeneralAdversarialLayer(content, options);
case 'anti-gptZero':
return await applyAntiGPTZeroLayer(content, options);
case 'anti-originality':
return await applyAntiOriginalityLayer(content, options);
case 'anti-winston':
return await applyAntiWinstonLayer(content, options);
case 'light':
return await applyLightAdversarialLayer(content, options);
case 'intensive':
return await applyIntensiveAdversarialLayer(content, options);
default:
throw new Error(`Type de couche inconnu: ${type}`);
}
}
/**
* Analyser risques du contenu pour adaptation
*/
function analyzeContentRisks(content) {
const analysis = {
globalRisk: 0,
detectorRisks: {},
riskFactors: []
};
const allContent = Object.values(content).join(' ');
// Risques génériques
let riskScore = 0;
// 1. Mots typiques IA
const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge', 'furthermore', 'moreover'];
const aiWordCount = aiWords.filter(word => allContent.toLowerCase().includes(word)).length;
if (aiWordCount > 2) {
riskScore += 0.3;
analysis.riskFactors.push(`mots_ia: ${aiWordCount}`);
}
// 2. Structure uniforme
const contentLengths = Object.values(content).map(c => c.length);
const avgLength = contentLengths.reduce((a, b) => a + b, 0) / contentLengths.length;
const variance = contentLengths.reduce((sum, len) => sum + Math.pow(len - avgLength, 2), 0) / contentLengths.length;
const uniformity = 1 - (Math.sqrt(variance) / Math.max(avgLength, 1));
if (uniformity > 0.8) {
riskScore += 0.2;
analysis.riskFactors.push(`uniformité: ${uniformity.toFixed(2)}`);
}
// 3. Connecteurs répétitifs
const repetitiveConnectors = ['par ailleurs', 'en effet', 'de plus', 'cependant'];
const connectorCount = repetitiveConnectors.filter(conn =>
(allContent.match(new RegExp(conn, 'gi')) || []).length > 1
).length;
if (connectorCount > 2) {
riskScore += 0.2;
analysis.riskFactors.push(`connecteurs_répétitifs: ${connectorCount}`);
}
analysis.globalRisk = Math.min(1, riskScore);
// Risques spécifiques par détecteur
analysis.detectorRisks = {
gptZero: analysis.globalRisk + (uniformity > 0.7 ? 0.3 : 0),
originality: analysis.globalRisk + (aiWordCount > 3 ? 0.4 : 0),
winston: analysis.globalRisk + (connectorCount > 2 ? 0.2 : 0)
};
return analysis;
}
/**
* Obtenir informations sur les stacks disponibles
*/
function getAvailableStacks() {
return Object.keys(PREDEFINED_LAYERS).map(stackName => ({
name: stackName,
layersCount: PREDEFINED_LAYERS[stackName].length,
description: getStackDescription(stackName),
layers: PREDEFINED_LAYERS[stackName]
}));
}
/**
* Description des stacks prédéfinis
*/
function getStackDescription(stackName) {
const descriptions = {
lightDefense: 'Protection légère préservant la qualité',
standardDefense: 'Protection équilibrée multi-détecteurs',
heavyDefense: 'Protection maximale tous détecteurs',
gptZeroFocused: 'Optimisation spécifique anti-GPTZero',
originalityFocused: 'Optimisation spécifique anti-Originality.ai'
};
return descriptions[stackName] || 'Stack personnalisé';
}
module.exports = {
// Couches individuelles
applyAntiGPTZeroLayer,
applyAntiOriginalityLayer,
applyAntiWinstonLayer,
applyGeneralAdversarialLayer,
applyLightAdversarialLayer,
applyIntensiveAdversarialLayer,
// Pipeline et stacks
applyLayerPipeline, // ← MAIN ENTRY POINT PIPELINE
applyPredefinedStack, // ← MAIN ENTRY POINT STACKS
applyAdaptiveLayers, // ← MAIN ENTRY POINT ADAPTATIF
// Utilitaires
getAvailableStacks,
analyzeContentRisks,
PREDEFINED_LAYERS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/human-simulation/FatiguePatterns.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: FatiguePatterns.js
// RESPONSABILITÉ: Simulation fatigue cognitive
// Implémentation courbe fatigue exacte du plan.md
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* PROFILS DE FATIGUE PAR PERSONNALITÉ
* Basé sur les 15 personnalités du système
*/
const FATIGUE_PROFILES = {
// Techniques - Résistent plus longtemps
marc: { peakAt: 0.45, recovery: 0.85, intensity: 0.8 },
amara: { peakAt: 0.43, recovery: 0.87, intensity: 0.7 },
yasmine: { peakAt: 0.47, recovery: 0.83, intensity: 0.75 },
fabrice: { peakAt: 0.44, recovery: 0.86, intensity: 0.8 },
// Créatifs - Fatigue plus variable
sophie: { peakAt: 0.55, recovery: 0.90, intensity: 1.0 },
émilie: { peakAt: 0.52, recovery: 0.88, intensity: 0.9 },
chloé: { peakAt: 0.58, recovery: 0.92, intensity: 1.1 },
minh: { peakAt: 0.53, recovery: 0.89, intensity: 0.95 },
// Commerciaux - Fatigue rapide mais récupération
laurent: { peakAt: 0.40, recovery: 0.80, intensity: 1.2 },
julie: { peakAt: 0.38, recovery: 0.78, intensity: 1.0 },
// Terrain - Endurance élevée
kévin: { peakAt: 0.35, recovery: 0.75, intensity: 0.6 },
mamadou: { peakAt: 0.37, recovery: 0.77, intensity: 0.65 },
linh: { peakAt: 0.36, recovery: 0.76, intensity: 0.7 },
// Patrimoniaux - Fatigue progressive
'pierre-henri': { peakAt: 0.48, recovery: 0.82, intensity: 0.85 },
thierry: { peakAt: 0.46, recovery: 0.84, intensity: 0.8 },
// Profil par défaut
default: { peakAt: 0.50, recovery: 0.85, intensity: 1.0 }
};
/**
* CALCUL FATIGUE COGNITIVE - FORMULE EXACTE DU PLAN
* Peak à 50% de progression selon courbe sinusoïdale
* @param {number} elementIndex - Position élément (0-based)
* @param {number} totalElements - Nombre total d'éléments
* @returns {number} - Niveau fatigue (0-0.8)
*/
function calculateFatigue(elementIndex, totalElements) {
if (totalElements <= 1) return 0;
const position = elementIndex / totalElements;
const fatigueLevel = Math.sin(position * Math.PI) * 0.8; // Peak à 50%
logSh(`🧠 Fatigue calculée: position=${position.toFixed(2)}, niveau=${fatigueLevel.toFixed(2)}`, 'DEBUG');
return Math.max(0, fatigueLevel);
}
/**
* OBTENIR PROFIL FATIGUE PAR PERSONNALITÉ
* @param {string} personalityName - Nom personnalité
* @returns {object} - Profil fatigue
*/
function getFatigueProfile(personalityName) {
const normalizedName = personalityName?.toLowerCase() || 'default';
const profile = FATIGUE_PROFILES[normalizedName] || FATIGUE_PROFILES.default;
logSh(`🎭 Profil fatigue sélectionné pour ${personalityName}: peakAt=${profile.peakAt}, intensity=${profile.intensity}`, 'DEBUG');
return profile;
}
/**
* INJECTION MARQUEURS DE FATIGUE
* @param {string} content - Contenu à modifier
* @param {number} fatigueLevel - Niveau fatigue (0-0.8)
* @param {object} options - Options { profile, intensity }
* @returns {object} - { content, modifications }
*/
function injectFatigueMarkers(content, fatigueLevel, options = {}) {
if (!content || fatigueLevel < 0.05) { // FIXÉ: Seuil beaucoup plus bas (était 0.2)
return { content, modifications: 0 };
}
const profile = options.profile || FATIGUE_PROFILES.default;
const baseIntensity = options.intensity || 1.0;
// Intensité ajustée selon personnalité
const adjustedIntensity = fatigueLevel * profile.intensity * baseIntensity;
logSh(`💤 Injection fatigue: niveau=${fatigueLevel.toFixed(2)}, intensité=${adjustedIntensity.toFixed(2)}`, 'DEBUG');
let modifiedContent = content;
let modifications = 0;
// ========================================
// FATIGUE LÉGÈRE (0.05 - 0.4) - FIXÉ: Seuil plus bas
// ========================================
if (fatigueLevel >= 0.05 && fatigueLevel < 0.4) {
const lightFatigueResult = applyLightFatigue(modifiedContent, adjustedIntensity);
modifiedContent = lightFatigueResult.content;
modifications += lightFatigueResult.count;
}
// ========================================
// FATIGUE MODÉRÉE (0.4 - 0.6)
// ========================================
if (fatigueLevel >= 0.4 && fatigueLevel < 0.6) {
const moderateFatigueResult = applyModerateFatigue(modifiedContent, adjustedIntensity);
modifiedContent = moderateFatigueResult.content;
modifications += moderateFatigueResult.count;
}
// ========================================
// FATIGUE ÉLEVÉE (0.6+)
// ========================================
if (fatigueLevel >= 0.6) {
const heavyFatigueResult = applyHeavyFatigue(modifiedContent, adjustedIntensity);
modifiedContent = heavyFatigueResult.content;
modifications += heavyFatigueResult.count;
}
logSh(`💤 Fatigue appliquée: ${modifications} modifications`, 'DEBUG');
return {
content: modifiedContent,
modifications
};
}
/**
* FATIGUE LÉGÈRE - Connecteurs simplifiés
*/
function applyLightFatigue(content, intensity) {
let modified = content;
let count = 0;
// Probabilité d'application basée sur l'intensité - ENCORE PLUS AGRESSIF
const shouldApply = Math.random() < (intensity * 0.9); // FIXÉ: 90% chance d'appliquer
if (!shouldApply) return { content: modified, count };
// Simplification des connecteurs complexes - ÉLARGI
const complexConnectors = [
{ from: /néanmoins/gi, to: 'cependant' },
{ from: /par conséquent/gi, to: 'donc' },
{ from: /ainsi que/gi, to: 'et' },
{ from: /en outre/gi, to: 'aussi' },
{ from: /de surcroît/gi, to: 'de plus' },
// NOUVEAUX AJOUTS AGRESSIFS
{ from: /toutefois/gi, to: 'mais' },
{ from: /cependant/gi, to: 'mais bon' },
{ from: /par ailleurs/gi, to: 'sinon' },
{ from: /en effet/gi, to: 'effectivement' },
{ from: /de fait/gi, to: 'en fait' }
];
complexConnectors.forEach(connector => {
const matches = modified.match(connector.from);
if (matches && Math.random() < 0.9) { // FIXÉ: 90% chance très agressive
modified = modified.replace(connector.from, connector.to);
count++;
}
});
// AJOUT FIX: Si aucun connecteur complexe trouvé, appliquer une modification alternative
if (count === 0 && Math.random() < 0.7) {
// Injecter des simplifications basiques
if (modified.includes(' et ') && Math.random() < 0.5) {
modified = modified.replace(' et ', ' puis ');
count++;
}
}
return { content: modified, count };
}
/**
* FATIGUE MODÉRÉE - Phrases plus courtes
*/
function applyModerateFatigue(content, intensity) {
let modified = content;
let count = 0;
const shouldApply = Math.random() < (intensity * 0.5);
if (!shouldApply) return { content: modified, count };
// Découpage phrases longues (>120 caractères)
const sentences = modified.split('. ');
const processedSentences = sentences.map(sentence => {
if (sentence.length > 120 && Math.random() < 0.3) { // 30% chance
// Trouver un point de découpe logique
const cutPoints = [', qui', ', que', ', dont', ' et ', ' car '];
for (const cutPoint of cutPoints) {
const cutIndex = sentence.indexOf(cutPoint);
if (cutIndex > 30 && cutIndex < sentence.length - 30) {
count++;
return sentence.substring(0, cutIndex) + '. ' +
sentence.substring(cutIndex + cutPoint.length);
}
}
}
return sentence;
});
modified = processedSentences.join('. ');
// Vocabulaire plus simple
const simplifications = [
{ from: /optimisation/gi, to: 'amélioration' },
{ from: /méthodologie/gi, to: 'méthode' },
{ from: /problématique/gi, to: 'problème' },
{ from: /spécifications/gi, to: 'détails' }
];
simplifications.forEach(simpl => {
if (modified.match(simpl.from) && Math.random() < 0.3) {
modified = modified.replace(simpl.from, simpl.to);
count++;
}
});
return { content: modified, count };
}
/**
* FATIGUE ÉLEVÉE - Répétitions et vocabulaire basique
*/
function applyHeavyFatigue(content, intensity) {
let modified = content;
let count = 0;
const shouldApply = Math.random() < (intensity * 0.7);
if (!shouldApply) return { content: modified, count };
// Injection répétitions naturelles
const repetitionWords = ['bien', 'très', 'vraiment', 'assez', 'plutôt'];
const sentences = modified.split('. ');
sentences.forEach((sentence, index) => {
if (Math.random() < 0.2 && sentence.length > 50) { // 20% chance
const word = repetitionWords[Math.floor(Math.random() * repetitionWords.length)];
// Injecter le mot répétitif au milieu de la phrase
const words = sentence.split(' ');
const insertIndex = Math.floor(words.length / 2);
words.splice(insertIndex, 0, word);
sentences[index] = words.join(' ');
count++;
}
});
modified = sentences.join('. ');
// Vocabulaire très basique
const basicVocab = [
{ from: /excellente?/gi, to: 'bonne' },
{ from: /remarquable/gi, to: 'bien' },
{ from: /sophistiqué/gi, to: 'avancé' },
{ from: /performant/gi, to: 'efficace' },
{ from: /innovations?/gi, to: 'nouveautés' }
];
basicVocab.forEach(vocab => {
if (modified.match(vocab.from) && Math.random() < 0.4) {
modified = modified.replace(vocab.from, vocab.to);
count++;
}
});
// Hésitations légères (rare)
if (Math.random() < 0.1) { // 10% chance
const hesitations = ['... enfin', '... disons', '... comment dire'];
const hesitation = hesitations[Math.floor(Math.random() * hesitations.length)];
const words = modified.split(' ');
const insertIndex = Math.floor(words.length * 0.7); // Vers la fin
words.splice(insertIndex, 0, hesitation);
modified = words.join(' ');
count++;
}
return { content: modified, count };
}
/**
* RÉCUPÉRATION FATIGUE (pour les éléments en fin)
* @param {string} content - Contenu à traiter
* @param {number} recoveryLevel - Niveau récupération (0-1)
* @returns {object} - { content, modifications }
*/
function applyFatigueRecovery(content, recoveryLevel) {
if (recoveryLevel < 0.8) return { content, modifications: 0 };
let modified = content;
let count = 0;
// Réintroduire vocabulaire plus sophistiqué
const recoveryVocab = [
{ from: /\bbien\b/gi, to: 'excellent' },
{ from: /\befficace\b/gi, to: 'performant' },
{ from: /\bméthode\b/gi, to: 'méthodologie' }
];
recoveryVocab.forEach(vocab => {
if (modified.match(vocab.from) && Math.random() < 0.3) {
modified = modified.replace(vocab.from, vocab.to);
count++;
}
});
return { content: modified, count };
}
// ============= EXPORTS =============
module.exports = {
calculateFatigue,
getFatigueProfile,
injectFatigueMarkers,
applyLightFatigue,
applyModerateFatigue,
applyHeavyFatigue,
applyFatigueRecovery,
FATIGUE_PROFILES
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/human-simulation/PersonalityErrors.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: PersonalityErrors.js
// RESPONSABILITÉ: Erreurs cohérentes par personnalité
// 15 profils d'erreurs basés sur les personnalités système
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* PATTERNS D'ERREURS PAR PERSONNALITÉ
* Basé sur les 15 personnalités du BrainConfig
* Chaque personnalité a ses tics linguistiques et erreurs typiques
*/
const PERSONALITY_ERROR_PATTERNS = {
// ========================================
// PERSONNALITÉS TECHNIQUES
// ========================================
marc: {
name: 'Marc - Expert Technique',
tendencies: ['sur-technicisation', 'anglicismes techniques', 'jargon professionnel'],
repetitions: ['précis', 'efficace', 'optimal', 'performant', 'système'],
syntaxErrors: [
'phrases techniques non finies',
'parenthèses explicatives excessives',
'abréviations sans développement'
],
vocabularyTics: ['niveau technique', 'en termes de', 'au niveau de'],
anglicisms: ['upgrade', 'process', 'workflow', 'pipeline'],
errorFrequency: 0.7 // Probabilité base
},
amara: {
name: 'Amara - Ingénieure Système',
tendencies: ['méthodologie rigide', 'références normes', 'vocabulaire industriel'],
repetitions: ['conforme', 'standard', 'spécifications', 'protocole'],
syntaxErrors: ['énumérations lourdes', 'références normatives'],
vocabularyTics: ['selon les normes', 'conformément à', 'dans le respect de'],
anglicisms: ['compliance', 'standard', 'guidelines'],
errorFrequency: 0.6
},
yasmine: {
name: 'Yasmine - GreenTech',
tendencies: ['éco-vocabulaire répétitif', 'superlatifs environnementaux'],
repetitions: ['durable', 'écologique', 'responsable', 'vert', 'bio'],
syntaxErrors: ['accumulation adjectifs éco', 'phrases militantes'],
vocabularyTics: ['respectueux de l\'environnement', 'développement durable'],
anglicisms: ['green', 'eco-friendly', 'sustainable'],
errorFrequency: 0.8
},
fabrice: {
name: 'Fabrice - Métallurgie',
tendencies: ['vocabulaire métier spécialisé', 'références techniques'],
repetitions: ['résistant', 'robuste', 'solide', 'qualité', 'finition'],
syntaxErrors: ['termes techniques sans explication'],
vocabularyTics: ['en terme de résistance', 'question de solidité'],
anglicisms: ['coating', 'finish', 'design'],
errorFrequency: 0.5
},
// ========================================
// PERSONNALITÉS CRÉATIVES
// ========================================
sophie: {
name: 'Sophie - Déco Design',
tendencies: ['vocabulaire déco répétitif', 'superlatifs esthétiques'],
repetitions: ['magnifique', 'élégant', 'harmonieux', 'raffiné', 'style'],
syntaxErrors: ['accord couleurs/matières', 'accumulation adjectifs'],
vocabularyTics: ['en terme de style', 'au niveau esthétique', 'côté design'],
anglicisms: ['design', 'style', 'trendy', 'vintage'],
errorFrequency: 0.9
},
émilie: {
name: 'Émilie - Digital Native',
tendencies: ['anglicismes numériques', 'vocabulaire web'],
repetitions: ['digital', 'online', 'connecté', 'smart', 'moderne'],
syntaxErrors: ['néologismes numériques'],
vocabularyTics: ['au niveau digital', 'côté technologique'],
anglicisms: ['user-friendly', 'responsive', 'digital', 'smart'],
errorFrequency: 1.0
},
chloé: {
name: 'Chloé - Content Creator',
tendencies: ['ton familier', 'expressions actuelles', 'anglicismes réseaux'],
repetitions: ['super', 'génial', 'top', 'incontournable', 'tendance'],
syntaxErrors: ['familiarités', 'expressions jeunes'],
vocabularyTics: ['c\'est vraiment', 'on va dire que', 'du coup'],
anglicisms: ['content', 'trending', 'viral', 'lifestyle'],
errorFrequency: 1.1
},
minh: {
name: 'Minh - Designer Industriel',
tendencies: ['références design', 'vocabulaire forme/fonction'],
repetitions: ['fonctionnel', 'ergonomique', 'esthétique', 'innovant'],
syntaxErrors: ['descriptions techniques design'],
vocabularyTics: ['en terme de design', 'niveau ergonomie'],
anglicisms: ['design', 'user experience', 'ergonomic'],
errorFrequency: 0.7
},
// ========================================
// PERSONNALITÉS COMMERCIALES
// ========================================
laurent: {
name: 'Laurent - Commercial BtoB',
tendencies: ['vocabulaire vente', 'superlatifs commerciaux'],
repetitions: ['excellent', 'exceptionnel', 'unique', 'incontournable'],
syntaxErrors: ['promesses excessives', 'superlatifs empilés'],
vocabularyTics: ['c\'est vraiment', 'je vous garantis', 'sans aucun doute'],
anglicisms: ['business', 'deal', 'top niveau'],
errorFrequency: 1.2
},
julie: {
name: 'Julie - Architecture Commerciale',
tendencies: ['vocabulaire technique commercial', 'références projets'],
repetitions: ['projet', 'réalisation', 'conception', 'sur-mesure'],
syntaxErrors: ['énumérations projets'],
vocabularyTics: ['dans le cadre de', 'au niveau projet'],
anglicisms: ['design', 'custom', 'high-end'],
errorFrequency: 0.8
},
// ========================================
// PERSONNALITÉS TERRAIN
// ========================================
kévin: {
name: 'Kévin - Homme de Terrain',
tendencies: ['expressions familières', 'vocabulaire pratique'],
repetitions: ['pratique', 'concret', 'simple', 'direct', 'efficace'],
syntaxErrors: ['tournures familières', 'expressions populaires'],
vocabularyTics: ['franchement', 'concrètement', 'dans les faits'],
anglicisms: ['basique', 'standard'],
errorFrequency: 0.6
},
mamadou: {
name: 'Mamadou - Artisan Expérimenté',
tendencies: ['références tradition', 'vocabulaire métier'],
repetitions: ['traditionnel', 'artisanal', 'savoir-faire', 'qualité'],
syntaxErrors: ['expressions métier', 'références tradition'],
vocabularyTics: ['comme on dit', 'dans le métier', 'selon l\'expérience'],
anglicisms: [], // Évite les anglicismes
errorFrequency: 0.4
},
linh: {
name: 'Linh - Production Industrielle',
tendencies: ['vocabulaire production', 'références process'],
repetitions: ['production', 'fabrication', 'process', 'qualité', 'série'],
syntaxErrors: ['termes production techniques'],
vocabularyTics: ['au niveau production', 'côté fabrication'],
anglicisms: ['process', 'manufacturing', 'quality'],
errorFrequency: 0.5
},
// ========================================
// PERSONNALITÉS PATRIMOINE
// ========================================
'pierre-henri': {
name: 'Pierre-Henri - Patrimoine Classique',
tendencies: ['vocabulaire soutenu', 'références historiques'],
repetitions: ['traditionnel', 'authentique', 'noble', 'raffinement', 'héritage'],
syntaxErrors: ['formulations recherchées', 'références culturelles'],
vocabularyTics: ['il convient de', 'il est à noter que', 'dans la tradition'],
anglicisms: [], // Évite complètement
errorFrequency: 0.3
},
thierry: {
name: 'Thierry - Créole Authentique',
tendencies: ['expressions créoles', 'tournures locales'],
repetitions: ['authentique', 'local', 'tradition', 'racines'],
syntaxErrors: ['tournures créoles', 'expressions locales'],
vocabularyTics: ['comme on dit chez nous', 'dans nos traditions'],
anglicisms: [], // Privilégie le français local
errorFrequency: 0.8
}
};
/**
* OBTENIR PROFIL D'ERREURS PAR PERSONNALITÉ
* @param {string} personalityName - Nom personnalité
* @returns {object} - Profil d'erreurs
*/
function getPersonalityErrorPatterns(personalityName) {
const normalizedName = personalityName?.toLowerCase() || 'default';
const profile = PERSONALITY_ERROR_PATTERNS[normalizedName];
if (!profile) {
logSh(`⚠️ Profil erreurs non trouvé pour ${personalityName}, utilisation profil générique`, 'WARNING');
return createGenericErrorProfile();
}
logSh(`🎭 Profil erreurs sélectionné pour ${personalityName}: ${profile.name}`, 'DEBUG');
return profile;
}
/**
* PROFIL D'ERREURS GÉNÉRIQUE
*/
function createGenericErrorProfile() {
return {
name: 'Profil Générique',
tendencies: ['répétitions standard', 'vocabulaire neutre'],
repetitions: ['bien', 'bon', 'intéressant', 'important'],
syntaxErrors: ['phrases standards'],
vocabularyTics: ['en effet', 'par ailleurs', 'de plus'],
anglicisms: [],
errorFrequency: 0.5
};
}
/**
* INJECTION ERREURS PERSONNALITÉ
* @param {string} content - Contenu à modifier
* @param {object} personalityProfile - Profil personnalité
* @param {number} intensity - Intensité (0-2.0)
* @returns {object} - { content, modifications }
*/
function injectPersonalityErrors(content, personalityProfile, intensity = 1.0) {
if (!content || !personalityProfile) {
return { content, modifications: 0 };
}
logSh(`🎭 Injection erreurs personnalité: ${personalityProfile.name}`, 'DEBUG');
let modifiedContent = content;
let modifications = 0;
// Probabilité d'application basée sur l'intensité et la fréquence du profil
const baseFrequency = personalityProfile.errorFrequency || 0.5;
const adjustedProbability = Math.min(1.0, baseFrequency * intensity);
logSh(`🎯 Probabilité erreurs: ${adjustedProbability.toFixed(2)} (base: ${baseFrequency}, intensité: ${intensity})`, 'DEBUG');
// ========================================
// 1. RÉPÉTITIONS CARACTÉRISTIQUES
// ========================================
const repetitionResult = injectRepetitions(modifiedContent, personalityProfile, adjustedProbability);
modifiedContent = repetitionResult.content;
modifications += repetitionResult.count;
// ========================================
// 2. TICS VOCABULAIRE
// ========================================
const vocabularyResult = injectVocabularyTics(modifiedContent, personalityProfile, adjustedProbability);
modifiedContent = vocabularyResult.content;
modifications += vocabularyResult.count;
// ========================================
// 3. ANGLICISMES (SI APPLICABLE)
// ========================================
if (personalityProfile.anglicisms && personalityProfile.anglicisms.length > 0) {
const anglicismResult = injectAnglicisms(modifiedContent, personalityProfile, adjustedProbability * 0.3);
modifiedContent = anglicismResult.content;
modifications += anglicismResult.count;
}
// ========================================
// 4. ERREURS SYNTAXIQUES TYPIQUES
// ========================================
const syntaxResult = injectSyntaxErrors(modifiedContent, personalityProfile, adjustedProbability * 0.2);
modifiedContent = syntaxResult.content;
modifications += syntaxResult.count;
logSh(`🎭 Erreurs personnalité injectées: ${modifications} modifications`, 'DEBUG');
return {
content: modifiedContent,
modifications
};
}
/**
* INJECTION RÉPÉTITIONS CARACTÉRISTIQUES
*/
function injectRepetitions(content, profile, probability) {
let modified = content;
let count = 0;
if (!profile.repetitions || profile.repetitions.length === 0) {
return { content: modified, count };
}
// Sélectionner 1-3 mots répétitifs pour ce contenu - FIXÉ: Plus de mots
const selectedWords = profile.repetitions
.sort(() => 0.5 - Math.random())
.slice(0, Math.random() < 0.5 ? 2 : 3); // FIXÉ: Au moins 2 mots sélectionnés
selectedWords.forEach(word => {
if (Math.random() < probability) {
// Chercher des endroits appropriés pour injecter le mot
const sentences = modified.split('. ');
const targetSentenceIndex = Math.floor(Math.random() * sentences.length);
if (sentences[targetSentenceIndex] &&
sentences[targetSentenceIndex].length > 30 &&
!sentences[targetSentenceIndex].toLowerCase().includes(word.toLowerCase())) {
// Injecter le mot de façon naturelle
const words = sentences[targetSentenceIndex].split(' ');
const insertIndex = Math.floor(words.length * (0.3 + Math.random() * 0.4)); // 30-70% de la phrase
// Adaptations contextuelles
const adaptedWord = adaptWordToContext(word, words[insertIndex] || '');
words.splice(insertIndex, 0, adaptedWord);
sentences[targetSentenceIndex] = words.join(' ');
modified = sentences.join('. ');
count++;
logSh(` 📝 Répétition injectée: "${adaptedWord}" dans phrase ${targetSentenceIndex + 1}`, 'DEBUG');
}
}
});
return { content: modified, count };
}
/**
* INJECTION TICS VOCABULAIRE
*/
function injectVocabularyTics(content, profile, probability) {
let modified = content;
let count = 0;
if (!profile.vocabularyTics || profile.vocabularyTics.length === 0) {
return { content: modified, count };
}
const selectedTics = profile.vocabularyTics.slice(0, 1); // Un seul tic par contenu
selectedTics.forEach(tic => {
if (Math.random() < probability * 0.8) { // Probabilité réduite pour les tics
// Remplacer des connecteurs standards par le tic
const standardConnectors = ['par ailleurs', 'de plus', 'également', 'aussi'];
standardConnectors.forEach(connector => {
const regex = new RegExp(`\\b${connector}\\b`, 'gi');
if (modified.match(regex) && Math.random() < 0.4) {
modified = modified.replace(regex, tic);
count++;
logSh(` 🗣️ Tic vocabulaire: "${connector}" → "${tic}"`, 'DEBUG');
}
});
}
});
return { content: modified, count };
}
/**
* INJECTION ANGLICISMES
*/
function injectAnglicisms(content, profile, probability) {
let modified = content;
let count = 0;
if (!profile.anglicisms || profile.anglicisms.length === 0) {
return { content: modified, count };
}
// Remplacements français → anglais
const replacements = {
'processus': 'process',
'conception': 'design',
'flux de travail': 'workflow',
'mise à jour': 'upgrade',
'contenu': 'content',
'tendance': 'trending',
'intelligent': 'smart',
'numérique': 'digital'
};
Object.entries(replacements).forEach(([french, english]) => {
if (profile.anglicisms.includes(english) && Math.random() < probability) {
const regex = new RegExp(`\\b${french}\\b`, 'gi');
if (modified.match(regex)) {
modified = modified.replace(regex, english);
count++;
logSh(` 🇬🇧 Anglicisme: "${french}" → "${english}"`, 'DEBUG');
}
}
});
return { content: modified, count };
}
/**
* INJECTION ERREURS SYNTAXIQUES
*/
function injectSyntaxErrors(content, profile, probability) {
let modified = content;
let count = 0;
if (Math.random() > probability) {
return { content: modified, count };
}
// Erreurs syntaxiques légères selon la personnalité
if (profile.name.includes('Marc') || profile.name.includes('Technique')) {
// Parenthèses techniques excessives
if (Math.random() < 0.3) {
modified = modified.replace(/(\w+)/, '$1 (système)');
count++;
logSh(` 🔧 Erreur technique: parenthèses ajoutées`, 'DEBUG');
}
}
if (profile.name.includes('Sophie') || profile.name.includes('Déco')) {
// Accumulation d'adjectifs
if (Math.random() < 0.3) {
modified = modified.replace(/élégant/gi, 'élégant et raffiné');
count++;
logSh(` 🎨 Erreur déco: adjectifs accumulés`, 'DEBUG');
}
}
if (profile.name.includes('Laurent') || profile.name.includes('Commercial')) {
// Superlatifs empilés
if (Math.random() < 0.3) {
modified = modified.replace(/excellent/gi, 'vraiment excellent');
count++;
logSh(` 💼 Erreur commerciale: superlatifs empilés`, 'DEBUG');
}
}
return { content: modified, count };
}
/**
* ADAPTATION CONTEXTUELLE DES MOTS
*/
function adaptWordToContext(word, contextWord) {
// Accords basiques
const contextLower = contextWord.toLowerCase();
// Accords féminins simples
if (contextLower.includes('la ') || contextLower.endsWith('e')) {
if (word === 'bon') return 'bonne';
if (word === 'précis') return 'précise';
}
return word;
}
// ============= EXPORTS =============
module.exports = {
getPersonalityErrorPatterns,
injectPersonalityErrors,
injectRepetitions,
injectVocabularyTics,
injectAnglicisms,
injectSyntaxErrors,
createGenericErrorProfile,
adaptWordToContext,
PERSONALITY_ERROR_PATTERNS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/human-simulation/TemporalStyles.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: TemporalStyles.js
// RESPONSABILITÉ: Variations temporelles d'écriture
// Simulation comportement humain selon l'heure
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* STYLES TEMPORELS PAR TRANCHES HORAIRES
* Simule l'énergie et style d'écriture selon l'heure
*/
const TEMPORAL_STYLES = {
// ========================================
// MATIN (6h-11h) - Énergique et Direct
// ========================================
morning: {
period: 'matin',
timeRange: [6, 11],
energy: 'high',
characteristics: {
sentenceLength: 'short', // Phrases plus courtes
vocabulary: 'dynamic', // Mots énergiques
connectors: 'direct', // Connecteurs simples
rhythm: 'fast' // Rythme soutenu
},
vocabularyPreferences: {
energy: ['dynamique', 'efficace', 'rapide', 'direct', 'actif', 'performant'],
connectors: ['donc', 'puis', 'ensuite', 'maintenant', 'immédiatement'],
modifiers: ['très', 'vraiment', 'particulièrement', 'nettement'],
actions: ['optimiser', 'accélérer', 'améliorer', 'développer', 'créer']
},
styleTendencies: {
shortSentencesBias: 0.7, // 70% chance phrases courtes
directConnectorsBias: 0.8, // 80% connecteurs simples
energyWordsBias: 0.6 // 60% mots énergiques
}
},
// ========================================
// APRÈS-MIDI (12h-17h) - Équilibré et Professionnel
// ========================================
afternoon: {
period: 'après-midi',
timeRange: [12, 17],
energy: 'medium',
characteristics: {
sentenceLength: 'medium', // Phrases équilibrées
vocabulary: 'professional', // Vocabulaire standard
connectors: 'balanced', // Connecteurs variés
rhythm: 'steady' // Rythme régulier
},
vocabularyPreferences: {
energy: ['professionnel', 'efficace', 'qualité', 'standard', 'adapté'],
connectors: ['par ailleurs', 'de plus', 'également', 'ainsi', 'cependant'],
modifiers: ['assez', 'plutôt', 'relativement', 'suffisamment'],
actions: ['réaliser', 'développer', 'analyser', 'étudier', 'concevoir']
},
styleTendencies: {
shortSentencesBias: 0.4, // 40% phrases courtes
directConnectorsBias: 0.5, // 50% connecteurs simples
energyWordsBias: 0.3 // 30% mots énergiques
}
},
// ========================================
// SOIR (18h-23h) - Détendu et Réflexif
// ========================================
evening: {
period: 'soir',
timeRange: [18, 23],
energy: 'low',
characteristics: {
sentenceLength: 'long', // Phrases plus longues
vocabulary: 'nuanced', // Vocabulaire nuancé
connectors: 'complex', // Connecteurs élaborés
rhythm: 'relaxed' // Rythme posé
},
vocabularyPreferences: {
energy: ['approfondi', 'réfléchi', 'considéré', 'nuancé', 'détaillé'],
connectors: ['néanmoins', 'cependant', 'par conséquent', 'en outre', 'toutefois'],
modifiers: ['quelque peu', 'relativement', 'dans une certaine mesure', 'assez'],
actions: ['examiner', 'considérer', 'réfléchir', 'approfondir', 'explorer']
},
styleTendencies: {
shortSentencesBias: 0.2, // 20% phrases courtes
directConnectorsBias: 0.2, // 20% connecteurs simples
energyWordsBias: 0.1 // 10% mots énergiques
}
},
// ========================================
// NUIT (0h-5h) - Fatigue et Simplicité
// ========================================
night: {
period: 'nuit',
timeRange: [0, 5],
energy: 'very_low',
characteristics: {
sentenceLength: 'short', // Phrases courtes par fatigue
vocabulary: 'simple', // Vocabulaire basique
connectors: 'minimal', // Connecteurs rares
rhythm: 'slow' // Rythme lent
},
vocabularyPreferences: {
energy: ['simple', 'basique', 'standard', 'normal', 'classique'],
connectors: ['et', 'mais', 'ou', 'donc', 'puis'],
modifiers: ['assez', 'bien', 'pas mal', 'correct'],
actions: ['faire', 'utiliser', 'prendre', 'mettre', 'avoir']
},
styleTendencies: {
shortSentencesBias: 0.8, // 80% phrases courtes
directConnectorsBias: 0.9, // 90% connecteurs simples
energyWordsBias: 0.1 // 10% mots énergiques
}
}
};
/**
* DÉTERMINER STYLE TEMPOREL SELON L'HEURE
* @param {number} currentHour - Heure actuelle (0-23)
* @returns {object} - Style temporel correspondant
*/
function getTemporalStyle(currentHour) {
// Validation heure
const hour = Math.max(0, Math.min(23, Math.floor(currentHour || new Date().getHours())));
logSh(`⏰ Détermination style temporel pour ${hour}h`, 'DEBUG');
// Déterminer période
let selectedStyle;
if (hour >= 6 && hour <= 11) {
selectedStyle = TEMPORAL_STYLES.morning;
} else if (hour >= 12 && hour <= 17) {
selectedStyle = TEMPORAL_STYLES.afternoon;
} else if (hour >= 18 && hour <= 23) {
selectedStyle = TEMPORAL_STYLES.evening;
} else {
selectedStyle = TEMPORAL_STYLES.night;
}
logSh(`⏰ Style temporel sélectionné: ${selectedStyle.period} (énergie: ${selectedStyle.energy})`, 'DEBUG');
return {
...selectedStyle,
currentHour: hour,
timestamp: new Date().toISOString()
};
}
/**
* APPLICATION STYLE TEMPOREL
* @param {string} content - Contenu à modifier
* @param {object} temporalStyle - Style temporel à appliquer
* @param {object} options - Options { intensity }
* @returns {object} - { content, modifications }
*/
function applyTemporalStyle(content, temporalStyle, options = {}) {
if (!content || !temporalStyle) {
return { content, modifications: 0 };
}
const intensity = options.intensity || 1.0;
logSh(`⏰ Application style temporel: ${temporalStyle.period} (intensité: ${intensity})`, 'DEBUG');
let modifiedContent = content;
let modifications = 0;
// ========================================
// 1. AJUSTEMENT LONGUEUR PHRASES
// ========================================
const sentenceResult = adjustSentenceLength(modifiedContent, temporalStyle, intensity);
modifiedContent = sentenceResult.content;
modifications += sentenceResult.count;
// ========================================
// 2. ADAPTATION VOCABULAIRE
// ========================================
const vocabularyResult = adaptVocabulary(modifiedContent, temporalStyle, intensity);
modifiedContent = vocabularyResult.content;
modifications += vocabularyResult.count;
// ========================================
// 3. MODIFICATION CONNECTEURS
// ========================================
const connectorResult = adjustConnectors(modifiedContent, temporalStyle, intensity);
modifiedContent = connectorResult.content;
modifications += connectorResult.count;
// ========================================
// 4. AJUSTEMENT RYTHME
// ========================================
const rhythmResult = adjustRhythm(modifiedContent, temporalStyle, intensity);
modifiedContent = rhythmResult.content;
modifications += rhythmResult.count;
logSh(`⏰ Style temporel appliqué: ${modifications} modifications`, 'DEBUG');
return {
content: modifiedContent,
modifications
};
}
/**
* AJUSTEMENT LONGUEUR PHRASES
*/
function adjustSentenceLength(content, temporalStyle, intensity) {
let modified = content;
let count = 0;
const bias = temporalStyle.styleTendencies.shortSentencesBias * intensity;
const sentences = modified.split('. ');
// Probabilité d'appliquer les modifications
if (Math.random() > intensity * 0.9) { // FIXÉ: Presque toujours appliquer (était 0.7)
return { content: modified, count };
}
const processedSentences = sentences.map(sentence => {
if (sentence.length < 20) return sentence; // Ignorer phrases très courtes
// Style MATIN/NUIT - Raccourcir phrases longues
if ((temporalStyle.period === 'matin' || temporalStyle.period === 'nuit') &&
sentence.length > 100 && Math.random() < bias) {
// Chercher point de coupe naturel
const cutPoints = [', qui', ', que', ', dont', ' et ', ' car ', ' mais '];
for (const cutPoint of cutPoints) {
const cutIndex = sentence.indexOf(cutPoint);
if (cutIndex > 30 && cutIndex < sentence.length - 30) {
count++;
logSh(` ✂️ Phrase raccourcie (${temporalStyle.period}): ${sentence.length}${cutIndex} chars`, 'DEBUG');
return sentence.substring(0, cutIndex) + '. ' +
sentence.substring(cutIndex + cutPoint.length);
}
}
}
// Style SOIR - Allonger phrases courtes
if (temporalStyle.period === 'soir' &&
sentence.length > 30 && sentence.length < 80 &&
Math.random() < (1 - bias)) {
// Ajouter développements
const developments = [
', ce qui constitue un avantage notable',
', permettant ainsi d\'optimiser les résultats',
', dans une démarche d\'amélioration continue',
', contribuant à l\'efficacité globale'
];
const development = developments[Math.floor(Math.random() * developments.length)];
count++;
logSh(` 📝 Phrase allongée (soir): ${sentence.length}${sentence.length + development.length} chars`, 'DEBUG');
return sentence + development;
}
return sentence;
});
modified = processedSentences.join('. ');
return { content: modified, count };
}
/**
* ADAPTATION VOCABULAIRE
*/
function adaptVocabulary(content, temporalStyle, intensity) {
let modified = content;
let count = 0;
const vocabularyPrefs = temporalStyle.vocabularyPreferences;
const energyBias = temporalStyle.styleTendencies.energyWordsBias * intensity;
// Probabilité d'appliquer
if (Math.random() > intensity * 0.9) { // FIXÉ: Presque toujours appliquer (était 0.6)
return { content: modified, count };
}
// Remplacements selon période
const replacements = buildVocabularyReplacements(temporalStyle.period, vocabularyPrefs);
replacements.forEach(replacement => {
if (Math.random() < Math.max(0.6, energyBias)) { // FIXÉ: Minimum 60% chance
const regex = new RegExp(`\\b${replacement.from}\\b`, 'gi');
if (modified.match(regex)) {
modified = modified.replace(regex, replacement.to);
count++;
logSh(` 📚 Vocabulaire adapté (${temporalStyle.period}): "${replacement.from}" → "${replacement.to}"`, 'DEBUG');
}
}
});
// AJOUT FIX: Si aucun remplacement, forcer au moins une modification temporelle basique
if (count === 0 && Math.random() < 0.5) {
// Modification basique selon période
if (temporalStyle.period === 'matin' && modified.includes('utiliser')) {
modified = modified.replace(/\butiliser\b/gi, 'optimiser');
count++;
logSh(` 📚 Modification temporelle forcée: utiliser → optimiser`, 'DEBUG');
}
}
return { content: modified, count };
}
/**
* CONSTRUCTION REMPLACEMENTS VOCABULAIRE
*/
function buildVocabularyReplacements(period, vocabPrefs) {
const replacements = [];
switch (period) {
case 'matin':
replacements.push(
{ from: 'bon', to: 'excellent' },
{ from: 'intéressant', to: 'dynamique' },
{ from: 'utiliser', to: 'optimiser' },
{ from: 'faire', to: 'créer' }
);
break;
case 'soir':
replacements.push(
{ from: 'bon', to: 'considérable' },
{ from: 'faire', to: 'examiner' },
{ from: 'utiliser', to: 'exploiter' },
{ from: 'voir', to: 'considérer' }
);
break;
case 'nuit':
replacements.push(
{ from: 'excellent', to: 'bien' },
{ from: 'optimiser', to: 'utiliser' },
{ from: 'considérable', to: 'correct' },
{ from: 'examiner', to: 'regarder' }
);
break;
default: // après-midi
// Vocabulaire équilibré - pas de remplacements drastiques
break;
}
return replacements;
}
/**
* AJUSTEMENT CONNECTEURS
*/
function adjustConnectors(content, temporalStyle, intensity) {
let modified = content;
let count = 0;
const connectorBias = temporalStyle.styleTendencies.directConnectorsBias * intensity;
const preferredConnectors = temporalStyle.vocabularyPreferences.connectors;
if (Math.random() > intensity * 0.5) {
return { content: modified, count };
}
// Connecteurs selon période
const connectorMappings = {
matin: [
{ from: /par conséquent/gi, to: 'donc' },
{ from: /néanmoins/gi, to: 'mais' },
{ from: /en outre/gi, to: 'aussi' }
],
soir: [
{ from: /donc/gi, to: 'par conséquent' },
{ from: /mais/gi, to: 'néanmoins' },
{ from: /aussi/gi, to: 'en outre' }
],
nuit: [
{ from: /par conséquent/gi, to: 'donc' },
{ from: /néanmoins/gi, to: 'mais' },
{ from: /cependant/gi, to: 'mais' }
]
};
const mappings = connectorMappings[temporalStyle.period] || [];
mappings.forEach(mapping => {
if (Math.random() < connectorBias) {
if (modified.match(mapping.from)) {
modified = modified.replace(mapping.from, mapping.to);
count++;
logSh(` 🔗 Connecteur adapté (${temporalStyle.period}): "${mapping.from}" → "${mapping.to}"`, 'DEBUG');
}
}
});
return { content: modified, count };
}
/**
* AJUSTEMENT RYTHME
*/
function adjustRhythm(content, temporalStyle, intensity) {
let modified = content;
let count = 0;
// Le rythme affecte la ponctuation et les pauses
if (Math.random() > intensity * 0.3) {
return { content: modified, count };
}
switch (temporalStyle.characteristics.rhythm) {
case 'fast': // Matin - moins de virgules, plus direct
if (Math.random() < 0.4) {
// Supprimer quelques virgules non essentielles
const originalCommas = (modified.match(/,/g) || []).length;
modified = modified.replace(/, qui /gi, ' qui ');
modified = modified.replace(/, que /gi, ' que ');
const newCommas = (modified.match(/,/g) || []).length;
count = originalCommas - newCommas;
if (count > 0) {
logSh(` ⚡ Rythme accéléré: ${count} virgules supprimées`, 'DEBUG');
}
}
break;
case 'relaxed': // Soir - plus de pauses
if (Math.random() < 0.3) {
// Ajouter quelques pauses réflexives
modified = modified.replace(/\. ([A-Z])/g, '. Ainsi, $1');
count++;
logSh(` 🧘 Rythme ralenti: pauses ajoutées`, 'DEBUG');
}
break;
case 'slow': // Nuit - simplification
if (Math.random() < 0.5) {
// Simplifier structures complexes
modified = modified.replace(/ ; /g, '. ');
count++;
logSh(` 😴 Rythme simplifié: structures allégées`, 'DEBUG');
}
break;
}
return { content: modified, count };
}
/**
* ANALYSE COHÉRENCE TEMPORELLE
* @param {string} content - Contenu à analyser
* @param {object} temporalStyle - Style appliqué
* @returns {object} - Métriques de cohérence
*/
function analyzeTemporalCoherence(content, temporalStyle) {
const sentences = content.split('. ');
const avgSentenceLength = sentences.reduce((sum, s) => sum + s.length, 0) / sentences.length;
const energyWords = temporalStyle.vocabularyPreferences.energy;
const energyWordCount = energyWords.reduce((count, word) => {
const regex = new RegExp(`\\b${word}\\b`, 'gi');
return count + (content.match(regex) || []).length;
}, 0);
return {
avgSentenceLength,
energyWordDensity: energyWordCount / sentences.length,
period: temporalStyle.period,
coherenceScore: calculateCoherenceScore(avgSentenceLength, temporalStyle),
expectedCharacteristics: temporalStyle.characteristics
};
}
/**
* CALCUL SCORE COHÉRENCE
*/
function calculateCoherenceScore(avgLength, temporalStyle) {
let score = 1.0;
// Vérifier cohérence longueur phrases avec période
const expectedLength = {
'matin': { min: 40, max: 80 },
'après-midi': { min: 60, max: 120 },
'soir': { min: 80, max: 150 },
'nuit': { min: 30, max: 70 }
};
const expected = expectedLength[temporalStyle.period];
if (expected) {
if (avgLength < expected.min || avgLength > expected.max) {
score *= 0.7;
}
}
return Math.max(0, Math.min(1, score));
}
// ============= EXPORTS =============
module.exports = {
getTemporalStyle,
applyTemporalStyle,
adjustSentenceLength,
adaptVocabulary,
adjustConnectors,
adjustRhythm,
analyzeTemporalCoherence,
calculateCoherenceScore,
buildVocabularyReplacements,
TEMPORAL_STYLES
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/human-simulation/HumanSimulationUtils.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: HumanSimulationUtils.js
// RESPONSABILITÉ: Utilitaires partagés Human Simulation
// Fonctions d'analyse, validation et helpers
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* SEUILS DE QUALITÉ
*/
const QUALITY_THRESHOLDS = {
readability: {
minimum: 0.3, // FIXÉ: Plus permissif (était 0.6)
good: 0.6,
excellent: 0.8
},
keywordPreservation: {
minimum: 0.7, // FIXÉ: Plus permissif (était 0.8)
good: 0.9,
excellent: 0.95
},
similarity: {
minimum: 0.5, // FIXÉ: Plus permissif (était 0.7)
maximum: 1.0 // FIXÉ: Accepter même contenu identique (était 0.95)
}
};
/**
* MOTS-CLÉS À PRÉSERVER ABSOLUMENT
*/
const CRITICAL_KEYWORDS = [
// Mots-clés SEO génériques
'plaque', 'personnalisée', 'gravure', 'métal', 'bois', 'acrylique',
'design', 'qualité', 'fabrication', 'artisanal', 'sur-mesure',
// Termes techniques importants
'laser', 'CNC', 'impression', 'découpe', 'finition', 'traitement',
// Termes commerciaux
'prix', 'tarif', 'devis', 'livraison', 'garantie', 'service'
];
/**
* ANALYSE COMPLEXITÉ CONTENU
* @param {object} content - Contenu à analyser
* @returns {object} - Métriques de complexité
*/
function analyzeContentComplexity(content) {
logSh('🔍 Analyse complexité contenu', 'DEBUG');
const contentArray = Object.values(content).filter(c => typeof c === 'string');
const totalText = contentArray.join(' ');
// Métriques de base
const totalWords = totalText.split(/\s+/).length;
const totalSentences = totalText.split(/[.!?]+/).length;
const totalParagraphs = contentArray.length;
// Complexité lexicale
const uniqueWords = new Set(totalText.toLowerCase().split(/\s+/)).size;
const lexicalDiversity = uniqueWords / totalWords;
// Longueur moyenne des phrases
const avgSentenceLength = totalWords / totalSentences;
// Complexité syntaxique (approximative)
const complexConnectors = (totalText.match(/néanmoins|cependant|par conséquent|en outre|toutefois/gi) || []).length;
const syntacticComplexity = complexConnectors / totalSentences;
// Score global de complexité
const complexityScore = (
(lexicalDiversity * 0.4) +
(Math.min(avgSentenceLength / 100, 1) * 0.3) +
(syntacticComplexity * 0.3)
);
const complexity = {
totalWords,
totalSentences,
totalParagraphs,
avgSentenceLength,
lexicalDiversity,
syntacticComplexity,
complexityScore,
level: complexityScore > 0.7 ? 'high' : complexityScore > 0.4 ? 'medium' : 'low'
};
logSh(` 📊 Complexité: ${complexity.level} (score: ${complexityScore.toFixed(2)})`, 'DEBUG');
logSh(` 📝 ${totalWords} mots, ${totalSentences} phrases, diversité: ${lexicalDiversity.toFixed(2)}`, 'DEBUG');
return complexity;
}
/**
* CALCUL SCORE LISIBILITÉ
* Approximation de l'index Flesch-Kincaid adapté au français
* @param {string} text - Texte à analyser
* @returns {number} - Score lisibilité (0-1)
*/
function calculateReadabilityScore(text) {
if (!text || text.trim().length === 0) {
return 0;
}
// Nettoyage du texte
const cleanText = text.replace(/[^\w\s.!?]/gi, '');
// Comptages de base
const sentences = cleanText.split(/[.!?]+/).filter(s => s.trim().length > 0);
const words = cleanText.split(/\s+/).filter(w => w.length > 0);
const syllables = countSyllables(cleanText);
if (sentences.length === 0 || words.length === 0) {
return 0;
}
// Métriques Flesch-Kincaid adaptées français
const avgWordsPerSentence = words.length / sentences.length;
const avgSyllablesPerWord = syllables / words.length;
// Formule adaptée (plus clémente que l'originale)
const fleschScore = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllablesPerWord);
// Normalisation 0-1 (100 = parfait en Flesch)
const normalizedScore = Math.max(0, Math.min(1, fleschScore / 100));
logSh(` 📖 Lisibilité: ${normalizedScore.toFixed(2)} (mots/phrase: ${avgWordsPerSentence.toFixed(1)}, syll/mot: ${avgSyllablesPerWord.toFixed(1)})`, 'DEBUG');
return normalizedScore;
}
/**
* COMPTAGE SYLLABES (APPROXIMATIF FRANÇAIS)
*/
function countSyllables(text) {
// Approximation pour le français
const vowels = /[aeiouyàáâäèéêëìíîïòóôöùúûü]/gi;
const vowelGroups = text.match(vowels) || [];
// Approximation: 1 groupe de voyelles ≈ 1 syllabe
// Ajustements pour le français
let syllables = vowelGroups.length;
// Corrections courantes
const corrections = [
{ pattern: /ion/gi, adjustment: 0 }, // "tion" = 1 syllabe, pas 2
{ pattern: /ieu/gi, adjustment: -1 }, // "ieux" = 1 syllabe
{ pattern: /eau/gi, adjustment: -1 }, // "eau" = 1 syllabe
{ pattern: /ai/gi, adjustment: -1 }, // "ai" = 1 syllabe
{ pattern: /ou/gi, adjustment: -1 }, // "ou" = 1 syllabe
{ pattern: /e$/gi, adjustment: -0.5 } // "e" final muet
];
corrections.forEach(correction => {
const matches = text.match(correction.pattern) || [];
syllables += matches.length * correction.adjustment;
});
return Math.max(1, Math.round(syllables));
}
/**
* PRÉSERVATION MOTS-CLÉS
* @param {string} originalText - Texte original
* @param {string} modifiedText - Texte modifié
* @returns {number} - Score préservation (0-1)
*/
function preserveKeywords(originalText, modifiedText) {
if (!originalText || !modifiedText) {
return 0;
}
const originalLower = originalText.toLowerCase();
const modifiedLower = modifiedText.toLowerCase();
// Extraire mots-clés du texte original
const originalKeywords = extractKeywords(originalLower);
// Vérifier préservation
let preservedCount = 0;
let criticalPreservedCount = 0;
let criticalTotalCount = 0;
originalKeywords.forEach(keyword => {
const isCritical = CRITICAL_KEYWORDS.some(ck =>
keyword.toLowerCase().includes(ck.toLowerCase()) ||
ck.toLowerCase().includes(keyword.toLowerCase())
);
if (isCritical) {
criticalTotalCount++;
}
// Vérifier présence dans texte modifié
const keywordRegex = new RegExp(`\\b${keyword}\\b`, 'gi');
if (modifiedLower.match(keywordRegex)) {
preservedCount++;
if (isCritical) {
criticalPreservedCount++;
}
}
});
// Score avec bonus pour mots-clés critiques
const basicPreservation = preservedCount / Math.max(1, originalKeywords.length);
const criticalPreservation = criticalTotalCount > 0 ?
criticalPreservedCount / criticalTotalCount : 1.0;
const finalScore = (basicPreservation * 0.6) + (criticalPreservation * 0.4);
logSh(` 🔑 Mots-clés: ${preservedCount}/${originalKeywords.length} préservés (${criticalPreservedCount}/${criticalTotalCount} critiques)`, 'DEBUG');
logSh(` 🎯 Score préservation: ${finalScore.toFixed(2)}`, 'DEBUG');
return finalScore;
}
/**
* EXTRACTION MOTS-CLÉS SIMPLES
*/
function extractKeywords(text) {
// Mots de plus de 3 caractères, non vides
const words = text.match(/\b\w{4,}\b/g) || [];
// Filtrer mots courants français
const stopWords = [
'avec', 'dans', 'pour', 'cette', 'sont', 'tout', 'mais', 'plus', 'très',
'bien', 'encore', 'aussi', 'comme', 'après', 'avant', 'entre', 'depuis'
];
const keywords = words
.filter(word => !stopWords.includes(word.toLowerCase()))
.filter((word, index, array) => array.indexOf(word) === index) // Unique
.slice(0, 20); // Limiter à 20 mots-clés
return keywords;
}
/**
* VALIDATION QUALITÉ SIMULATION
* @param {string} originalContent - Contenu original
* @param {string} simulatedContent - Contenu simulé
* @param {number} qualityThreshold - Seuil qualité minimum
* @returns {object} - Résultat validation
*/
function validateSimulationQuality(originalContent, simulatedContent, qualityThreshold = 0.7) {
if (!originalContent || !simulatedContent) {
return { acceptable: false, reason: 'Contenu manquant' };
}
logSh('🎯 Validation qualité simulation', 'DEBUG');
// Métriques de qualité
const readabilityScore = calculateReadabilityScore(simulatedContent);
const keywordScore = preserveKeywords(originalContent, simulatedContent);
const similarityScore = calculateSimilarity(originalContent, simulatedContent);
// Score global pondéré
const globalScore = (
readabilityScore * 0.4 +
keywordScore * 0.4 +
(similarityScore > QUALITY_THRESHOLDS.similarity.minimum &&
similarityScore < QUALITY_THRESHOLDS.similarity.maximum ? 0.2 : 0)
);
const acceptable = globalScore >= qualityThreshold;
const validation = {
acceptable,
globalScore,
readabilityScore,
keywordScore,
similarityScore,
reason: acceptable ? 'Qualité acceptable' : determineQualityIssue(readabilityScore, keywordScore, similarityScore),
details: {
readabilityOk: readabilityScore >= QUALITY_THRESHOLDS.readability.minimum,
keywordsOk: keywordScore >= QUALITY_THRESHOLDS.keywordPreservation.minimum,
similarityOk: similarityScore >= QUALITY_THRESHOLDS.similarity.minimum &&
similarityScore <= QUALITY_THRESHOLDS.similarity.maximum
}
};
logSh(` 🎯 Validation: ${acceptable ? 'ACCEPTÉ' : 'REJETÉ'} (score: ${globalScore.toFixed(2)})`, acceptable ? 'INFO' : 'WARNING');
logSh(` 📊 Lisibilité: ${readabilityScore.toFixed(2)} | Mots-clés: ${keywordScore.toFixed(2)} | Similarité: ${similarityScore.toFixed(2)}`, 'DEBUG');
return validation;
}
/**
* CALCUL SIMILARITÉ APPROXIMATIVE
*/
function calculateSimilarity(text1, text2) {
// Similarité basée sur les mots partagés (simple mais efficace)
const words1 = new Set(text1.toLowerCase().split(/\s+/));
const words2 = new Set(text2.toLowerCase().split(/\s+/));
const intersection = new Set([...words1].filter(word => words2.has(word)));
const union = new Set([...words1, ...words2]);
return intersection.size / union.size;
}
/**
* DÉTERMINER PROBLÈME QUALITÉ
*/
function determineQualityIssue(readabilityScore, keywordScore, similarityScore) {
if (readabilityScore < QUALITY_THRESHOLDS.readability.minimum) {
return 'Lisibilité insuffisante';
}
if (keywordScore < QUALITY_THRESHOLDS.keywordPreservation.minimum) {
return 'Mots-clés mal préservés';
}
if (similarityScore < QUALITY_THRESHOLDS.similarity.minimum) {
return 'Trop différent de l\'original';
}
if (similarityScore > QUALITY_THRESHOLDS.similarity.maximum) {
return 'Pas assez modifié';
}
return 'Score global insuffisant';
}
/**
* GÉNÉRATION RAPPORT QUALITÉ DÉTAILLÉ
* @param {object} content - Contenu à analyser
* @param {object} simulationStats - Stats simulation
* @returns {object} - Rapport détaillé
*/
function generateQualityReport(content, simulationStats) {
const report = {
timestamp: new Date().toISOString(),
contentAnalysis: analyzeContentComplexity(content),
simulationStats,
qualityMetrics: {},
recommendations: []
};
// Analyse par élément
Object.entries(content).forEach(([key, elementContent]) => {
if (typeof elementContent === 'string') {
const readability = calculateReadabilityScore(elementContent);
const complexity = analyzeContentComplexity({ [key]: elementContent });
report.qualityMetrics[key] = {
readability,
complexity: complexity.complexityScore,
wordCount: elementContent.split(/\s+/).length
};
}
});
// Recommandations automatiques
if (report.contentAnalysis.complexityScore > 0.8) {
report.recommendations.push('Simplifier le vocabulaire pour améliorer la lisibilité');
}
if (simulationStats.fatigueModifications < 1) {
report.recommendations.push('Augmenter l\'intensité de simulation fatigue');
}
return report;
}
/**
* HELPERS STATISTIQUES
*/
function calculateStatistics(values) {
const sorted = values.slice().sort((a, b) => a - b);
const length = values.length;
return {
mean: values.reduce((sum, val) => sum + val, 0) / length,
median: length % 2 === 0 ?
(sorted[length / 2 - 1] + sorted[length / 2]) / 2 :
sorted[Math.floor(length / 2)],
min: sorted[0],
max: sorted[length - 1],
stdDev: calculateStandardDeviation(values)
};
}
function calculateStandardDeviation(values) {
const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
const squaredDifferences = values.map(val => Math.pow(val - mean, 2));
const variance = squaredDifferences.reduce((sum, val) => sum + val, 0) / values.length;
return Math.sqrt(variance);
}
// ============= EXPORTS =============
module.exports = {
analyzeContentComplexity,
calculateReadabilityScore,
preserveKeywords,
validateSimulationQuality,
generateQualityReport,
calculateStatistics,
calculateStandardDeviation,
countSyllables,
extractKeywords,
calculateSimilarity,
determineQualityIssue,
QUALITY_THRESHOLDS,
CRITICAL_KEYWORDS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/human-simulation/HumanSimulationCore.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: HumanSimulationCore.js
// RESPONSABILITÉ: Orchestrateur principal Human Simulation
// Niveau 5: Temporal & Personality Injection
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { calculateFatigue, injectFatigueMarkers, getFatigueProfile } = require('./FatiguePatterns');
const { injectPersonalityErrors, getPersonalityErrorPatterns } = require('./PersonalityErrors');
const { applyTemporalStyle, getTemporalStyle } = require('./TemporalStyles');
const {
analyzeContentComplexity,
calculateReadabilityScore,
preserveKeywords,
validateSimulationQuality
} = require('./HumanSimulationUtils');
/**
* CONFIGURATION PAR DÉFAUT
*/
const DEFAULT_CONFIG = {
fatigueEnabled: true,
personalityErrorsEnabled: true,
temporalStyleEnabled: true,
imperfectionIntensity: 0.8, // FIXÉ: Plus d'intensité (était 0.3)
naturalRepetitions: true,
qualityThreshold: 0.4, // FIXÉ: Seuil plus bas (était 0.7)
maxModificationsPerElement: 5 // FIXÉ: Plus de modifs possibles (était 3)
};
/**
* ORCHESTRATEUR PRINCIPAL - Human Simulation Layer
* @param {object} content - Contenu généré à simuler
* @param {object} options - Options de simulation
* @returns {object} - { content, stats, fallback }
*/
async function applyHumanSimulationLayer(content, options = {}) {
return await tracer.run('HumanSimulationCore.applyHumanSimulationLayer()', async () => {
const startTime = Date.now();
await tracer.annotate({
contentKeys: Object.keys(content).length,
elementIndex: options.elementIndex,
totalElements: options.totalElements,
currentHour: options.currentHour,
personality: options.csvData?.personality?.nom
});
logSh(`🧠 HUMAN SIMULATION - Début traitement`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Position: ${options.elementIndex}/${options.totalElements}`, 'DEBUG');
try {
// Configuration fusionnée
const config = { ...DEFAULT_CONFIG, ...options };
// Stats de simulation
const simulationStats = {
elementsProcessed: 0,
fatigueModifications: 0,
personalityModifications: 0,
temporalModifications: 0,
totalModifications: 0,
qualityScore: 0,
fallbackUsed: false
};
// Contenu simulé
let simulatedContent = { ...content };
// ========================================
// 1. ANALYSE CONTEXTE GLOBAL
// ========================================
const globalContext = await analyzeGlobalContext(content, config);
logSh(` 🔍 Contexte: fatigue=${globalContext.fatigueLevel.toFixed(2)}, heure=${globalContext.currentHour}h, personnalité=${globalContext.personalityName}`, 'DEBUG');
// ========================================
// 2. TRAITEMENT PAR ÉLÉMENT
// ========================================
for (const [elementKey, elementContent] of Object.entries(content)) {
await tracer.run(`HumanSimulation.processElement(${elementKey})`, async () => {
logSh(` 🎯 Traitement élément: ${elementKey}`, 'DEBUG');
let processedContent = elementContent;
let elementModifications = 0;
try {
// 2a. Simulation Fatigue Cognitive
if (config.fatigueEnabled && globalContext.fatigueLevel > 0.1) { // FIXÉ: Seuil plus bas (était 0.3)
const fatigueResult = await applyFatigueSimulation(processedContent, globalContext, config);
processedContent = fatigueResult.content;
elementModifications += fatigueResult.modifications;
simulationStats.fatigueModifications += fatigueResult.modifications;
logSh(` 💤 Fatigue: ${fatigueResult.modifications} modifications (niveau: ${globalContext.fatigueLevel.toFixed(2)})`, 'DEBUG');
}
// 2b. Erreurs Personnalité
if (config.personalityErrorsEnabled && globalContext.personalityProfile) {
const personalityResult = await applyPersonalitySimulation(processedContent, globalContext, config);
processedContent = personalityResult.content;
elementModifications += personalityResult.modifications;
simulationStats.personalityModifications += personalityResult.modifications;
logSh(` 🎭 Personnalité: ${personalityResult.modifications} erreurs injectées`, 'DEBUG');
}
// 2c. Style Temporel
if (config.temporalStyleEnabled && globalContext.temporalStyle) {
const temporalResult = await applyTemporalSimulation(processedContent, globalContext, config);
processedContent = temporalResult.content;
elementModifications += temporalResult.modifications;
simulationStats.temporalModifications += temporalResult.modifications;
logSh(` ⏰ Temporel: ${temporalResult.modifications} ajustements (${globalContext.temporalStyle.period})`, 'DEBUG');
}
// 2d. Validation Qualité
const qualityCheck = validateSimulationQuality(elementContent, processedContent, config.qualityThreshold);
if (qualityCheck.acceptable) {
simulatedContent[elementKey] = processedContent;
simulationStats.elementsProcessed++;
simulationStats.totalModifications += elementModifications;
logSh(` ✅ Élément simulé: ${elementModifications} modifications totales`, 'DEBUG');
} else {
// Fallback: garder contenu original
simulatedContent[elementKey] = elementContent;
simulationStats.fallbackUsed = true;
logSh(` ⚠️ Qualité insuffisante, fallback vers contenu original`, 'WARNING');
}
} catch (elementError) {
logSh(` ❌ Erreur simulation élément ${elementKey}: ${elementError.message}`, 'WARNING');
simulatedContent[elementKey] = elementContent; // Fallback
simulationStats.fallbackUsed = true;
}
}, { elementKey, originalLength: elementContent?.length });
}
// ========================================
// 3. CALCUL SCORE QUALITÉ GLOBAL
// ========================================
simulationStats.qualityScore = calculateGlobalQualityScore(content, simulatedContent);
// ========================================
// 4. RÉSULTATS FINAUX
// ========================================
const duration = Date.now() - startTime;
const success = simulationStats.elementsProcessed > 0 && !simulationStats.fallbackUsed;
logSh(`🧠 HUMAN SIMULATION - Terminé (${duration}ms)`, 'INFO');
logSh(`${simulationStats.elementsProcessed}/${Object.keys(content).length} éléments simulés`, 'INFO');
logSh(` 📊 ${simulationStats.fatigueModifications} fatigue | ${simulationStats.personalityModifications} personnalité | ${simulationStats.temporalModifications} temporel`, 'INFO');
logSh(` 🎯 Score qualité: ${simulationStats.qualityScore.toFixed(2)} | Fallback: ${simulationStats.fallbackUsed ? 'OUI' : 'NON'}`, 'INFO');
await tracer.event('Human Simulation terminée', {
success,
duration,
stats: simulationStats
});
return {
content: simulatedContent,
stats: simulationStats,
fallback: simulationStats.fallbackUsed,
qualityScore: simulationStats.qualityScore,
duration
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ HUMAN SIMULATION ÉCHOUÉE (${duration}ms): ${error.message}`, 'ERROR');
await tracer.event('Human Simulation échouée', {
error: error.message,
duration,
contentKeys: Object.keys(content).length
});
// Fallback complet
return {
content,
stats: { fallbackUsed: true, error: error.message },
fallback: true,
qualityScore: 0,
duration
};
}
}, {
contentElements: Object.keys(content).length,
elementIndex: options.elementIndex,
personality: options.csvData?.personality?.nom
});
}
/**
* ANALYSE CONTEXTE GLOBAL
*/
async function analyzeGlobalContext(content, config) {
const elementIndex = config.elementIndex || 0;
const totalElements = config.totalElements || Object.keys(content).length;
const currentHour = config.currentHour || new Date().getHours();
const personality = config.csvData?.personality;
return {
fatigueLevel: calculateFatigue(elementIndex, totalElements),
fatigueProfile: personality ? getFatigueProfile(personality.nom) : null,
personalityName: personality?.nom || 'unknown',
personalityProfile: personality ? getPersonalityErrorPatterns(personality.nom) : null,
temporalStyle: getTemporalStyle(currentHour),
currentHour,
elementIndex,
totalElements,
contentComplexity: analyzeContentComplexity(content)
};
}
/**
* APPLICATION SIMULATION FATIGUE
*/
async function applyFatigueSimulation(content, globalContext, config) {
const fatigueResult = injectFatigueMarkers(content, globalContext.fatigueLevel, {
profile: globalContext.fatigueProfile,
intensity: config.imperfectionIntensity
});
return {
content: fatigueResult.content,
modifications: fatigueResult.modifications || 0
};
}
/**
* APPLICATION SIMULATION PERSONNALITÉ
*/
async function applyPersonalitySimulation(content, globalContext, config) {
const personalityResult = injectPersonalityErrors(
content,
globalContext.personalityProfile,
config.imperfectionIntensity
);
return {
content: personalityResult.content,
modifications: personalityResult.modifications || 0
};
}
/**
* APPLICATION SIMULATION TEMPORELLE
*/
async function applyTemporalSimulation(content, globalContext, config) {
const temporalResult = applyTemporalStyle(content, globalContext.temporalStyle, {
intensity: config.imperfectionIntensity
});
return {
content: temporalResult.content,
modifications: temporalResult.modifications || 0
};
}
/**
* CALCUL SCORE QUALITÉ GLOBAL
*/
function calculateGlobalQualityScore(originalContent, simulatedContent) {
let totalScore = 0;
let elementCount = 0;
for (const [key, original] of Object.entries(originalContent)) {
const simulated = simulatedContent[key];
if (simulated) {
const elementScore = calculateReadabilityScore(simulated) * 0.7 +
preserveKeywords(original, simulated) * 0.3;
totalScore += elementScore;
elementCount++;
}
}
return elementCount > 0 ? totalScore / elementCount : 0;
}
// ============= EXPORTS =============
module.exports = {
applyHumanSimulationLayer,
analyzeGlobalContext,
applyFatigueSimulation,
applyPersonalitySimulation,
applyTemporalSimulation,
calculateGlobalQualityScore,
DEFAULT_CONFIG
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/human-simulation/HumanSimulationLayers.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: HumanSimulationLayers.js
// RESPONSABILITÉ: Stacks prédéfinis Human Simulation
// Compatible avec architecture modulaire existante
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { applyHumanSimulationLayer } = require('./HumanSimulationCore');
/**
* STACKS PRÉDÉFINIS HUMAN SIMULATION
* Configuration par niveau d'intensité
*/
const HUMAN_SIMULATION_STACKS = {
// ========================================
// SIMULATION LÉGÈRE - Pour tests et développement
// ========================================
lightSimulation: {
name: 'lightSimulation',
description: 'Simulation humaine légère - développement et tests',
layersCount: 3,
config: {
fatigueEnabled: true,
personalityErrorsEnabled: true,
temporalStyleEnabled: false, // Désactivé en mode light
imperfectionIntensity: 0.3, // Faible intensité
naturalRepetitions: true,
qualityThreshold: 0.8, // Seuil élevé
maxModificationsPerElement: 2 // Limité à 2 modifs par élément
},
expectedImpact: {
modificationsPerElement: '1-2',
qualityPreservation: '95%',
detectionReduction: '10-15%',
executionTime: '+20%'
}
},
// ========================================
// SIMULATION STANDARD - Usage production normal
// ========================================
standardSimulation: {
name: 'standardSimulation',
description: 'Simulation humaine standard - équilibre performance/qualité',
layersCount: 3,
config: {
fatigueEnabled: true,
personalityErrorsEnabled: true,
temporalStyleEnabled: true, // Activé
imperfectionIntensity: 0.6, // Intensité moyenne
naturalRepetitions: true,
qualityThreshold: 0.7, // Seuil normal
maxModificationsPerElement: 3 // 3 modifs max
},
expectedImpact: {
modificationsPerElement: '2-3',
qualityPreservation: '85%',
detectionReduction: '25-35%',
executionTime: '+40%'
}
},
// ========================================
// SIMULATION INTENSIVE - Maximum anti-détection
// ========================================
heavySimulation: {
name: 'heavySimulation',
description: 'Simulation humaine intensive - anti-détection maximale',
layersCount: 3,
config: {
fatigueEnabled: true,
personalityErrorsEnabled: true,
temporalStyleEnabled: true,
imperfectionIntensity: 0.9, // Intensité élevée
naturalRepetitions: true,
qualityThreshold: 0.6, // Seuil plus permissif
maxModificationsPerElement: 5 // Jusqu'à 5 modifs
},
expectedImpact: {
modificationsPerElement: '3-5',
qualityPreservation: '75%',
detectionReduction: '40-50%',
executionTime: '+60%'
}
},
// ========================================
// SIMULATION ADAPTIVE - Intelligence contextuelle
// ========================================
adaptiveSimulation: {
name: 'adaptiveSimulation',
description: 'Simulation humaine adaptive - ajustement intelligent selon contexte',
layersCount: 3,
config: {
fatigueEnabled: true,
personalityErrorsEnabled: true,
temporalStyleEnabled: true,
imperfectionIntensity: 'adaptive', // Calculé dynamiquement
naturalRepetitions: true,
qualityThreshold: 'adaptive', // Ajusté selon complexité
maxModificationsPerElement: 'adaptive', // Variable
adaptiveLogic: true // Flag pour logique adaptive
},
expectedImpact: {
modificationsPerElement: '1-4',
qualityPreservation: '80-90%',
detectionReduction: '30-45%',
executionTime: '+45%'
}
},
// ========================================
// SIMULATION PERSONNALISÉE - Focus personnalité
// ========================================
personalityFocus: {
name: 'personalityFocus',
description: 'Focus erreurs personnalité - cohérence maximale',
layersCount: 2,
config: {
fatigueEnabled: false, // Désactivé
personalityErrorsEnabled: true,
temporalStyleEnabled: false, // Désactivé
imperfectionIntensity: 1.0, // Focus sur personnalité
naturalRepetitions: true,
qualityThreshold: 0.75,
maxModificationsPerElement: 3
},
expectedImpact: {
modificationsPerElement: '2-3',
qualityPreservation: '85%',
detectionReduction: '20-30%',
executionTime: '+25%'
}
},
// ========================================
// SIMULATION TEMPORELLE - Focus variations horaires
// ========================================
temporalFocus: {
name: 'temporalFocus',
description: 'Focus style temporel - variations selon heure',
layersCount: 2,
config: {
fatigueEnabled: false,
personalityErrorsEnabled: false,
temporalStyleEnabled: true, // Focus principal
imperfectionIntensity: 0.8,
naturalRepetitions: true,
qualityThreshold: 0.75,
maxModificationsPerElement: 3
},
expectedImpact: {
modificationsPerElement: '1-3',
qualityPreservation: '85%',
detectionReduction: '15-25%',
executionTime: '+20%'
}
}
};
/**
* APPLICATION STACK PRÉDÉFINI
* @param {object} content - Contenu à simuler
* @param {string} stackName - Nom du stack
* @param {object} options - Options additionnelles
* @returns {object} - Résultat simulation
*/
async function applyPredefinedSimulation(content, stackName, options = {}) {
return await tracer.run(`HumanSimulationLayers.applyPredefinedSimulation(${stackName})`, async () => {
const stack = HUMAN_SIMULATION_STACKS[stackName];
if (!stack) {
throw new Error(`Stack Human Simulation non trouvé: ${stackName}`);
}
await tracer.annotate({
stackName,
stackDescription: stack.description,
layersCount: stack.layersCount,
contentElements: Object.keys(content).length
});
logSh(`🧠 APPLICATION STACK: ${stack.name}`, 'INFO');
logSh(` 📝 ${stack.description}`, 'DEBUG');
logSh(` ⚙️ ${stack.layersCount} couches actives`, 'DEBUG');
try {
// Configuration fusionnée
let finalConfig = { ...stack.config, ...options };
// ========================================
// LOGIQUE ADAPTIVE (si applicable)
// ========================================
if (stack.config.adaptiveLogic) {
finalConfig = await applyAdaptiveLogic(content, finalConfig, options);
logSh(` 🧠 Logique adaptive appliquée`, 'DEBUG');
}
// ========================================
// APPLICATION SIMULATION PRINCIPALE
// ========================================
const simulationOptions = {
...finalConfig,
elementIndex: options.elementIndex || 0,
totalElements: options.totalElements || Object.keys(content).length,
currentHour: options.currentHour || new Date().getHours(),
csvData: options.csvData,
stackName: stack.name
};
const result = await applyHumanSimulationLayer(content, simulationOptions);
// ========================================
// ENRICHISSEMENT RÉSULTAT
// ========================================
const enrichedResult = {
...result,
stackInfo: {
name: stack.name,
description: stack.description,
layersCount: stack.layersCount,
expectedImpact: stack.expectedImpact,
configUsed: finalConfig
}
};
logSh(`✅ STACK ${stack.name} terminé: ${result.stats.totalModifications} modifications`, 'INFO');
await tracer.event('Stack Human Simulation terminé', {
stackName,
success: !result.fallback,
modifications: result.stats.totalModifications,
qualityScore: result.qualityScore
});
return enrichedResult;
} catch (error) {
logSh(`❌ ERREUR STACK ${stack.name}: ${error.message}`, 'ERROR');
await tracer.event('Stack Human Simulation échoué', {
stackName,
error: error.message
});
// Fallback gracieux
return {
content,
stats: { fallbackUsed: true, error: error.message },
fallback: true,
stackInfo: { name: stack.name, error: error.message }
};
}
}, { stackName, contentElements: Object.keys(content).length });
}
/**
* LOGIQUE ADAPTIVE INTELLIGENTE
* Ajuste la configuration selon le contexte
*/
async function applyAdaptiveLogic(content, config, options) {
logSh('🧠 Application logique adaptive', 'DEBUG');
const adaptedConfig = { ...config };
// ========================================
// 1. ANALYSE COMPLEXITÉ CONTENU
// ========================================
const totalText = Object.values(content).join(' ');
const wordCount = totalText.split(/\s+/).length;
const avgElementLength = wordCount / Object.keys(content).length;
// ========================================
// 2. AJUSTEMENT INTENSITÉ SELON COMPLEXITÉ
// ========================================
if (avgElementLength > 200) {
// Contenu long = intensité réduite pour préserver qualité
adaptedConfig.imperfectionIntensity = 0.5;
adaptedConfig.qualityThreshold = 0.8;
logSh(' 📏 Contenu long détecté: intensité réduite', 'DEBUG');
} else if (avgElementLength < 50) {
// Contenu court = intensité augmentée
adaptedConfig.imperfectionIntensity = 1.0;
adaptedConfig.qualityThreshold = 0.6;
logSh(' 📏 Contenu court détecté: intensité augmentée', 'DEBUG');
} else {
// Contenu moyen = intensité équilibrée
adaptedConfig.imperfectionIntensity = 0.7;
adaptedConfig.qualityThreshold = 0.7;
}
// ========================================
// 3. AJUSTEMENT SELON PERSONNALITÉ
// ========================================
const personality = options.csvData?.personality;
if (personality) {
const personalityName = personality.nom.toLowerCase();
// Personnalités techniques = moins d'erreurs de personnalité
if (['marc', 'amara', 'fabrice'].includes(personalityName)) {
adaptedConfig.imperfectionIntensity *= 0.8;
logSh(' 🎭 Personnalité technique: intensité erreurs réduite', 'DEBUG');
}
// Personnalités créatives = plus d'erreurs stylistiques
if (['sophie', 'émilie', 'chloé'].includes(personalityName)) {
adaptedConfig.imperfectionIntensity *= 1.2;
logSh(' 🎭 Personnalité créative: intensité erreurs augmentée', 'DEBUG');
}
}
// ========================================
// 4. AJUSTEMENT SELON HEURE
// ========================================
const currentHour = options.currentHour || new Date().getHours();
if (currentHour >= 22 || currentHour <= 6) {
// Nuit = plus de fatigue, moins de complexité
adaptedConfig.fatigueEnabled = true;
adaptedConfig.temporalStyleEnabled = true;
adaptedConfig.imperfectionIntensity *= 1.3;
logSh(' 🌙 Période nocturne: simulation fatigue renforcée', 'DEBUG');
} else if (currentHour >= 6 && currentHour <= 10) {
// Matin = énergie, moins d'erreurs
adaptedConfig.imperfectionIntensity *= 0.7;
logSh(' 🌅 Période matinale: intensité réduite', 'DEBUG');
}
// ========================================
// 5. LIMITATION SÉCURITÉ
// ========================================
adaptedConfig.imperfectionIntensity = Math.max(0.2, Math.min(1.5, adaptedConfig.imperfectionIntensity));
adaptedConfig.qualityThreshold = Math.max(0.5, Math.min(0.9, adaptedConfig.qualityThreshold));
// Modifs max adaptées à la taille du contenu
adaptedConfig.maxModificationsPerElement = Math.min(6, Math.max(1, Math.ceil(avgElementLength / 50)));
logSh(` 🎯 Config adaptée: intensité=${adaptedConfig.imperfectionIntensity.toFixed(2)}, seuil=${adaptedConfig.qualityThreshold.toFixed(2)}`, 'DEBUG');
return adaptedConfig;
}
/**
* OBTENIR STACKS DISPONIBLES
* @returns {array} - Liste des stacks avec métadonnées
*/
function getAvailableSimulationStacks() {
return Object.values(HUMAN_SIMULATION_STACKS).map(stack => ({
name: stack.name,
description: stack.description,
layersCount: stack.layersCount,
expectedImpact: stack.expectedImpact,
configPreview: {
fatigueEnabled: stack.config.fatigueEnabled,
personalityErrorsEnabled: stack.config.personalityErrorsEnabled,
temporalStyleEnabled: stack.config.temporalStyleEnabled,
intensity: stack.config.imperfectionIntensity
}
}));
}
/**
* VALIDATION STACK
* @param {string} stackName - Nom du stack à valider
* @returns {object} - Résultat validation
*/
function validateSimulationStack(stackName) {
const stack = HUMAN_SIMULATION_STACKS[stackName];
if (!stack) {
return {
valid: false,
error: `Stack '${stackName}' non trouvé`,
availableStacks: Object.keys(HUMAN_SIMULATION_STACKS)
};
}
// Validation configuration
const configIssues = [];
if (typeof stack.config.imperfectionIntensity === 'number' &&
(stack.config.imperfectionIntensity < 0 || stack.config.imperfectionIntensity > 2)) {
configIssues.push('intensité hors limites (0-2)');
}
if (typeof stack.config.qualityThreshold === 'number' &&
(stack.config.qualityThreshold < 0.3 || stack.config.qualityThreshold > 1)) {
configIssues.push('seuil qualité hors limites (0.3-1)');
}
return {
valid: configIssues.length === 0,
stack,
issues: configIssues,
recommendation: configIssues.length > 0 ?
'Corriger la configuration avant utilisation' :
'Stack prêt à utiliser'
};
}
/**
* RECOMMANDATION STACK AUTOMATIQUE
* @param {object} context - Contexte { contentLength, personality, hour, goal }
* @returns {string} - Nom du stack recommandé
*/
function recommendSimulationStack(context = {}) {
const { contentLength, personality, hour, goal } = context;
logSh('🤖 Recommandation stack automatique', 'DEBUG');
// Priorité 1: Objectif spécifique
if (goal === 'development') return 'lightSimulation';
if (goal === 'maximum_stealth') return 'heavySimulation';
if (goal === 'personality_focus') return 'personalityFocus';
if (goal === 'temporal_focus') return 'temporalFocus';
// Priorité 2: Complexité contenu
if (contentLength > 2000) return 'lightSimulation'; // Contenu long = prudent
if (contentLength < 300) return 'heavySimulation'; // Contenu court = intensif
// Priorité 3: Personnalité
if (personality) {
const personalityName = personality.toLowerCase();
if (['marc', 'amara', 'fabrice'].includes(personalityName)) {
return 'standardSimulation'; // Techniques = équilibré
}
if (['sophie', 'chloé', 'émilie'].includes(personalityName)) {
return 'personalityFocus'; // Créatives = focus personnalité
}
}
// Priorité 4: Heure
if (hour >= 22 || hour <= 6) return 'temporalFocus'; // Nuit = focus temporel
if (hour >= 6 && hour <= 10) return 'lightSimulation'; // Matin = léger
// Par défaut: adaptive pour intelligence contextuelle
logSh(' 🎯 Recommandation: adaptiveSimulation (par défaut)', 'DEBUG');
return 'adaptiveSimulation';
}
// ============= EXPORTS =============
module.exports = {
applyPredefinedSimulation,
getAvailableSimulationStacks,
validateSimulationStack,
recommendSimulationStack,
applyAdaptiveLogic,
HUMAN_SIMULATION_STACKS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/pattern-breaking/SyntaxVariations.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: SyntaxVariations.js
// RESPONSABILITÉ: Variations syntaxiques pour casser patterns LLM
// Techniques: découpage, fusion, restructuration phrases
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* PATTERNS SYNTAXIQUES TYPIQUES LLM À ÉVITER
*/
const LLM_SYNTAX_PATTERNS = {
// Structures trop prévisibles
repetitiveStarts: [
/^Il est important de/gi,
/^Il convient de/gi,
/^Il faut noter que/gi,
/^Dans ce contexte/gi,
/^Par ailleurs/gi
],
// Phrases trop parfaites
perfectStructures: [
/^De plus, .+ En outre, .+ Enfin,/gi,
/^Premièrement, .+ Deuxièmement, .+ Troisièmement,/gi
],
// Longueurs trop régulières (détection pattern)
uniformLengths: true // Détecté dynamiquement
};
/**
* VARIATION STRUCTURES SYNTAXIQUES - FONCTION PRINCIPALE
* @param {string} text - Texte à varier
* @param {number} intensity - Intensité variation (0-1)
* @param {object} options - Options { preserveReadability, maxModifications }
* @returns {object} - { content, modifications, stats }
*/
function varyStructures(text, intensity = 0.3, options = {}) {
if (!text || text.trim().length === 0) {
return { content: text, modifications: 0 };
}
const config = {
preserveReadability: true,
maxModifications: 3,
...options
};
logSh(`📝 Variation syntaxique: intensité ${intensity}, préservation: ${config.preserveReadability}`, 'DEBUG');
let modifiedText = text;
let totalModifications = 0;
const stats = {
sentencesSplit: 0,
sentencesMerged: 0,
structuresReorganized: 0,
repetitiveStartsFixed: 0
};
try {
// 1. Analyser structure phrases
const sentences = analyzeSentenceStructure(modifiedText);
logSh(` 📊 ${sentences.length} phrases analysées`, 'DEBUG');
// 2. Découper phrases longues
if (Math.random() < intensity) {
const splitResult = splitLongSentences(modifiedText, intensity);
modifiedText = splitResult.content;
totalModifications += splitResult.modifications;
stats.sentencesSplit = splitResult.modifications;
}
// 3. Fusionner phrases courtes
if (Math.random() < intensity * 0.7) {
const mergeResult = mergeShorter(modifiedText, intensity);
modifiedText = mergeResult.content;
totalModifications += mergeResult.modifications;
stats.sentencesMerged = mergeResult.modifications;
}
// 4. Réorganiser structures prévisibles
if (Math.random() < intensity * 0.8) {
const reorganizeResult = reorganizeStructures(modifiedText, intensity);
modifiedText = reorganizeResult.content;
totalModifications += reorganizeResult.modifications;
stats.structuresReorganized = reorganizeResult.modifications;
}
// 5. Corriger débuts répétitifs
if (Math.random() < intensity * 0.6) {
const repetitiveResult = fixRepetitiveStarts(modifiedText);
modifiedText = repetitiveResult.content;
totalModifications += repetitiveResult.modifications;
stats.repetitiveStartsFixed = repetitiveResult.modifications;
}
// 6. Limitation sécurité
if (totalModifications > config.maxModifications) {
logSh(` ⚠️ Limitation appliquée: ${totalModifications}${config.maxModifications} modifications`, 'DEBUG');
totalModifications = config.maxModifications;
}
logSh(`📝 Syntaxe modifiée: ${totalModifications} changements (${stats.sentencesSplit} splits, ${stats.sentencesMerged} merges)`, 'DEBUG');
} catch (error) {
logSh(`❌ Erreur variation syntaxique: ${error.message}`, 'WARNING');
return { content: text, modifications: 0, stats: {} };
}
return {
content: modifiedText,
modifications: totalModifications,
stats
};
}
/**
* ANALYSE STRUCTURE PHRASES
*/
function analyzeSentenceStructure(text) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
return sentences.map((sentence, index) => ({
index,
content: sentence.trim(),
length: sentence.trim().length,
wordCount: sentence.trim().split(/\s+/).length,
isLong: sentence.trim().length > 120,
isShort: sentence.trim().length < 40,
hasComplexStructure: sentence.includes(',') && sentence.includes(' qui ') || sentence.includes(' que ')
}));
}
/**
* DÉCOUPAGE PHRASES LONGUES
*/
function splitLongSentences(text, intensity) {
let modified = text;
let modifications = 0;
const sentences = modified.split('. ');
const processedSentences = sentences.map(sentence => {
// Phrases longues (>100 chars) et probabilité selon intensité - PLUS AGRESSIF
if (sentence.length > 100 && Math.random() < (intensity * 0.6)) {
// Points de découpe naturels
const cutPoints = [
{ pattern: /, qui (.+)/, replacement: '. Celui-ci $1' },
{ pattern: /, que (.+)/, replacement: '. Cela $1' },
{ pattern: /, dont (.+)/, replacement: '. Celui-ci $1' },
{ pattern: / et (.{30,})/, replacement: '. De plus, $1' },
{ pattern: /, car (.+)/, replacement: '. En effet, $1' },
{ pattern: /, mais (.+)/, replacement: '. Cependant, $1' }
];
for (const cutPoint of cutPoints) {
if (sentence.match(cutPoint.pattern)) {
const newSentence = sentence.replace(cutPoint.pattern, cutPoint.replacement);
if (newSentence !== sentence) {
modifications++;
logSh(` ✂️ Phrase découpée: ${sentence.length}${newSentence.length} chars`, 'DEBUG');
return newSentence;
}
}
}
}
return sentence;
});
return {
content: processedSentences.join('. '),
modifications
};
}
/**
* FUSION PHRASES COURTES
*/
function mergeShorter(text, intensity) {
let modified = text;
let modifications = 0;
const sentences = modified.split('. ');
const processedSentences = [];
for (let i = 0; i < sentences.length; i++) {
const current = sentences[i];
const next = sentences[i + 1];
// Si phrase courte (<50 chars) et phrase suivante existe - PLUS AGRESSIF
if (current && current.length < 50 && next && next.length < 70 && Math.random() < (intensity * 0.5)) {
// Connecteurs pour fusion naturelle
const connectors = [', de plus,', ', également,', ', aussi,', ' et'];
const connector = connectors[Math.floor(Math.random() * connectors.length)];
const merged = current + connector + ' ' + next.toLowerCase();
processedSentences.push(merged);
modifications++;
logSh(` 🔗 Phrases fusionnées: ${current.length} + ${next.length}${merged.length} chars`, 'DEBUG');
i++; // Passer la phrase suivante car fusionnée
} else {
processedSentences.push(current);
}
}
return {
content: processedSentences.join('. '),
modifications
};
}
/**
* RÉORGANISATION STRUCTURES PRÉVISIBLES
*/
function reorganizeStructures(text, intensity) {
let modified = text;
let modifications = 0;
// Détecter énumérations prévisibles
const enumerationPatterns = [
{
pattern: /Premièrement, (.+?)\. Deuxièmement, (.+?)\. Troisièmement, (.+?)\./gi,
replacement: 'D\'abord, $1. Ensuite, $2. Enfin, $3.'
},
{
pattern: /D\'une part, (.+?)\. D\'autre part, (.+?)\./gi,
replacement: 'Tout d\'abord, $1. Par ailleurs, $2.'
},
{
pattern: /En premier lieu, (.+?)\. En second lieu, (.+?)\./gi,
replacement: 'Dans un premier temps, $1. Puis, $2.'
}
];
enumerationPatterns.forEach(pattern => {
if (modified.match(pattern.pattern) && Math.random() < intensity) {
modified = modified.replace(pattern.pattern, pattern.replacement);
modifications++;
logSh(` 🔄 Structure réorganisée: énumération variée`, 'DEBUG');
}
});
return {
content: modified,
modifications
};
}
/**
* CORRECTION DÉBUTS RÉPÉTITIFS
*/
function fixRepetitiveStarts(text) {
let modified = text;
let modifications = 0;
const sentences = modified.split('. ');
const startWords = [];
// Analyser débuts de phrases
sentences.forEach(sentence => {
const words = sentence.trim().split(/\s+/);
if (words.length > 0) {
startWords.push(words[0].toLowerCase());
}
});
// Détecter répétitions
const startCounts = {};
startWords.forEach(word => {
startCounts[word] = (startCounts[word] || 0) + 1;
});
// Remplacer débuts répétitifs
const alternatives = {
'il': ['Cet élément', 'Cette solution', 'Ce produit'],
'cette': ['Cette option', 'Cette approche', 'Cette méthode'],
'pour': ['Afin de', 'Dans le but de', 'En vue de'],
'avec': ['Grâce à', 'Au moyen de', 'En utilisant'],
'dans': ['Au sein de', 'À travers', 'Parmi']
};
const processedSentences = sentences.map(sentence => {
const firstWord = sentence.trim().split(/\s+/)[0]?.toLowerCase();
if (firstWord && startCounts[firstWord] > 2 && alternatives[firstWord] && Math.random() < 0.4) {
const replacement = alternatives[firstWord][Math.floor(Math.random() * alternatives[firstWord].length)];
const newSentence = sentence.replace(/^\w+/, replacement);
modifications++;
logSh(` 🔄 Début varié: "${firstWord}" → "${replacement}"`, 'DEBUG');
return newSentence;
}
return sentence;
});
return {
content: processedSentences.join('. '),
modifications
};
}
/**
* DÉTECTION UNIFORMITÉ LONGUEURS (Pattern LLM)
*/
function detectUniformLengths(text) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
if (sentences.length < 3) return { uniform: false, variance: 0 };
const lengths = sentences.map(s => s.trim().length);
const avgLength = lengths.reduce((sum, len) => sum + len, 0) / lengths.length;
// Calculer variance
const variance = lengths.reduce((sum, len) => sum + Math.pow(len - avgLength, 2), 0) / lengths.length;
const standardDev = Math.sqrt(variance);
// Uniformité si écart-type faible par rapport à moyenne
const coefficientVariation = standardDev / avgLength;
const uniform = coefficientVariation < 0.3; // Seuil arbitraire
return {
uniform,
variance: coefficientVariation,
avgLength,
standardDev,
sentenceCount: sentences.length
};
}
/**
* AJOUT VARIATIONS MICRO-SYNTAXIQUES
*/
function addMicroVariations(text, intensity) {
let modified = text;
let modifications = 0;
// Micro-variations subtiles
const microPatterns = [
{ from: /\btrès (.+?)\b/g, to: 'particulièrement $1', probability: 0.3 },
{ from: /\bassez (.+?)\b/g, to: 'plutôt $1', probability: 0.4 },
{ from: /\bbeaucoup de/g, to: 'de nombreux', probability: 0.3 },
{ from: /\bpermets de/g, to: 'permet de', probability: 0.8 }, // Correction fréquente
{ from: /\bien effet\b/g, to: 'effectivement', probability: 0.2 }
];
microPatterns.forEach(pattern => {
if (Math.random() < (intensity * pattern.probability)) {
const before = modified;
modified = modified.replace(pattern.from, pattern.to);
if (modified !== before) {
modifications++;
logSh(` 🔧 Micro-variation: ${pattern.from}${pattern.to}`, 'DEBUG');
}
}
});
return {
content: modified,
modifications
};
}
// ============= EXPORTS =============
module.exports = {
varyStructures,
splitLongSentences,
mergeShorter,
reorganizeStructures,
fixRepetitiveStarts,
analyzeSentenceStructure,
detectUniformLengths,
addMicroVariations,
LLM_SYNTAX_PATTERNS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/pattern-breaking/LLMFingerprints.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: LLMFingerprints.js
// RESPONSABILITÉ: Remplacement mots et expressions typiques LLM
// Identification et remplacement des "fingerprints" IA
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* MOTS ET EXPRESSIONS TYPIQUES LLM À REMPLACER
* Classés par niveau de suspicion et fréquence d'usage LLM
*/
const LLM_FINGERPRINTS = {
// ========================================
// NIVEAU CRITIQUE - Très suspects
// ========================================
critical: {
adjectives: [
{ word: 'comprehensive', alternatives: ['complet', 'détaillé', 'approfondi', 'exhaustif'], suspicion: 0.95 },
{ word: 'robust', alternatives: ['solide', 'fiable', 'résistant', 'durable'], suspicion: 0.92 },
{ word: 'seamless', alternatives: ['fluide', 'harmonieux', 'sans accroc', 'naturel'], suspicion: 0.90 },
{ word: 'optimal', alternatives: ['idéal', 'parfait', 'excellent', 'adapté'], suspicion: 0.88 },
{ word: 'cutting-edge', alternatives: ['innovant', 'moderne', 'récent', 'avancé'], suspicion: 0.87 },
{ word: 'state-of-the-art', alternatives: ['dernier cri', 'moderne', 'récent'], suspicion: 0.95 }
],
expressions: [
{ phrase: 'il est important de noter que', alternatives: ['remarquons que', 'signalons que', 'précisons que'], suspicion: 0.85 },
{ phrase: 'dans le paysage actuel', alternatives: ['actuellement', 'de nos jours', 'aujourd\'hui'], suspicion: 0.82 },
{ phrase: 'il convient de souligner', alternatives: ['il faut noter', 'soulignons', 'remarquons'], suspicion: 0.80 },
{ phrase: 'en fin de compte', alternatives: ['finalement', 'au final', 'pour conclure'], suspicion: 0.75 }
]
},
// ========================================
// NIVEAU ÉLEVÉ - Souvent suspects
// ========================================
high: {
adjectives: [
{ word: 'innovative', alternatives: ['novateur', 'créatif', 'original', 'moderne'], suspicion: 0.75 },
{ word: 'efficient', alternatives: ['efficace', 'performant', 'rapide', 'pratique'], suspicion: 0.70 },
{ word: 'versatile', alternatives: ['polyvalent', 'adaptable', 'flexible', 'modulable'], suspicion: 0.68 },
{ word: 'sophisticated', alternatives: ['raffiné', 'élaboré', 'avancé', 'complexe'], suspicion: 0.65 },
{ word: 'compelling', alternatives: ['convaincant', 'captivant', 'intéressant'], suspicion: 0.72 }
],
verbs: [
{ word: 'leverage', alternatives: ['utiliser', 'exploiter', 'tirer parti de', 'employer'], suspicion: 0.80 },
{ word: 'optimize', alternatives: ['améliorer', 'perfectionner', 'ajuster'], suspicion: 0.65 },
{ word: 'streamline', alternatives: ['simplifier', 'rationaliser', 'organiser'], suspicion: 0.75 },
{ word: 'enhance', alternatives: ['améliorer', 'enrichir', 'renforcer'], suspicion: 0.60 }
],
expressions: [
{ phrase: 'par ailleurs', alternatives: ['de plus', 'également', 'aussi', 'en outre'], suspicion: 0.65 },
{ phrase: 'en outre', alternatives: ['de plus', 'également', 'aussi'], suspicion: 0.70 },
{ phrase: 'cela dit', alternatives: ['néanmoins', 'toutefois', 'cependant'], suspicion: 0.60 }
]
},
// ========================================
// NIVEAU MODÉRÉ - Parfois suspects
// ========================================
moderate: {
adjectives: [
{ word: 'significant', alternatives: ['important', 'notable', 'considérable', 'marquant'], suspicion: 0.55 },
{ word: 'essential', alternatives: ['indispensable', 'crucial', 'vital', 'nécessaire'], suspicion: 0.50 },
{ word: 'comprehensive', alternatives: ['complet', 'global', 'détaillé'], suspicion: 0.58 },
{ word: 'effective', alternatives: ['efficace', 'performant', 'réussi'], suspicion: 0.45 }
],
expressions: [
{ phrase: 'il est essentiel de', alternatives: ['il faut', 'il importe de', 'il est crucial de'], suspicion: 0.55 },
{ phrase: 'dans cette optique', alternatives: ['dans cette perspective', 'ainsi', 'de ce fait'], suspicion: 0.52 },
{ phrase: 'à cet égard', alternatives: ['sur ce point', 'concernant cela', 'à ce propos'], suspicion: 0.48 }
]
}
};
/**
* PATTERNS STRUCTURELS LLM
*/
const STRUCTURAL_PATTERNS = {
// Débuts de phrases trop formels
formalStarts: [
/^Il est important de souligner que/gi,
/^Il convient de noter que/gi,
/^Il est essentiel de comprendre que/gi,
/^Dans ce contexte, il est crucial de/gi,
/^Il est primordial de/gi
],
// Transitions trop parfaites
perfectTransitions: [
/\. Par ailleurs, (.+?)\. En outre, (.+?)\. De plus,/gi,
/\. Premièrement, (.+?)\. Deuxièmement, (.+?)\. Troisièmement,/gi
],
// Conclusions trop formelles
formalConclusions: [
/En conclusion, il apparaît clairement que/gi,
/Pour conclure, il est évident que/gi,
/En définitive, nous pouvons affirmer que/gi
]
};
/**
* DÉTECTION PATTERNS LLM DANS LE TEXTE
* @param {string} text - Texte à analyser
* @returns {object} - { count, patterns, suspicionScore }
*/
function detectLLMPatterns(text) {
if (!text || text.trim().length === 0) {
return { count: 0, patterns: [], suspicionScore: 0 };
}
const detectedPatterns = [];
let totalSuspicion = 0;
let wordCount = text.split(/\s+/).length;
// Analyser tous les niveaux de fingerprints
Object.entries(LLM_FINGERPRINTS).forEach(([level, categories]) => {
Object.entries(categories).forEach(([category, items]) => {
items.forEach(item => {
const regex = new RegExp(`\\b${item.word || item.phrase}\\b`, 'gi');
const matches = text.match(regex);
if (matches) {
detectedPatterns.push({
pattern: item.word || item.phrase,
type: category,
level: level,
count: matches.length,
suspicion: item.suspicion,
alternatives: item.alternatives
});
totalSuspicion += item.suspicion * matches.length;
}
});
});
});
// Analyser patterns structurels
Object.entries(STRUCTURAL_PATTERNS).forEach(([patternType, patterns]) => {
patterns.forEach(pattern => {
const matches = text.match(pattern);
if (matches) {
detectedPatterns.push({
pattern: pattern.source,
type: 'structural',
level: 'high',
count: matches.length,
suspicion: 0.80
});
totalSuspicion += 0.80 * matches.length;
}
});
});
const suspicionScore = wordCount > 0 ? totalSuspicion / wordCount : 0;
logSh(`🔍 Patterns LLM détectés: ${detectedPatterns.length} (score suspicion: ${suspicionScore.toFixed(3)})`, 'DEBUG');
return {
count: detectedPatterns.length,
patterns: detectedPatterns.map(p => p.pattern),
detailedPatterns: detectedPatterns,
suspicionScore,
recommendation: suspicionScore > 0.05 ? 'replacement' : 'minor_cleanup'
};
}
/**
* REMPLACEMENT FINGERPRINTS LLM
* @param {string} text - Texte à traiter
* @param {object} options - Options { intensity, preserveContext, maxReplacements }
* @returns {object} - { content, replacements, details }
*/
function replaceLLMFingerprints(text, options = {}) {
if (!text || text.trim().length === 0) {
return { content: text, replacements: 0 };
}
const config = {
intensity: 0.5,
preserveContext: true,
maxReplacements: 5,
...options
};
logSh(`🤖 Remplacement fingerprints LLM: intensité ${config.intensity}`, 'DEBUG');
let modifiedText = text;
let totalReplacements = 0;
const replacementDetails = [];
try {
// Détecter d'abord les patterns
const detection = detectLLMPatterns(modifiedText);
if (detection.count === 0) {
logSh(` ✅ Aucun fingerprint LLM détecté`, 'DEBUG');
return { content: text, replacements: 0, details: [] };
}
// Traiter par niveau de priorité
const priorities = ['critical', 'high', 'moderate'];
for (const priority of priorities) {
if (totalReplacements >= config.maxReplacements) break;
const categoryData = LLM_FINGERPRINTS[priority];
if (!categoryData) continue;
// Traiter chaque catégorie
Object.entries(categoryData).forEach(([category, items]) => {
items.forEach(item => {
if (totalReplacements >= config.maxReplacements) return;
const searchTerm = item.word || item.phrase;
const regex = new RegExp(`\\b${searchTerm}\\b`, 'gi');
// Probabilité de remplacement basée sur suspicion et intensité
const replacementProbability = item.suspicion * config.intensity;
if (modifiedText.match(regex) && Math.random() < replacementProbability) {
// Choisir alternative aléatoire
const alternative = item.alternatives[Math.floor(Math.random() * item.alternatives.length)];
const beforeText = modifiedText;
modifiedText = modifiedText.replace(regex, alternative);
if (modifiedText !== beforeText) {
totalReplacements++;
replacementDetails.push({
original: searchTerm,
replacement: alternative,
category,
level: priority,
suspicion: item.suspicion
});
logSh(` 🔄 Remplacé "${searchTerm}" → "${alternative}" (suspicion: ${item.suspicion})`, 'DEBUG');
}
}
});
});
}
// Traitement patterns structurels
if (totalReplacements < config.maxReplacements) {
const structuralResult = replaceStructuralPatterns(modifiedText, config.intensity);
modifiedText = structuralResult.content;
totalReplacements += structuralResult.replacements;
replacementDetails.push(...structuralResult.details);
}
logSh(`🤖 Fingerprints remplacés: ${totalReplacements} modifications`, 'DEBUG');
} catch (error) {
logSh(`❌ Erreur remplacement fingerprints: ${error.message}`, 'WARNING');
return { content: text, replacements: 0, details: [] };
}
return {
content: modifiedText,
replacements: totalReplacements,
details: replacementDetails
};
}
/**
* REMPLACEMENT PATTERNS STRUCTURELS
*/
function replaceStructuralPatterns(text, intensity) {
let modified = text;
let replacements = 0;
const details = [];
// Débuts formels → versions plus naturelles
const formalStartReplacements = [
{
from: /^Il est important de souligner que (.+)/gim,
to: 'Notons que $1',
name: 'début formel'
},
{
from: /^Il convient de noter que (.+)/gim,
to: 'Précisons que $1',
name: 'formulation convient'
},
{
from: /^Dans ce contexte, il est crucial de (.+)/gim,
to: 'Il faut $1',
name: 'contexte crucial'
}
];
formalStartReplacements.forEach(replacement => {
if (Math.random() < intensity * 0.7) {
const before = modified;
modified = modified.replace(replacement.from, replacement.to);
if (modified !== before) {
replacements++;
details.push({
original: replacement.name,
replacement: 'version naturelle',
category: 'structural',
level: 'high',
suspicion: 0.80
});
logSh(` 🏗️ Pattern structurel remplacé: ${replacement.name}`, 'DEBUG');
}
}
});
return {
content: modified,
replacements,
details
};
}
/**
* ANALYSE DENSITÉ FINGERPRINTS
*/
function analyzeFingerprintDensity(text) {
const detection = detectLLMPatterns(text);
const wordCount = text.split(/\s+/).length;
const density = detection.count / wordCount;
const riskLevel = density > 0.08 ? 'high' : density > 0.04 ? 'medium' : 'low';
return {
fingerprintCount: detection.count,
wordCount,
density,
riskLevel,
suspicionScore: detection.suspicionScore,
recommendation: riskLevel === 'high' ? 'immediate_replacement' :
riskLevel === 'medium' ? 'selective_replacement' : 'minimal_cleanup'
};
}
/**
* SUGGESTIONS CONTEXTUELLES
*/
function generateContextualAlternatives(word, context, personality) {
// Adapter selon personnalité si fournie
if (personality) {
const personalityAdaptations = {
'marc': { 'optimal': 'efficace', 'robust': 'solide', 'comprehensive': 'complet' },
'sophie': { 'optimal': 'parfait', 'robust': 'résistant', 'comprehensive': 'détaillé' },
'kevin': { 'optimal': 'nickel', 'robust': 'costaud', 'comprehensive': 'complet' }
};
const adaptations = personalityAdaptations[personality.toLowerCase()];
if (adaptations && adaptations[word]) {
return [adaptations[word]];
}
}
// Suggestions contextuelles basiques
const contextualMappings = {
'optimal': context.includes('solution') ? ['idéale', 'parfaite'] : ['excellent', 'adapté'],
'robust': context.includes('système') ? ['fiable', 'stable'] : ['solide', 'résistant'],
'comprehensive': context.includes('analyse') ? ['approfondie', 'détaillée'] : ['complète', 'globale']
};
return contextualMappings[word] || ['standard'];
}
// ============= EXPORTS =============
module.exports = {
detectLLMPatterns,
replaceLLMFingerprints,
replaceStructuralPatterns,
analyzeFingerprintDensity,
generateContextualAlternatives,
LLM_FINGERPRINTS,
STRUCTURAL_PATTERNS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/pattern-breaking/NaturalConnectors.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: NaturalConnectors.js
// RESPONSABILITÉ: Humanisation des connecteurs et transitions
// Remplacement connecteurs formels par versions naturelles
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* CONNECTEURS FORMELS LLM À HUMANISER
*/
const FORMAL_CONNECTORS = {
// Connecteurs trop formels/académiques
formal: [
{ connector: 'par ailleurs', alternatives: ['aussi', 'également', 'de plus', 'en plus'], suspicion: 0.75 },
{ connector: 'en outre', alternatives: ['de plus', 'également', 'aussi', 'en plus'], suspicion: 0.80 },
{ connector: 'de surcroît', alternatives: ['de plus', 'aussi', 'en plus'], suspicion: 0.85 },
{ connector: 'qui plus est', alternatives: ['en plus', 'et puis', 'aussi'], suspicion: 0.80 },
{ connector: 'par conséquent', alternatives: ['donc', 'alors', 'du coup', 'résultat'], suspicion: 0.70 },
{ connector: 'en conséquence', alternatives: ['donc', 'alors', 'du coup'], suspicion: 0.75 },
{ connector: 'néanmoins', alternatives: ['mais', 'pourtant', 'cependant', 'malgré ça'], suspicion: 0.65 },
{ connector: 'toutefois', alternatives: ['mais', 'pourtant', 'cependant'], suspicion: 0.70 }
],
// Débuts de phrases formels
formalStarts: [
{ phrase: 'il convient de noter que', alternatives: ['notons que', 'remarquons que', 'précisons que'], suspicion: 0.90 },
{ phrase: 'il est important de souligner que', alternatives: ['soulignons que', 'notons que', 'précisons que'], suspicion: 0.85 },
{ phrase: 'il est à noter que', alternatives: ['notons que', 'signalons que', 'précisons que'], suspicion: 0.80 },
{ phrase: 'il convient de préciser que', alternatives: ['précisons que', 'ajoutons que', 'notons que'], suspicion: 0.75 },
{ phrase: 'dans ce contexte', alternatives: ['ici', 'dans ce cas', 'alors'], suspicion: 0.70 }
],
// Transitions artificielles
artificialTransitions: [
{ phrase: 'abordons maintenant', alternatives: ['passons à', 'voyons', 'parlons de'], suspicion: 0.75 },
{ phrase: 'examinons à présent', alternatives: ['voyons', 'regardons', 'passons à'], suspicion: 0.80 },
{ phrase: 'intéressons-nous désormais à', alternatives: ['voyons', 'parlons de', 'passons à'], suspicion: 0.85 },
{ phrase: 'penchons-nous sur', alternatives: ['voyons', 'regardons', 'parlons de'], suspicion: 0.70 }
]
};
/**
* CONNECTEURS NATURELS PAR CONTEXTE
*/
const NATURAL_CONNECTORS_BY_CONTEXT = {
// Selon le ton/registre souhaité
casual: ['du coup', 'alors', 'et puis', 'aussi', 'en fait'],
conversational: ['bon', 'eh bien', 'donc', 'alors', 'et puis'],
technical: ['donc', 'ainsi', 'alors', 'par là', 'de cette façon'],
commercial: ['donc', 'alors', 'ainsi', 'de plus', 'aussi']
};
/**
* HUMANISATION CONNECTEURS ET TRANSITIONS - FONCTION PRINCIPALE
* @param {string} text - Texte à humaniser
* @param {object} options - Options { intensity, preserveMeaning, maxReplacements }
* @returns {object} - { content, replacements, details }
*/
function humanizeTransitions(text, options = {}) {
if (!text || text.trim().length === 0) {
return { content: text, replacements: 0 };
}
const config = {
intensity: 0.6,
preserveMeaning: true,
maxReplacements: 4,
tone: 'casual', // casual, conversational, technical, commercial
...options
};
logSh(`🔗 Humanisation connecteurs: intensité ${config.intensity}, ton ${config.tone}`, 'DEBUG');
let modifiedText = text;
let totalReplacements = 0;
const replacementDetails = [];
try {
// 1. Remplacer connecteurs formels
const connectorsResult = replaceFormalConnectors(modifiedText, config);
modifiedText = connectorsResult.content;
totalReplacements += connectorsResult.replacements;
replacementDetails.push(...connectorsResult.details);
// 2. Humaniser débuts de phrases
if (totalReplacements < config.maxReplacements) {
const startsResult = humanizeFormalStarts(modifiedText, config);
modifiedText = startsResult.content;
totalReplacements += startsResult.replacements;
replacementDetails.push(...startsResult.details);
}
// 3. Remplacer transitions artificielles
if (totalReplacements < config.maxReplacements) {
const transitionsResult = replaceArtificialTransitions(modifiedText, config);
modifiedText = transitionsResult.content;
totalReplacements += transitionsResult.replacements;
replacementDetails.push(...transitionsResult.details);
}
// 4. Ajouter variabilité contextuelle
if (totalReplacements < config.maxReplacements) {
const contextResult = addContextualVariability(modifiedText, config);
modifiedText = contextResult.content;
totalReplacements += contextResult.replacements;
replacementDetails.push(...contextResult.details);
}
logSh(`🔗 Connecteurs humanisés: ${totalReplacements} remplacements effectués`, 'DEBUG');
} catch (error) {
logSh(`❌ Erreur humanisation connecteurs: ${error.message}`, 'WARNING');
return { content: text, replacements: 0, details: [] };
}
return {
content: modifiedText,
replacements: totalReplacements,
details: replacementDetails
};
}
/**
* REMPLACEMENT CONNECTEURS FORMELS
*/
function replaceFormalConnectors(text, config) {
let modified = text;
let replacements = 0;
const details = [];
FORMAL_CONNECTORS.formal.forEach(connector => {
if (replacements >= Math.floor(config.maxReplacements / 2)) return;
const regex = new RegExp(`\\b${connector.connector}\\b`, 'gi');
const matches = modified.match(regex);
if (matches && Math.random() < (config.intensity * connector.suspicion)) {
// Choisir alternative selon contexte/ton
const availableAlts = connector.alternatives;
const contextualAlts = NATURAL_CONNECTORS_BY_CONTEXT[config.tone] || [];
// Préférer alternatives contextuelles si disponibles
const preferredAlts = availableAlts.filter(alt => contextualAlts.includes(alt));
const finalAlts = preferredAlts.length > 0 ? preferredAlts : availableAlts;
const chosen = finalAlts[Math.floor(Math.random() * finalAlts.length)];
const beforeText = modified;
modified = modified.replace(regex, chosen);
if (modified !== beforeText) {
replacements++;
details.push({
original: connector.connector,
replacement: chosen,
type: 'formal_connector',
suspicion: connector.suspicion
});
logSh(` 🔄 Connecteur formalisé: "${connector.connector}" → "${chosen}"`, 'DEBUG');
}
}
});
return { content: modified, replacements, details };
}
/**
* HUMANISATION DÉBUTS DE PHRASES FORMELS
*/
function humanizeFormalStarts(text, config) {
let modified = text;
let replacements = 0;
const details = [];
FORMAL_CONNECTORS.formalStarts.forEach(start => {
if (replacements >= Math.floor(config.maxReplacements / 3)) return;
const regex = new RegExp(start.phrase, 'gi');
if (modified.match(regex) && Math.random() < (config.intensity * start.suspicion)) {
const alternative = start.alternatives[Math.floor(Math.random() * start.alternatives.length)];
const beforeText = modified;
modified = modified.replace(regex, alternative);
if (modified !== beforeText) {
replacements++;
details.push({
original: start.phrase,
replacement: alternative,
type: 'formal_start',
suspicion: start.suspicion
});
logSh(` 🚀 Début formalisé: "${start.phrase}" → "${alternative}"`, 'DEBUG');
}
}
});
return { content: modified, replacements, details };
}
/**
* REMPLACEMENT TRANSITIONS ARTIFICIELLES
*/
function replaceArtificialTransitions(text, config) {
let modified = text;
let replacements = 0;
const details = [];
FORMAL_CONNECTORS.artificialTransitions.forEach(transition => {
if (replacements >= Math.floor(config.maxReplacements / 4)) return;
const regex = new RegExp(transition.phrase, 'gi');
if (modified.match(regex) && Math.random() < (config.intensity * transition.suspicion * 0.8)) {
const alternative = transition.alternatives[Math.floor(Math.random() * transition.alternatives.length)];
const beforeText = modified;
modified = modified.replace(regex, alternative);
if (modified !== beforeText) {
replacements++;
details.push({
original: transition.phrase,
replacement: alternative,
type: 'artificial_transition',
suspicion: transition.suspicion
});
logSh(` 🌉 Transition artificialisée: "${transition.phrase}" → "${alternative}"`, 'DEBUG');
}
}
});
return { content: modified, replacements, details };
}
/**
* AJOUT VARIABILITÉ CONTEXTUELLE
*/
function addContextualVariability(text, config) {
let modified = text;
let replacements = 0;
const details = [];
// Connecteurs génériques à contextualiser selon le ton
const genericPatterns = [
{ from: /\bet puis\b/g, contextual: true },
{ from: /\bdone\b/g, contextual: true },
{ from: /\bainsi\b/g, contextual: true }
];
const contextualReplacements = NATURAL_CONNECTORS_BY_CONTEXT[config.tone] || NATURAL_CONNECTORS_BY_CONTEXT.casual;
genericPatterns.forEach(pattern => {
if (replacements >= 2) return;
if (pattern.contextual && Math.random() < (config.intensity * 0.4)) {
const matches = modified.match(pattern.from);
if (matches && contextualReplacements.length > 0) {
const replacement = contextualReplacements[Math.floor(Math.random() * contextualReplacements.length)];
// Éviter remplacements identiques
if (replacement !== matches[0]) {
const beforeText = modified;
modified = modified.replace(pattern.from, replacement);
if (modified !== beforeText) {
replacements++;
details.push({
original: matches[0],
replacement,
type: 'contextual_variation',
suspicion: 0.4
});
logSh(` 🎯 Variation contextuelle: "${matches[0]}" → "${replacement}"`, 'DEBUG');
}
}
}
}
});
return { content: modified, replacements, details };
}
/**
* DÉTECTION CONNECTEURS FORMELS DANS TEXTE
*/
function detectFormalConnectors(text) {
if (!text || text.trim().length === 0) {
return { count: 0, connectors: [], suspicionScore: 0 };
}
const detectedConnectors = [];
let totalSuspicion = 0;
// Vérifier tous les types de connecteurs formels
Object.values(FORMAL_CONNECTORS).flat().forEach(item => {
const searchTerm = item.connector || item.phrase;
const regex = new RegExp(`\\b${searchTerm}\\b`, 'gi');
const matches = text.match(regex);
if (matches) {
detectedConnectors.push({
connector: searchTerm,
count: matches.length,
suspicion: item.suspicion,
alternatives: item.alternatives
});
totalSuspicion += item.suspicion * matches.length;
}
});
const wordCount = text.split(/\s+/).length;
const suspicionScore = wordCount > 0 ? totalSuspicion / wordCount : 0;
logSh(`🔍 Connecteurs formels détectés: ${detectedConnectors.length} (score: ${suspicionScore.toFixed(3)})`, 'DEBUG');
return {
count: detectedConnectors.length,
connectors: detectedConnectors.map(c => c.connector),
detailedConnectors: detectedConnectors,
suspicionScore,
recommendation: suspicionScore > 0.03 ? 'humanize' : 'minimal_changes'
};
}
/**
* ANALYSE DENSITÉ CONNECTEURS FORMELS
*/
function analyzeConnectorFormality(text) {
const detection = detectFormalConnectors(text);
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
const density = detection.count / sentences.length;
const formalityLevel = density > 0.4 ? 'high' : density > 0.2 ? 'medium' : 'low';
return {
connectorsCount: detection.count,
sentenceCount: sentences.length,
density,
formalityLevel,
suspicionScore: detection.suspicionScore,
recommendation: formalityLevel === 'high' ? 'extensive_humanization' :
formalityLevel === 'medium' ? 'selective_humanization' : 'minimal_humanization'
};
}
// ============= EXPORTS =============
module.exports = {
humanizeTransitions,
replaceFormalConnectors,
humanizeFormalStarts,
replaceArtificialTransitions,
addContextualVariability,
detectFormalConnectors,
analyzeConnectorFormality,
FORMAL_CONNECTORS,
NATURAL_CONNECTORS_BY_CONTEXT
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/pattern-breaking/PatternBreakingCore.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: PatternBreakingCore.js
// RESPONSABILITÉ: Orchestrateur principal Pattern Breaking
// Niveau 2: Casser les patterns syntaxiques typiques des LLMs
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { varyStructures, splitLongSentences, mergeShorter } = require('./SyntaxVariations');
const { replaceLLMFingerprints, detectLLMPatterns } = require('./LLMFingerprints');
const { humanizeTransitions, replaceConnectors } = require('./NaturalConnectors');
/**
* CONFIGURATION MODULAIRE AGRESSIVE PATTERN BREAKING
* Chaque feature peut être activée/désactivée individuellement
*/
const DEFAULT_CONFIG = {
// ========================================
// CONTRÔLES GLOBAUX
// ========================================
intensityLevel: 0.8, // Intensité globale (0-1) - PLUS AGRESSIVE
preserveReadability: true, // Maintenir lisibilité
maxModificationsPerElement: 8, // Limite modifications par élément - DOUBLÉE
qualityThreshold: 0.5, // Seuil qualité minimum - ABAISSÉ
// ========================================
// FEATURES SYNTAXE & STRUCTURE
// ========================================
syntaxVariationEnabled: true, // Variations syntaxiques de base
aggressiveSentenceSplitting: true, // Découpage phrases plus agressif (<80 chars)
aggressiveSentenceMerging: true, // Fusion phrases courtes (<60 chars)
microSyntaxVariations: true, // Micro-variations subtiles
questionInjection: true, // Injection questions rhétoriques
// ========================================
// FEATURES LLM FINGERPRINTS
// ========================================
llmFingerprintReplacement: true, // Remplacement fingerprints de base
frenchLLMPatterns: true, // Patterns spécifiques français
overlyFormalVocabulary: true, // Vocabulaire trop formel → casual
repetitiveStarters: true, // Débuts de phrases répétitifs
perfectTransitions: true, // Transitions trop parfaites
// ========================================
// FEATURES CONNECTEURS & TRANSITIONS
// ========================================
naturalConnectorsEnabled: true, // Connecteurs naturels de base
casualConnectors: true, // Connecteurs très casual (genre, enfin, bref)
hesitationMarkers: true, // Marqueurs d'hésitation (..., euh)
colloquialTransitions: true, // Transitions colloquiales
// ========================================
// FEATURES IMPERFECTIONS HUMAINES
// ========================================
humanImperfections: true, // Système d'imperfections humaines
vocabularyRepetitions: true, // Répétitions vocabulaire naturelles
casualizationIntensive: true, // Casualisation intensive
naturalHesitations: true, // Hésitations naturelles en fin de phrase
informalExpressions: true, // Expressions informelles ("pas mal", "sympa")
// ========================================
// FEATURES RESTRUCTURATION
// ========================================
intelligentRestructuring: true, // Restructuration intelligente
paragraphBreaking: true, // Cassage paragraphes longs
listToTextConversion: true, // Listes → texte naturel
redundancyInjection: true, // Injection redondances naturelles
// ========================================
// FEATURES SPÉCIALISÉES
// ========================================
personalityAdaptation: true, // Adaptation selon personnalité
temporalConsistency: true, // Cohérence temporelle (maintenant/aujourd'hui)
contextualVocabulary: true, // Vocabulaire contextuel
registerVariation: true // Variation registre langue
};
/**
* ORCHESTRATEUR PRINCIPAL - Pattern Breaking Layer
* @param {object} content - Contenu généré à traiter
* @param {object} options - Options de pattern breaking
* @returns {object} - { content, stats, fallback }
*/
async function applyPatternBreakingLayer(content, options = {}) {
return await tracer.run('PatternBreakingCore.applyPatternBreakingLayer()', async () => {
const startTime = Date.now();
await tracer.annotate({
contentKeys: Object.keys(content).length,
intensityLevel: options.intensityLevel,
personality: options.csvData?.personality?.nom
});
logSh(`🔧 PATTERN BREAKING - Début traitement`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Intensité: ${options.intensityLevel || DEFAULT_CONFIG.intensityLevel}`, 'DEBUG');
try {
// Configuration fusionnée
const config = { ...DEFAULT_CONFIG, ...options };
// Stats de pattern breaking
const patternStats = {
elementsProcessed: 0,
syntaxModifications: 0,
llmFingerprintReplacements: 0,
connectorReplacements: 0,
totalModifications: 0,
fallbackUsed: false,
patternsDetected: 0
};
// Contenu traité
let processedContent = { ...content };
// ========================================
// TRAITEMENT PAR ÉLÉMENT
// ========================================
for (const [elementKey, elementContent] of Object.entries(content)) {
await tracer.run(`PatternBreaking.processElement(${elementKey})`, async () => {
logSh(` 🎯 Traitement élément: ${elementKey}`, 'DEBUG');
let currentContent = elementContent;
let elementModifications = 0;
try {
// 1. Détection patterns LLM
const detectedPatterns = detectLLMPatterns(currentContent);
patternStats.patternsDetected += detectedPatterns.count;
if (detectedPatterns.count > 0) {
logSh(` 🔍 ${detectedPatterns.count} patterns LLM détectés: ${detectedPatterns.patterns.slice(0, 3).join(', ')}`, 'DEBUG');
}
// 2. SYNTAXE & STRUCTURE - Couche de base
if (config.syntaxVariationEnabled) {
const syntaxResult = await applySyntaxVariation(currentContent, config);
currentContent = syntaxResult.content;
elementModifications += syntaxResult.modifications;
patternStats.syntaxModifications += syntaxResult.modifications;
logSh(` 📝 Syntaxe: ${syntaxResult.modifications} variations appliquées`, 'DEBUG');
}
// 3. SYNTAXE AGRESSIVE - Couche intensive
if (config.aggressiveSentenceSplitting || config.aggressiveSentenceMerging) {
const aggressiveResult = await applyAggressiveSyntax(currentContent, config);
currentContent = aggressiveResult.content;
elementModifications += aggressiveResult.modifications;
patternStats.syntaxModifications += aggressiveResult.modifications;
logSh(` ✂️ Syntaxe agressive: ${aggressiveResult.modifications} modifications`, 'DEBUG');
}
// 4. MICRO-VARIATIONS - Subtiles mais importantes
if (config.microSyntaxVariations) {
const microResult = await applyMicroVariations(currentContent, config);
currentContent = microResult.content;
elementModifications += microResult.modifications;
patternStats.syntaxModifications += microResult.modifications;
logSh(` 🔧 Micro-variations: ${microResult.modifications} ajustements`, 'DEBUG');
}
// 5. LLM FINGERPRINTS - Détection de base
if (config.llmFingerprintReplacement && detectedPatterns.count > 0) {
const fingerprintResult = await applyLLMFingerprints(currentContent, config);
currentContent = fingerprintResult.content;
elementModifications += fingerprintResult.modifications;
patternStats.llmFingerprintReplacements += fingerprintResult.modifications;
logSh(` 🤖 LLM Fingerprints: ${fingerprintResult.modifications} remplacements`, 'DEBUG');
}
// 6. PATTERNS FRANÇAIS - Spécifique langue française
if (config.frenchLLMPatterns) {
const frenchResult = await applyFrenchPatterns(currentContent, config);
currentContent = frenchResult.content;
elementModifications += frenchResult.modifications;
patternStats.llmFingerprintReplacements += frenchResult.modifications;
logSh(` 🇫🇷 Patterns français: ${frenchResult.modifications} corrections`, 'DEBUG');
}
// 7. VOCABULAIRE FORMEL - Casualisation
if (config.overlyFormalVocabulary) {
const casualResult = await applyCasualization(currentContent, config);
currentContent = casualResult.content;
elementModifications += casualResult.modifications;
patternStats.llmFingerprintReplacements += casualResult.modifications;
logSh(` 😎 Casualisation: ${casualResult.modifications} simplifications`, 'DEBUG');
}
// 8. CONNECTEURS NATURELS - Base
if (config.naturalConnectorsEnabled) {
const connectorResult = await applyNaturalConnectors(currentContent, config);
currentContent = connectorResult.content;
elementModifications += connectorResult.modifications;
patternStats.connectorReplacements += connectorResult.modifications;
logSh(` 🔗 Connecteurs naturels: ${connectorResult.modifications} humanisés`, 'DEBUG');
}
// 9. CONNECTEURS CASUAL - Très familier
if (config.casualConnectors) {
const casualConnResult = await applyCasualConnectors(currentContent, config);
currentContent = casualConnResult.content;
elementModifications += casualConnResult.modifications;
patternStats.connectorReplacements += casualConnResult.modifications;
logSh(` 🗣️ Connecteurs casual: ${casualConnResult.modifications} familiarisés`, 'DEBUG');
}
// 10. IMPERFECTIONS HUMAINES - Système principal
if (config.humanImperfections) {
const imperfResult = await applyHumanImperfections(currentContent, config);
currentContent = imperfResult.content;
elementModifications += imperfResult.modifications;
patternStats.totalModifications += imperfResult.modifications;
logSh(` 👤 Imperfections: ${imperfResult.modifications} humanisations`, 'DEBUG');
}
// 11. QUESTIONS RHÉTORIQUES - Engagement
if (config.questionInjection) {
const questionResult = await applyQuestionInjection(currentContent, config);
currentContent = questionResult.content;
elementModifications += questionResult.modifications;
patternStats.totalModifications += questionResult.modifications;
logSh(` ❓ Questions: ${questionResult.modifications} injections`, 'DEBUG');
}
// 12. RESTRUCTURATION INTELLIGENTE - Dernière couche
if (config.intelligentRestructuring) {
const restructResult = await applyIntelligentRestructuring(currentContent, config);
currentContent = restructResult.content;
elementModifications += restructResult.modifications;
patternStats.totalModifications += restructResult.modifications;
logSh(` 🧠 Restructuration: ${restructResult.modifications} réorganisations`, 'DEBUG');
}
// 5. Validation qualité
const qualityCheck = validatePatternBreakingQuality(elementContent, currentContent, config.qualityThreshold);
if (qualityCheck.acceptable) {
processedContent[elementKey] = currentContent;
patternStats.elementsProcessed++;
patternStats.totalModifications += elementModifications;
logSh(` ✅ Élément traité: ${elementModifications} modifications totales`, 'DEBUG');
} else {
// Fallback: garder contenu original
processedContent[elementKey] = elementContent;
patternStats.fallbackUsed = true;
logSh(` ⚠️ Qualité insuffisante, fallback vers contenu original`, 'WARNING');
}
} catch (elementError) {
logSh(` ❌ Erreur pattern breaking élément ${elementKey}: ${elementError.message}`, 'WARNING');
processedContent[elementKey] = elementContent; // Fallback
patternStats.fallbackUsed = true;
}
}, { elementKey, originalLength: elementContent?.length });
}
// ========================================
// RÉSULTATS FINAUX
// ========================================
const duration = Date.now() - startTime;
const success = patternStats.elementsProcessed > 0 && !patternStats.fallbackUsed;
logSh(`🔧 PATTERN BREAKING - Terminé (${duration}ms)`, 'INFO');
logSh(`${patternStats.elementsProcessed}/${Object.keys(content).length} éléments traités`, 'INFO');
logSh(` 📊 ${patternStats.syntaxModifications} syntaxe | ${patternStats.llmFingerprintReplacements} fingerprints | ${patternStats.connectorReplacements} connecteurs`, 'INFO');
logSh(` 🎯 Patterns détectés: ${patternStats.patternsDetected} | Fallback: ${patternStats.fallbackUsed ? 'OUI' : 'NON'}`, 'INFO');
await tracer.event('Pattern Breaking terminé', {
success,
duration,
stats: patternStats
});
return {
content: processedContent,
stats: patternStats,
fallback: patternStats.fallbackUsed,
duration
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ PATTERN BREAKING ÉCHOUÉ (${duration}ms): ${error.message}`, 'ERROR');
await tracer.event('Pattern Breaking échoué', {
error: error.message,
duration,
contentKeys: Object.keys(content).length
});
// Fallback complet
return {
content,
stats: { fallbackUsed: true, error: error.message },
fallback: true,
duration
};
}
}, {
contentElements: Object.keys(content).length,
intensityLevel: options.intensityLevel
});
}
/**
* APPLICATION VARIATION SYNTAXIQUE
*/
async function applySyntaxVariation(content, config) {
const syntaxResult = varyStructures(content, config.intensityLevel, {
preserveReadability: config.preserveReadability,
maxModifications: Math.floor(config.maxModificationsPerElement / 2)
});
return {
content: syntaxResult.content,
modifications: syntaxResult.modifications || 0
};
}
/**
* APPLICATION REMPLACEMENT LLM FINGERPRINTS
*/
async function applyLLMFingerprints(content, config) {
const fingerprintResult = replaceLLMFingerprints(content, {
intensity: config.intensityLevel,
preserveContext: true,
maxReplacements: Math.floor(config.maxModificationsPerElement / 2)
});
return {
content: fingerprintResult.content,
modifications: fingerprintResult.replacements || 0
};
}
/**
* APPLICATION CONNECTEURS NATURELS
*/
async function applyNaturalConnectors(content, config) {
const connectorResult = humanizeTransitions(content, {
intensity: config.intensityLevel,
preserveMeaning: true,
maxReplacements: Math.floor(config.maxModificationsPerElement / 2)
});
return {
content: connectorResult.content,
modifications: connectorResult.replacements || 0
};
}
/**
* VALIDATION QUALITÉ PATTERN BREAKING
*/
function validatePatternBreakingQuality(originalContent, processedContent, qualityThreshold) {
if (!originalContent || !processedContent) {
return { acceptable: false, reason: 'Contenu manquant' };
}
// Métriques de base
const lengthDiff = Math.abs(processedContent.length - originalContent.length) / originalContent.length;
const wordCountOriginal = originalContent.split(/\s+/).length;
const wordCountProcessed = processedContent.split(/\s+/).length;
const wordCountDiff = Math.abs(wordCountProcessed - wordCountOriginal) / wordCountOriginal;
// Vérifications qualité
const checks = {
lengthPreserved: lengthDiff < 0.3, // Pas plus de 30% de différence
wordCountPreserved: wordCountDiff < 0.2, // Pas plus de 20% de différence
noEmpty: processedContent.trim().length > 0, // Pas de contenu vide
readableStructure: processedContent.includes('.') // Structure lisible
};
const passedChecks = Object.values(checks).filter(Boolean).length;
const score = passedChecks / Object.keys(checks).length;
const acceptable = score >= qualityThreshold;
logSh(` 🎯 Validation Pattern Breaking: ${acceptable ? 'ACCEPTÉ' : 'REJETÉ'} (score: ${score.toFixed(2)})`, acceptable ? 'DEBUG' : 'WARNING');
return {
acceptable,
score,
checks,
reason: acceptable ? 'Qualité acceptable' : 'Score qualité insuffisant'
};
}
/**
* APPLICATION SYNTAXE AGRESSIVE
* Seuils plus bas pour plus de transformations
*/
async function applyAggressiveSyntax(content, config) {
let modified = content;
let modifications = 0;
// Découpage agressif phrases longues (>80 chars au lieu de >120)
if (config.aggressiveSentenceSplitting) {
const sentences = modified.split('. ');
const processedSentences = sentences.map(sentence => {
if (sentence.length > 80 && Math.random() < (config.intensityLevel * 0.7)) {
const cutPoints = [
{ pattern: /, qui (.+)/, replacement: '. Celui-ci $1' },
{ pattern: /, que (.+)/, replacement: '. Cette solution $1' },
{ pattern: /, car (.+)/, replacement: '. En fait, $1' },
{ pattern: /, donc (.+)/, replacement: '. Du coup, $1' },
{ pattern: / et (.{20,})/, replacement: '. Aussi, $1' },
{ pattern: /, mais (.+)/, replacement: '. Par contre, $1' }
];
for (const cutPoint of cutPoints) {
if (sentence.match(cutPoint.pattern)) {
modifications++;
return sentence.replace(cutPoint.pattern, cutPoint.replacement);
}
}
}
return sentence;
});
modified = processedSentences.join('. ');
}
// Fusion agressive phrases courtes (<60 chars au lieu de <40)
if (config.aggressiveSentenceMerging) {
const sentences = modified.split('. ');
const processedSentences = [];
for (let i = 0; i < sentences.length; i++) {
const current = sentences[i];
const next = sentences[i + 1];
if (current && current.length < 60 && next && next.length < 80 && Math.random() < (config.intensityLevel * 0.5)) {
const connectors = [', du coup,', ', genre,', ', enfin,', ' et puis'];
const connector = connectors[Math.floor(Math.random() * connectors.length)];
processedSentences.push(current + connector + ' ' + next.toLowerCase());
modifications++;
i++; // Skip next sentence
} else {
processedSentences.push(current);
}
}
modified = processedSentences.join('. ');
}
return { content: modified, modifications };
}
/**
* APPLICATION MICRO-VARIATIONS
* Changements subtiles mais nombreux
*/
async function applyMicroVariations(content, config) {
let modified = content;
let modifications = 0;
const microPatterns = [
// Intensificateurs
{ from: /\btrès (.+?)\b/g, to: 'super $1', probability: 0.4 },
{ from: /\bassez (.+?)\b/g, to: 'plutôt $1', probability: 0.5 },
{ from: /\bextrêmement\b/g, to: 'vraiment', probability: 0.6 },
// Connecteurs basiques
{ from: /\bainsi\b/g, to: 'du coup', probability: 0.4 },
{ from: /\bpar conséquent\b/g, to: 'donc', probability: 0.7 },
{ from: /\bcependant\b/g, to: 'mais', probability: 0.3 },
// Formulations casual
{ from: /\bde cette manière\b/g, to: 'comme ça', probability: 0.5 },
{ from: /\bafin de\b/g, to: 'pour', probability: 0.4 },
{ from: /\ben vue de\b/g, to: 'pour', probability: 0.6 }
];
microPatterns.forEach(pattern => {
if (Math.random() < (config.intensityLevel * pattern.probability)) {
const before = modified;
modified = modified.replace(pattern.from, pattern.to);
if (modified !== before) modifications++;
}
});
return { content: modified, modifications };
}
/**
* APPLICATION PATTERNS FRANÇAIS SPÉCIFIQUES
* Détection patterns français typiques LLM
*/
async function applyFrenchPatterns(content, config) {
let modified = content;
let modifications = 0;
// Patterns français typiques LLM
const frenchPatterns = [
// Expressions trop soutenues
{ from: /\bil convient de noter que\b/gi, to: 'on peut dire que', probability: 0.8 },
{ from: /\bil est important de souligner que\b/gi, to: 'c\'est important de voir que', probability: 0.8 },
{ from: /\bdans ce contexte\b/gi, to: 'là-dessus', probability: 0.6 },
{ from: /\bpar ailleurs\b/gi, to: 'sinon', probability: 0.5 },
{ from: /\ben outre\b/gi, to: 'aussi', probability: 0.7 },
// Formulations administratives
{ from: /\bil s'avère que\b/gi, to: 'en fait', probability: 0.6 },
{ from: /\btoutefois\b/gi, to: 'par contre', probability: 0.5 },
{ from: /\bnéanmoins\b/gi, to: 'quand même', probability: 0.7 }
];
frenchPatterns.forEach(pattern => {
if (Math.random() < (config.intensityLevel * pattern.probability)) {
const before = modified;
modified = modified.replace(pattern.from, pattern.to);
if (modified !== before) modifications++;
}
});
return { content: modified, modifications };
}
/**
* APPLICATION CASUALISATION INTENSIVE
* Rendre le vocabulaire plus décontracté
*/
async function applyCasualization(content, config) {
let modified = content;
let modifications = 0;
const casualizations = [
// Verbes formels → casual
{ from: /\boptimiser\b/gi, to: 'améliorer', probability: 0.7 },
{ from: /\beffectuer\b/gi, to: 'faire', probability: 0.8 },
{ from: /\bréaliser\b/gi, to: 'faire', probability: 0.6 },
{ from: /\bmettre en œuvre\b/gi, to: 'faire', probability: 0.7 },
// Adjectifs formels → casual
{ from: /\bexceptionnel\b/gi, to: 'super', probability: 0.4 },
{ from: /\bremarquable\b/gi, to: 'pas mal', probability: 0.5 },
{ from: /\bconsidérable\b/gi, to: 'important', probability: 0.6 },
{ from: /\bsubstantiel\b/gi, to: 'important', probability: 0.8 },
// Expressions formelles → casual
{ from: /\bde manière significative\b/gi, to: 'pas mal', probability: 0.6 },
{ from: /\ben définitive\b/gi, to: 'au final', probability: 0.7 },
{ from: /\bdans l'ensemble\b/gi, to: 'globalement', probability: 0.5 }
];
casualizations.forEach(casual => {
if (Math.random() < (config.intensityLevel * casual.probability)) {
const before = modified;
modified = modified.replace(casual.from, casual.to);
if (modified !== before) modifications++;
}
});
return { content: modified, modifications };
}
/**
* APPLICATION CONNECTEURS CASUAL
* Connecteurs très familiers
*/
async function applyCasualConnectors(content, config) {
let modified = content;
let modifications = 0;
const casualConnectors = [
{ from: /\. De plus,/g, to: '. Genre,', probability: 0.3 },
{ from: /\. En outre,/g, to: '. Puis,', probability: 0.4 },
{ from: /\. Par ailleurs,/g, to: '. Sinon,', probability: 0.3 },
{ from: /\. Cependant,/g, to: '. Mais bon,', probability: 0.4 },
{ from: /\. Néanmoins,/g, to: '. Ceci dit,', probability: 0.5 },
{ from: /\. Ainsi,/g, to: '. Du coup,', probability: 0.6 }
];
casualConnectors.forEach(connector => {
if (Math.random() < (config.intensityLevel * connector.probability)) {
const before = modified;
modified = modified.replace(connector.from, connector.to);
if (modified !== before) modifications++;
}
});
return { content: modified, modifications };
}
/**
* APPLICATION IMPERFECTIONS HUMAINES
* Injection d'imperfections réalistes
*/
async function applyHumanImperfections(content, config) {
let modified = content;
let modifications = 0;
// Répétitions vocabulaire
if (config.vocabularyRepetitions && Math.random() < (config.intensityLevel * 0.4)) {
const repetitionWords = ['vraiment', 'bien', 'assez', 'plutôt', 'super'];
const word = repetitionWords[Math.floor(Math.random() * repetitionWords.length)];
const sentences = modified.split('. ');
if (sentences.length > 2) {
sentences[1] = word + ' ' + sentences[1];
modified = sentences.join('. ');
modifications++;
}
}
// Hésitations naturelles
if (config.naturalHesitations && Math.random() < (config.intensityLevel * 0.2)) {
const hesitations = ['... enfin', '... disons', '... bon'];
const hesitation = hesitations[Math.floor(Math.random() * hesitations.length)];
const words = modified.split(' ');
const insertPos = Math.floor(words.length * 0.6);
words.splice(insertPos, 0, hesitation);
modified = words.join(' ');
modifications++;
}
// Expressions informelles
if (config.informalExpressions && Math.random() < (config.intensityLevel * 0.3)) {
const informalReplacements = [
{ from: /\bc'est bien\b/gi, to: 'c\'est sympa', probability: 0.4 },
{ from: /\bc'est intéressant\b/gi, to: 'c\'est pas mal', probability: 0.5 },
{ from: /\bc'est efficace\b/gi, to: 'ça marche bien', probability: 0.4 }
];
informalReplacements.forEach(replacement => {
if (Math.random() < replacement.probability) {
const before = modified;
modified = modified.replace(replacement.from, replacement.to);
if (modified !== before) modifications++;
}
});
}
return { content: modified, modifications };
}
/**
* APPLICATION QUESTIONS RHÉTORIQUES
* Injection questions pour engagement
*/
async function applyQuestionInjection(content, config) {
let modified = content;
let modifications = 0;
if (Math.random() < (config.intensityLevel * 0.3)) {
const sentences = modified.split('. ');
if (sentences.length > 3) {
const questionTemplates = [
'Mais pourquoi est-ce important ?',
'Comment faire alors ?',
'Que faut-il retenir ?',
'Est-ce vraiment efficace ?'
];
const question = questionTemplates[Math.floor(Math.random() * questionTemplates.length)];
const insertPos = Math.floor(sentences.length / 2);
sentences.splice(insertPos, 0, question);
modified = sentences.join('. ');
modifications++;
}
}
return { content: modified, modifications };
}
/**
* APPLICATION RESTRUCTURATION INTELLIGENTE
* Réorganisation structure générale
*/
async function applyIntelligentRestructuring(content, config) {
let modified = content;
let modifications = 0;
// Cassage paragraphes longs
if (config.paragraphBreaking && modified.length > 400) {
const sentences = modified.split('. ');
if (sentences.length > 6) {
const breakPoint = Math.floor(sentences.length / 2);
sentences[breakPoint] = sentences[breakPoint] + '\n\n';
modified = sentences.join('. ');
modifications++;
}
}
// Injection redondances naturelles
if (config.redundancyInjection && Math.random() < (config.intensityLevel * 0.2)) {
const redundancies = ['comme je le disais', 'encore une fois', 'je le répète'];
const redundancy = redundancies[Math.floor(Math.random() * redundancies.length)];
const sentences = modified.split('. ');
if (sentences.length > 2) {
sentences[sentences.length - 2] = redundancy + ', ' + sentences[sentences.length - 2];
modified = sentences.join('. ');
modifications++;
}
}
return { content: modified, modifications };
}
// ============= EXPORTS =============
module.exports = {
applyPatternBreakingLayer,
applySyntaxVariation,
applyLLMFingerprints,
applyNaturalConnectors,
validatePatternBreakingQuality,
applyAggressiveSyntax,
applyMicroVariations,
applyFrenchPatterns,
applyCasualization,
applyCasualConnectors,
applyHumanImperfections,
applyQuestionInjection,
applyIntelligentRestructuring,
DEFAULT_CONFIG
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/pattern-breaking/PatternBreakingLayers.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: PatternBreakingLayers.js
// RESPONSABILITÉ: Stacks prédéfinis pour Pattern Breaking
// Configurations optimisées par cas d'usage
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* CONFIGURATIONS PRÉDÉFINIES PATTERN BREAKING
* Optimisées pour différents niveaux et cas d'usage
*/
const PATTERN_BREAKING_STACKS = {
// ========================================
// STACK LÉGER - Usage quotidien
// ========================================
lightPatternBreaking: {
name: 'Light Pattern Breaking',
description: 'Anti-détection subtile pour usage quotidien',
intensity: 0.3,
config: {
syntaxVariationEnabled: true,
llmFingerprintReplacement: false, // Pas de remplacement mots
naturalConnectorsEnabled: true,
preserveReadability: true,
maxModificationsPerElement: 2,
qualityThreshold: 0.7
},
expectedReduction: '10-15%',
useCase: 'Articles standard, faible risque détection'
},
// ========================================
// STACK STANDARD - Équilibre optimal
// ========================================
standardPatternBreaking: {
name: 'Standard Pattern Breaking',
description: 'Équilibre optimal efficacité/naturalité',
intensity: 0.5,
config: {
syntaxVariationEnabled: true,
llmFingerprintReplacement: true,
naturalConnectorsEnabled: true,
preserveReadability: true,
maxModificationsPerElement: 4,
qualityThreshold: 0.6
},
expectedReduction: '20-25%',
useCase: 'Usage général recommandé'
},
// ========================================
// STACK INTENSIF - Anti-détection poussée
// ========================================
heavyPatternBreaking: {
name: 'Heavy Pattern Breaking',
description: 'Anti-détection intensive pour cas critiques',
intensity: 0.8,
config: {
syntaxVariationEnabled: true,
llmFingerprintReplacement: true,
naturalConnectorsEnabled: true,
preserveReadability: true,
maxModificationsPerElement: 6,
qualityThreshold: 0.5
},
expectedReduction: '30-35%',
useCase: 'Détection élevée, contenu critique'
},
// ========================================
// STACK ADAPTATIF - Selon contenu
// ========================================
adaptivePatternBreaking: {
name: 'Adaptive Pattern Breaking',
description: 'Adaptation intelligente selon détection patterns',
intensity: 0.6,
config: {
syntaxVariationEnabled: true,
llmFingerprintReplacement: true,
naturalConnectorsEnabled: true,
preserveReadability: true,
maxModificationsPerElement: 5,
qualityThreshold: 0.6,
adaptiveMode: true // Ajuste selon détection patterns
},
expectedReduction: '25-30%',
useCase: 'Adaptation automatique par contenu'
},
// ========================================
// STACK SYNTAXE FOCUS - Syntaxe uniquement
// ========================================
syntaxFocus: {
name: 'Syntax Focus',
description: 'Focus sur variations syntaxiques uniquement',
intensity: 0.7,
config: {
syntaxVariationEnabled: true,
llmFingerprintReplacement: false,
naturalConnectorsEnabled: false,
preserveReadability: true,
maxModificationsPerElement: 6,
qualityThreshold: 0.7
},
expectedReduction: '15-20%',
useCase: 'Préservation vocabulaire, syntaxe variable'
},
// ========================================
// STACK CONNECTEURS FOCUS - Connecteurs uniquement
// ========================================
connectorsFocus: {
name: 'Connectors Focus',
description: 'Humanisation connecteurs et transitions',
intensity: 0.8,
config: {
syntaxVariationEnabled: false,
llmFingerprintReplacement: false,
naturalConnectorsEnabled: true,
preserveReadability: true,
maxModificationsPerElement: 4,
qualityThreshold: 0.8,
connectorTone: 'casual' // casual, conversational, technical, commercial
},
expectedReduction: '12-18%',
useCase: 'Textes formels à humaniser'
}
};
/**
* APPLICATION STACK PATTERN BREAKING
* @param {string} stackName - Nom du stack à appliquer
* @param {object} content - Contenu à traiter
* @param {object} overrides - Options pour surcharger le stack
* @returns {object} - { content, stats, stackUsed }
*/
async function applyPatternBreakingStack(stackName, content, overrides = {}) {
const { applyPatternBreakingLayer } = require('./PatternBreakingCore');
logSh(`📦 Application Stack Pattern Breaking: ${stackName}`, 'INFO');
const stack = PATTERN_BREAKING_STACKS[stackName];
if (!stack) {
logSh(`❌ Stack Pattern Breaking inconnu: ${stackName}`, 'WARNING');
throw new Error(`Stack Pattern Breaking inconnu: ${stackName}`);
}
try {
// Configuration fusionnée (stack + overrides)
const finalConfig = {
...stack.config,
intensityLevel: stack.intensity,
...overrides
};
logSh(` 🎯 Configuration: ${stack.description}`, 'DEBUG');
logSh(` ⚡ Intensité: ${finalConfig.intensityLevel} | Réduction attendue: ${stack.expectedReduction}`, 'DEBUG');
// Mode adaptatif si activé
if (finalConfig.adaptiveMode) {
const adaptedConfig = await adaptConfigurationToContent(content, finalConfig);
Object.assign(finalConfig, adaptedConfig);
logSh(` 🧠 Mode adaptatif appliqué`, 'DEBUG');
}
// Application Pattern Breaking
const result = await applyPatternBreakingLayer(content, finalConfig);
logSh(`📦 Stack Pattern Breaking terminé: ${result.stats?.totalModifications || 0} modifications`, 'INFO');
return {
content: result.content,
stats: {
...result.stats,
stackUsed: stackName,
stackDescription: stack.description,
expectedReduction: stack.expectedReduction
},
fallback: result.fallback,
stackUsed: stackName
};
} catch (error) {
logSh(`❌ Erreur application Stack Pattern Breaking ${stackName}: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* ADAPTATION CONFIGURATION SELON CONTENU
*/
async function adaptConfigurationToContent(content, baseConfig) {
const { detectLLMPatterns } = require('./LLMFingerprints');
const { detectFormalConnectors } = require('./NaturalConnectors');
logSh(`🧠 Adaptation configuration selon contenu...`, 'DEBUG');
const adaptations = { ...baseConfig };
try {
// Analyser patterns LLM
const llmDetection = detectLLMPatterns(content);
const formalDetection = detectFormalConnectors(content);
logSh(` 📊 Patterns LLM: ${llmDetection.count} (score: ${llmDetection.suspicionScore.toFixed(3)})`, 'DEBUG');
logSh(` 📊 Connecteurs formels: ${formalDetection.count} (score: ${formalDetection.suspicionScore.toFixed(3)})`, 'DEBUG');
// Adapter selon détection patterns LLM
if (llmDetection.suspicionScore > 0.06) {
adaptations.llmFingerprintReplacement = true;
adaptations.intensityLevel = Math.min(1.0, baseConfig.intensityLevel + 0.2);
logSh(` 🔧 Intensité augmentée pour patterns LLM élevés: ${adaptations.intensityLevel}`, 'DEBUG');
} else if (llmDetection.suspicionScore < 0.02) {
adaptations.llmFingerprintReplacement = false;
logSh(` 🔧 Remplacement LLM désactivé (faible détection)`, 'DEBUG');
}
// Adapter selon connecteurs formels
if (formalDetection.suspicionScore > 0.04) {
adaptations.naturalConnectorsEnabled = true;
adaptations.maxModificationsPerElement = Math.min(8, baseConfig.maxModificationsPerElement + 2);
logSh(` 🔧 Focus connecteurs activé: max ${adaptations.maxModificationsPerElement} modifications`, 'DEBUG');
}
// Adapter selon longueur texte
const wordCount = content.split(/\s+/).length;
if (wordCount > 500) {
adaptations.maxModificationsPerElement = Math.min(10, baseConfig.maxModificationsPerElement + 3);
logSh(` 🔧 Texte long détecté: max ${adaptations.maxModificationsPerElement} modifications`, 'DEBUG');
}
} catch (error) {
logSh(`⚠️ Erreur adaptation configuration: ${error.message}`, 'WARNING');
}
return adaptations;
}
/**
* RECOMMANDATION STACK AUTOMATIQUE
*/
function recommendPatternBreakingStack(content, context = {}) {
const { detectLLMPatterns } = require('./LLMFingerprints');
const { detectFormalConnectors } = require('./NaturalConnectors');
try {
const llmDetection = detectLLMPatterns(content);
const formalDetection = detectFormalConnectors(content);
const wordCount = content.split(/\s+/).length;
logSh(`🤖 Recommandation Stack Pattern Breaking...`, 'DEBUG');
// Critères de recommandation
const criteria = {
llmPatternsHigh: llmDetection.suspicionScore > 0.05,
formalConnectorsHigh: formalDetection.suspicionScore > 0.03,
longContent: wordCount > 300,
criticalContext: context.critical === true,
preserveQuality: context.preserveQuality === true
};
// Logique de recommandation
let recommendedStack = 'standardPatternBreaking';
let reason = 'Configuration équilibrée par défaut';
if (criteria.criticalContext) {
recommendedStack = 'heavyPatternBreaking';
reason = 'Contexte critique détecté';
} else if (criteria.llmPatternsHigh && criteria.formalConnectorsHigh) {
recommendedStack = 'heavyPatternBreaking';
reason = 'Patterns LLM et connecteurs formels élevés';
} else if (criteria.llmPatternsHigh) {
recommendedStack = 'adaptivePatternBreaking';
reason = 'Patterns LLM élevés détectés';
} else if (criteria.formalConnectorsHigh) {
recommendedStack = 'connectorsFocus';
reason = 'Connecteurs formels prédominants';
} else if (criteria.preserveQuality) {
recommendedStack = 'lightPatternBreaking';
reason = 'Préservation qualité prioritaire';
} else if (!criteria.llmPatternsHigh && !criteria.formalConnectorsHigh) {
recommendedStack = 'syntaxFocus';
reason = 'Faible détection patterns, focus syntaxe';
}
logSh(`🎯 Stack recommandé: ${recommendedStack} (${reason})`, 'DEBUG');
return {
recommendedStack,
reason,
criteria,
confidence: calculateRecommendationConfidence(criteria)
};
} catch (error) {
logSh(`⚠️ Erreur recommandation Stack: ${error.message}`, 'WARNING');
return {
recommendedStack: 'standardPatternBreaking',
reason: 'Fallback suite erreur analyse',
criteria: {},
confidence: 0.5
};
}
}
/**
* CALCUL CONFIANCE RECOMMANDATION
*/
function calculateRecommendationConfidence(criteria) {
let confidence = 0.5; // Base
// Augmenter confiance selon critères détectés
if (criteria.llmPatternsHigh) confidence += 0.2;
if (criteria.formalConnectorsHigh) confidence += 0.2;
if (criteria.criticalContext) confidence += 0.3;
if (criteria.longContent) confidence += 0.1;
return Math.min(1.0, confidence);
}
/**
* LISTE STACKS DISPONIBLES
*/
function listAvailableStacks() {
return Object.entries(PATTERN_BREAKING_STACKS).map(([key, stack]) => ({
name: key,
displayName: stack.name,
description: stack.description,
intensity: stack.intensity,
expectedReduction: stack.expectedReduction,
useCase: stack.useCase
}));
}
/**
* VALIDATION STACK
*/
function validateStack(stackName) {
const stack = PATTERN_BREAKING_STACKS[stackName];
if (!stack) {
return { valid: false, error: `Stack inconnu: ${stackName}` };
}
// Vérifications configuration
const config = stack.config;
const checks = {
hasIntensity: typeof stack.intensity === 'number',
hasConfig: typeof config === 'object',
hasValidThreshold: config.qualityThreshold >= 0 && config.qualityThreshold <= 1,
hasValidMaxMods: config.maxModificationsPerElement > 0
};
const valid = Object.values(checks).every(Boolean);
return {
valid,
checks,
error: valid ? null : 'Configuration stack invalide'
};
}
// ============= EXPORTS =============
module.exports = {
applyPatternBreakingStack,
recommendPatternBreakingStack,
adaptConfigurationToContent,
listAvailableStacks,
validateStack,
PATTERN_BREAKING_STACKS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/Main.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// MAIN MODULAIRE - PIPELINE ARCHITECTURALE MODERNE
// Responsabilité: Orchestration workflow avec architecture modulaire complète
// Usage: node main_modulaire.js [rowNumber] [stackType]
// ========================================
const { logSh } = require('./ErrorReporting');
const { tracer } = require('./trace');
// Imports pipeline de base
const { readInstructionsData, selectPersonalityWithAI, getPersonalities } = require('./BrainConfig');
const { extractElements, buildSmartHierarchy } = require('./ElementExtraction');
const { generateMissingKeywords } = require('./MissingKeywords');
// ContentGeneration.js supprimé - Utiliser generateSimple depuis selective-enhancement
const { generateSimple } = require('./selective-enhancement/SelectiveUtils');
const { injectGeneratedContent } = require('./ContentAssembly');
const { saveGeneratedArticleOrganic } = require('./ArticleStorage');
// Imports modules modulaires
const { applySelectiveLayer } = require('./selective-enhancement/SelectiveCore');
const {
applyPredefinedStack,
applyAdaptiveLayers,
getAvailableStacks
} = require('./selective-enhancement/SelectiveLayers');
const {
applyAdversarialLayer
} = require('./adversarial-generation/AdversarialCore');
const {
applyPredefinedStack: applyAdversarialStack
} = require('./adversarial-generation/AdversarialLayers');
const {
applyHumanSimulationLayer
} = require('./human-simulation/HumanSimulationCore');
const {
applyPredefinedSimulation,
getAvailableSimulationStacks,
recommendSimulationStack
} = require('./human-simulation/HumanSimulationLayers');
const {
applyPatternBreakingLayer
} = require('./pattern-breaking/PatternBreakingCore');
const {
applyPatternBreakingStack,
recommendPatternBreakingStack,
listAvailableStacks: listPatternBreakingStacks
} = require('./pattern-breaking/PatternBreakingLayers');
/**
* WORKFLOW MODULAIRE AVEC DONNÉES FOURNIES (COMPATIBILITÉ MAKE.COM/DIGITAL OCEAN)
*/
async function handleModularWorkflowWithData(data, config = {}) {
return await tracer.run('Main.handleModularWorkflowWithData()', async () => {
const {
selectiveStack = 'standardEnhancement',
adversarialMode = 'light',
humanSimulationMode = 'none',
patternBreakingMode = 'none',
saveIntermediateSteps = false,
source = 'compatibility_mode'
} = config;
await tracer.annotate({
modularWorkflow: true,
compatibilityMode: true,
selectiveStack,
adversarialMode,
humanSimulationMode,
patternBreakingMode,
source
});
const startTime = Date.now();
logSh(`🚀 WORKFLOW MODULAIRE COMPATIBILITÉ DÉMARRÉ`, 'INFO');
logSh(` 📊 Source: ${source} | Selective: ${selectiveStack} | Adversarial: ${adversarialMode}`, 'INFO');
try {
// Utiliser les données fournies directement (skippping phases 1-4)
const csvData = data.csvData;
const xmlTemplate = data.xmlTemplate;
// Décoder XML si nécessaire
let xmlString = xmlTemplate;
if (xmlTemplate && !xmlTemplate.startsWith('<?xml') && !xmlTemplate.startsWith('<')) {
xmlString = Buffer.from(xmlTemplate, 'base64').toString('utf8');
}
csvData.xmlTemplate = xmlString;
// ========================================
// PHASE 2-4: EXTRACTION ÉLÉMENTS (SIMPLIFIÉ)
// ========================================
logSh(`📝 PHASE 2-4: Extraction éléments et préparation`, 'INFO');
const elements = await extractElements(xmlString, csvData);
const finalElements = await generateMissingKeywords(elements, csvData);
// S'assurer que finalElements est un array
const elementsArray = Array.isArray(finalElements) ? finalElements :
(finalElements && typeof finalElements === 'object') ? Object.values(finalElements) : [];
const hierarchy = await buildSmartHierarchy(elementsArray);
logSh(`${Object.keys(hierarchy).length} sections hiérarchisées`, 'DEBUG');
// ========================================
// PHASE 5-8B: WORKFLOW MODULAIRE COMPLET
// ========================================
// Continuer avec le workflow modulaire normal
const generationResult = await generateSimple(hierarchy, csvData);
const generatedContent = generationResult.content || generationResult;
logSh(`${Object.keys(generatedContent).length} éléments générés`, 'DEBUG');
// Appliquer toutes les couches modulaires comme dans le workflow normal
let finalContent = generatedContent;
let versionHistory = [];
let parentArticleId = null;
// SELECTIVE ENHANCEMENT
logSh(`🔧 PHASE 6: Selective Enhancement Modulaire (${selectiveStack})`, 'INFO');
let selectiveResult;
if (selectiveStack === 'adaptive') {
selectiveResult = await applyAdaptiveLayers(finalContent, {
maxIntensity: 1.1,
analysisThreshold: 0.3,
csvData
});
} else {
selectiveResult = await applyPredefinedStack(finalContent, selectiveStack, {
csvData,
analysisMode: true
});
}
finalContent = selectiveResult.content;
logSh(` ✅ Selective: ${selectiveResult.stats.elementsEnhanced || selectiveResult.stats.totalModifications || 0} améliorations`, 'INFO');
// ADVERSARIAL ENHANCEMENT
if (adversarialMode !== 'none') {
logSh(`🎯 PHASE 7: Adversarial Enhancement (${adversarialMode})`, 'INFO');
let adversarialResult;
if (adversarialMode === 'adaptive') {
adversarialResult = await applyAdversarialLayer(finalContent, {
detectorTarget: 'general',
method: 'hybrid',
intensity: 0.8,
analysisMode: true
});
} else {
const stackMapping = {
light: 'lightDefense',
standard: 'standardDefense',
heavy: 'heavyDefense'
};
adversarialResult = await applyAdversarialStack(finalContent, stackMapping[adversarialMode], {
csvData
});
}
if (adversarialResult && !adversarialResult.fallback) {
finalContent = adversarialResult.content;
logSh(` ✅ Adversarial: ${adversarialResult.stats.elementsModified || 0} modifications`, 'INFO');
}
}
// HUMAN SIMULATION & PATTERN BREAKING (si demandés)
// ... Implémentations similaires au workflow complet
// ASSEMBLAGE FINAL
logSh(`🔗 PHASE 9: Assemblage et stockage final`, 'INFO');
const assembledContent = await injectGeneratedContent(xmlString, finalContent, finalElements);
const storageResult = await saveGeneratedArticleOrganic({ generatedTexts: finalContent }, csvData, {
version: '1.0',
stage: 'final_version',
source,
stageDescription: `Version finale compatibility mode`,
useVersionedSheet: false
});
logSh(` ✅ Stocké: ${storageResult.textLength} caractères`, 'DEBUG');
// RÉSUMÉ FINAL
const totalDuration = Date.now() - startTime;
const finalStats = {
source,
selectiveStack,
adversarialMode,
totalDuration,
elementsGenerated: Object.keys(generatedContent).length,
finalLength: storageResult.textLength,
personality: csvData.personality?.nom || 'Unknown'
};
logSh(`✅ WORKFLOW MODULAIRE COMPATIBILITÉ TERMINÉ (${totalDuration}ms)`, 'INFO');
await tracer.event('Workflow modulaire compatibilité terminé', finalStats);
return {
success: true,
stats: finalStats,
content: finalContent,
assembledContent,
storageResult,
xmlContent: assembledContent,
generatedTexts: finalContent,
elementsGenerated: finalElements.length,
personality: csvData.personality?.nom || 'Unknown',
csvData,
timestamp: new Date().toISOString(),
// Compatibilité avec l'ancien format de réponse
antiDetectionLevel: 'Selective_Enhancement',
llmsUsed: ['claude', 'openai', 'gemini', 'mistral'],
enhancementApplied: true,
workflowVersion: '2.0-Modular'
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ WORKFLOW MODULAIRE COMPATIBILITÉ ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
logSh(`Stack trace: ${error.stack}`, 'ERROR');
await tracer.event('Workflow modulaire compatibilité échoué', {
error: error.message,
duration,
source
});
throw error;
}
}, { data, config });
}
/**
* WORKFLOW MODULAIRE PRINCIPAL
*/
async function handleModularWorkflow(config = {}) {
return await tracer.run('MainModulaire.handleModularWorkflow()', async () => {
const {
rowNumber = 2,
selectiveStack = 'standardEnhancement', // lightEnhancement, standardEnhancement, fullEnhancement, personalityFocus, fluidityFocus, adaptive
adversarialMode = 'light', // none, light, standard, heavy, adaptive
humanSimulationMode = 'none', // none, lightSimulation, standardSimulation, heavySimulation, adaptiveSimulation, personalityFocus, temporalFocus
patternBreakingMode = 'none', // none, lightPatternBreaking, standardPatternBreaking, heavyPatternBreaking, adaptivePatternBreaking, syntaxFocus, connectorsFocus
saveIntermediateSteps = true, // 🆕 NOUVELLE OPTION: Sauvegarder chaque étape
source = 'main_modulaire'
} = config;
await tracer.annotate({
modularWorkflow: true,
rowNumber,
selectiveStack,
adversarialMode,
humanSimulationMode,
patternBreakingMode,
source
});
const startTime = Date.now();
logSh(`🚀 WORKFLOW MODULAIRE DÉMARRÉ`, 'INFO');
logSh(` 📊 Ligne: ${rowNumber} | Selective: ${selectiveStack} | Adversarial: ${adversarialMode} | Human: ${humanSimulationMode} | Pattern: ${patternBreakingMode}`, 'INFO');
try {
// ========================================
// PHASE 1: PRÉPARATION DONNÉES
// ========================================
logSh(`📋 PHASE 1: Préparation données`, 'INFO');
const csvData = await readInstructionsData(rowNumber);
if (!csvData) {
throw new Error(`Impossible de lire les données ligne ${rowNumber}`);
}
const personalities = await getPersonalities();
const selectedPersonality = await selectPersonalityWithAI(
csvData.mc0,
csvData.t0,
personalities
);
csvData.personality = selectedPersonality;
logSh(` ✅ Données: ${csvData.mc0} | Personnalité: ${selectedPersonality.nom}`, 'DEBUG');
// ========================================
// PHASE 2: EXTRACTION ÉLÉMENTS
// ========================================
logSh(`📝 PHASE 2: Extraction éléments XML`, 'INFO');
const elements = await extractElements(csvData.xmlTemplate, csvData);
logSh(`${elements.length} éléments extraits`, 'DEBUG');
// ========================================
// PHASE 3: GÉNÉRATION MOTS-CLÉS MANQUANTS
// ========================================
logSh(`🔍 PHASE 3: Génération mots-clés manquants`, 'INFO');
const finalElements = await generateMissingKeywords(elements, csvData);
logSh(` ✅ Mots-clés complétés`, 'DEBUG');
// ========================================
// PHASE 4: CONSTRUCTION HIÉRARCHIE
// ========================================
logSh(`🏗️ PHASE 4: Construction hiérarchie`, 'INFO');
const hierarchy = await buildSmartHierarchy(finalElements);
logSh(`${Object.keys(hierarchy).length} sections hiérarchisées`, 'DEBUG');
// ========================================
// PHASE 5: GÉNÉRATION CONTENU DE BASE
// ========================================
logSh(`💫 PHASE 5: Génération contenu de base`, 'INFO');
const generatedContent = await generateSimple(hierarchy, csvData);
logSh(`${Object.keys(generatedContent).length} éléments générés`, 'DEBUG');
// 🆕 SAUVEGARDE ÉTAPE 1: Génération initiale
let parentArticleId = null;
let versionHistory = [];
if (saveIntermediateSteps) {
logSh(`💾 SAUVEGARDE v1.0: Génération initiale`, 'INFO');
const xmlString = csvData.xmlTemplate.startsWith('<?xml') ? csvData.xmlTemplate : Buffer.from(csvData.xmlTemplate, 'base64').toString('utf8');
const initialAssembledContent = await injectGeneratedContent(xmlString, generatedContent, finalElements);
const initialStorage = await saveGeneratedArticleOrganic({ generatedTexts: generatedContent }, csvData, {
version: 'v1.0',
stage: 'initial_generation',
source: `${source}_initial`,
stageDescription: 'Génération de contenu de base sans améliorations',
useVersionedSheet: true // 🆕 Utiliser Generated_Articles_Versioned
});
parentArticleId = initialStorage.articleId;
versionHistory.push({
version: 'v1.0',
stage: 'initial_generation',
articleId: initialStorage.articleId,
length: initialStorage.textLength,
wordCount: initialStorage.wordCount
});
logSh(` ✅ Sauvé v1.0 - ID: ${parentArticleId}`, 'DEBUG');
}
// ========================================
// PHASE 6: SELECTIVE ENHANCEMENT MODULAIRE
// ========================================
logSh(`🔧 PHASE 6: Selective Enhancement Modulaire (${selectiveStack})`, 'INFO');
let selectiveResult;
switch (selectiveStack) {
case 'adaptive':
selectiveResult = await applyAdaptiveLayers(generatedContent, {
maxIntensity: 1.1,
analysisThreshold: 0.3,
csvData
});
break;
case 'technical':
case 'transitions':
case 'style':
selectiveResult = await applySelectiveLayer(generatedContent, {
layerType: selectiveStack,
llmProvider: 'auto',
intensity: 1.0,
csvData
});
break;
default:
// Stack prédéfini
selectiveResult = await applyPredefinedStack(generatedContent, selectiveStack, {
csvData,
analysisMode: true
});
}
const enhancedContent = selectiveResult.content;
logSh(` ✅ Selective: ${selectiveResult.stats.elementsEnhanced || selectiveResult.stats.totalModifications || 0} améliorations`, 'INFO');
// 🆕 SAUVEGARDE ÉTAPE 2: Selective Enhancement
if (saveIntermediateSteps) {
logSh(`💾 SAUVEGARDE v1.1: Selective Enhancement`, 'INFO');
const xmlString = csvData.xmlTemplate.startsWith('<?xml') ? csvData.xmlTemplate : Buffer.from(csvData.xmlTemplate, 'base64').toString('utf8');
const selectiveAssembledContent = await injectGeneratedContent(xmlString, enhancedContent, finalElements);
const selectiveStorage = await saveGeneratedArticleOrganic({ generatedTexts: enhancedContent }, csvData, {
version: 'v1.1',
stage: 'selective_enhancement',
source: `${source}_selective_${selectiveStack}`,
stageDescription: `Amélioration selective (${selectiveStack}) - ${selectiveResult.stats.elementsEnhanced || selectiveResult.stats.totalModifications || 0} modifications`,
parentArticleId: parentArticleId,
useVersionedSheet: true // 🆕 Utiliser Generated_Articles_Versioned
});
versionHistory.push({
version: 'v1.1',
stage: 'selective_enhancement',
articleId: selectiveStorage.articleId,
length: selectiveStorage.textLength,
wordCount: selectiveStorage.wordCount,
modifications: selectiveResult.stats.elementsEnhanced || selectiveResult.stats.totalModifications || 0
});
logSh(` ✅ Sauvé v1.1 - ID: ${selectiveStorage.articleId}`, 'DEBUG');
}
// ========================================
// PHASE 7: ADVERSARIAL ENHANCEMENT (OPTIONNEL)
// ========================================
let finalContent = enhancedContent;
let adversarialStats = null;
if (adversarialMode !== 'none') {
logSh(`🎯 PHASE 7: Adversarial Enhancement (${adversarialMode})`, 'INFO');
let adversarialResult;
switch (adversarialMode) {
case 'adaptive':
// Utiliser adversarial adaptatif
adversarialResult = await applyAdversarialLayer(enhancedContent, {
detectorTarget: 'general',
method: 'hybrid',
intensity: 0.8,
analysisMode: true
});
break;
case 'light':
case 'standard':
case 'heavy':
// Utiliser stack adversarial prédéfini
const stackMapping = {
light: 'lightDefense',
standard: 'standardDefense',
heavy: 'heavyDefense'
};
adversarialResult = await applyAdversarialStack(enhancedContent, stackMapping[adversarialMode], {
csvData
});
break;
}
if (adversarialResult && !adversarialResult.fallback) {
finalContent = adversarialResult.content;
adversarialStats = adversarialResult.stats;
logSh(` ✅ Adversarial: ${adversarialStats.elementsModified || adversarialStats.totalModifications || 0} modifications`, 'INFO');
// 🆕 SAUVEGARDE ÉTAPE 3: Adversarial Enhancement
if (saveIntermediateSteps) {
logSh(`💾 SAUVEGARDE v1.2: Adversarial Enhancement`, 'INFO');
const xmlString = csvData.xmlTemplate.startsWith('<?xml') ? csvData.xmlTemplate : Buffer.from(csvData.xmlTemplate, 'base64').toString('utf8');
const adversarialAssembledContent = await injectGeneratedContent(xmlString, finalContent, finalElements);
const adversarialStorage = await saveGeneratedArticleOrganic({ generatedTexts: finalContent }, csvData, {
version: 'v1.2',
stage: 'adversarial_enhancement',
source: `${source}_adversarial_${adversarialMode}`,
stageDescription: `Amélioration adversarial (${adversarialMode}) - ${adversarialStats.elementsModified || adversarialStats.totalModifications || 0} modifications`,
parentArticleId: parentArticleId,
useVersionedSheet: true // 🆕 Utiliser Generated_Articles_Versioned
});
versionHistory.push({
version: 'v1.2',
stage: 'adversarial_enhancement',
articleId: adversarialStorage.articleId,
length: adversarialStorage.textLength,
wordCount: adversarialStorage.wordCount,
modifications: adversarialStats.elementsModified || adversarialStats.totalModifications || 0
});
logSh(` ✅ Sauvé v1.2 - ID: ${adversarialStorage.articleId}`, 'DEBUG');
}
} else {
logSh(` ⚠️ Adversarial fallback: contenu selective préservé`, 'WARNING');
}
}
// ========================================
// PHASE 8: HUMAN SIMULATION (NIVEAU 5)
// ========================================
let humanSimulationStats = null;
if (humanSimulationMode !== 'none') {
logSh(`🧠 PHASE 8: Human Simulation (${humanSimulationMode})`, 'INFO');
let humanSimulationResult;
try {
// Calculer éléments pour simulation fatigue
const totalElements = Object.keys(finalContent).length;
const elementIndex = Math.floor(totalElements * 0.6); // Position simulée milieu-fin pour fatigue
// Configuration simulation
const simulationConfig = {
elementIndex,
totalElements,
currentHour: new Date().getHours(),
csvData,
source: `human_simulation_${humanSimulationMode}`
};
// Application selon mode
switch (humanSimulationMode) {
case 'adaptiveSimulation':
// Recommandation automatique si adaptive
const context = {
contentLength: Object.values(finalContent).join('').length,
personality: csvData.personality?.nom,
hour: new Date().getHours(),
goal: 'adaptive'
};
const recommendedStack = recommendSimulationStack(context);
logSh(` 🤖 Recommandation adaptive: ${recommendedStack}`, 'DEBUG');
humanSimulationResult = await applyPredefinedSimulation(finalContent, recommendedStack, simulationConfig);
break;
case 'lightSimulation':
case 'standardSimulation':
case 'heavySimulation':
case 'personalityFocus':
case 'temporalFocus':
// Stack prédéfini
humanSimulationResult = await applyPredefinedSimulation(finalContent, humanSimulationMode, simulationConfig);
break;
default:
// Mode custom ou direct
humanSimulationResult = await applyHumanSimulationLayer(finalContent, simulationConfig);
}
// Vérification résultat
if (humanSimulationResult && !humanSimulationResult.fallback) {
finalContent = humanSimulationResult.content;
humanSimulationStats = humanSimulationResult.stats;
logSh(` ✅ Human Simulation: ${humanSimulationStats.totalModifications || 0} modifications`, 'INFO');
logSh(` 🎯 Score qualité: ${humanSimulationResult.qualityScore?.toFixed(2) || 'N/A'}`, 'INFO');
// 🆕 SAUVEGARDE ÉTAPE 4: Human Simulation
if (saveIntermediateSteps) {
logSh(`💾 SAUVEGARDE v1.3: Human Simulation`, 'INFO');
const xmlString = csvData.xmlTemplate.startsWith('<?xml') ? csvData.xmlTemplate : Buffer.from(csvData.xmlTemplate, 'base64').toString('utf8');
const humanAssembledContent = await injectGeneratedContent(xmlString, finalContent, finalElements);
const humanStorage = await saveGeneratedArticleOrganic({ generatedTexts: finalContent }, csvData, {
version: 'v1.3',
stage: 'human_simulation',
source: `${source}_human_${humanSimulationMode}`,
stageDescription: `Simulation humaine (${humanSimulationMode}) - ${humanSimulationStats.totalModifications || 0} modifications`,
parentArticleId: parentArticleId,
useVersionedSheet: true // 🆕 Utiliser Generated_Articles_Versioned
});
versionHistory.push({
version: 'v1.3',
stage: 'human_simulation',
articleId: humanStorage.articleId,
length: humanStorage.textLength,
wordCount: humanStorage.wordCount,
modifications: humanSimulationStats.totalModifications || 0,
qualityScore: humanSimulationResult.qualityScore
});
logSh(` ✅ Sauvé v1.3 - ID: ${humanStorage.articleId}`, 'DEBUG');
}
} else {
logSh(` ⚠️ Human Simulation fallback: contenu précédent préservé`, 'WARNING');
}
} catch (humanSimulationError) {
logSh(` ❌ Erreur Human Simulation: ${humanSimulationError.message}`, 'ERROR');
logSh(` 🔄 Fallback: contenu adversarial préservé`, 'INFO');
}
}
// ========================================
// PHASE 8B: PATTERN BREAKING (NIVEAU 2)
// ========================================
let patternBreakingStats = null;
if (patternBreakingMode !== 'none') {
logSh(`🔧 PHASE 8B: Pattern Breaking (${patternBreakingMode})`, 'INFO');
let patternBreakingResult;
try {
// Configuration Pattern Breaking
const patternConfig = {
csvData,
source: `pattern_breaking_${patternBreakingMode}`,
personality: csvData.personality
};
// Application selon mode
switch (patternBreakingMode) {
case 'adaptivePatternBreaking':
// Recommandation automatique si adaptive
const patternContext = {
contentLength: Object.values(finalContent).join('').length,
critical: false,
preserveQuality: true
};
const recommendedPattern = recommendPatternBreakingStack(Object.values(finalContent).join(' '), patternContext);
logSh(` 🤖 Recommandation Pattern Breaking: ${recommendedPattern.recommendedStack}`, 'DEBUG');
patternBreakingResult = await applyPatternBreakingStack(recommendedPattern.recommendedStack, finalContent, patternConfig);
break;
case 'lightPatternBreaking':
case 'standardPatternBreaking':
case 'heavyPatternBreaking':
case 'syntaxFocus':
case 'connectorsFocus':
// Stack prédéfini
patternBreakingResult = await applyPatternBreakingStack(patternBreakingMode, finalContent, patternConfig);
break;
default:
// Mode custom ou direct
patternBreakingResult = await applyPatternBreakingLayer(finalContent, patternConfig);
}
// Vérification résultat
if (patternBreakingResult && !patternBreakingResult.fallback) {
finalContent = patternBreakingResult.content;
patternBreakingStats = patternBreakingResult.stats;
logSh(` ✅ Pattern Breaking: ${patternBreakingStats.totalModifications || 0} modifications`, 'INFO');
logSh(` 🎯 Patterns détectés: ${patternBreakingStats.patternsDetected || 0}`, 'INFO');
// 🆕 SAUVEGARDE ÉTAPE 5: Pattern Breaking
if (saveIntermediateSteps) {
logSh(`💾 SAUVEGARDE v1.4: Pattern Breaking`, 'INFO');
const xmlString = csvData.xmlTemplate.startsWith('<?xml') ? csvData.xmlTemplate : Buffer.from(csvData.xmlTemplate, 'base64').toString('utf8');
const patternAssembledContent = await injectGeneratedContent(xmlString, finalContent, finalElements);
const patternStorage = await saveGeneratedArticleOrganic({ generatedTexts: finalContent }, csvData, {
version: 'v1.4',
stage: 'pattern_breaking',
source: `${source}_pattern_${patternBreakingMode}`,
stageDescription: `Pattern Breaking (${patternBreakingMode}) - ${patternBreakingStats.totalModifications || 0} modifications`,
parentArticleId: parentArticleId,
useVersionedSheet: true // 🆕 Utiliser Generated_Articles_Versioned
});
versionHistory.push({
version: 'v1.4',
stage: 'pattern_breaking',
articleId: patternStorage.articleId,
length: patternStorage.textLength,
wordCount: patternStorage.wordCount,
modifications: patternBreakingStats.totalModifications || 0,
patternsDetected: patternBreakingStats.patternsDetected || 0
});
logSh(` ✅ Sauvé v1.4 - ID: ${patternStorage.articleId}`, 'DEBUG');
}
} else {
logSh(` ⚠️ Pattern Breaking fallback: contenu précédent préservé`, 'WARNING');
}
} catch (patternBreakingError) {
logSh(` ❌ Erreur Pattern Breaking: ${patternBreakingError.message}`, 'ERROR');
logSh(` 🔄 Fallback: contenu précédent préservé`, 'INFO');
}
}
// ========================================
// PHASE 9: ASSEMBLAGE ET STOCKAGE FINAL
// ========================================
logSh(`🔗 PHASE 9: Assemblage et stockage final`, 'INFO');
// D'abord récupérer le XML décodé et les finalElements
const xmlString = csvData.xmlTemplate.startsWith('<?xml') ? csvData.xmlTemplate : Buffer.from(csvData.xmlTemplate, 'base64').toString('utf8');
const assembledContent = await injectGeneratedContent(xmlString, finalContent, finalElements);
// 🆕 SAUVEGARDE VERSION FINALE
const finalSourceTag = `${source}_${selectiveStack}${adversarialMode !== 'none' ? `_${adversarialMode}` : ''}${humanSimulationMode !== 'none' ? `_${humanSimulationMode}` : ''}${patternBreakingMode !== 'none' ? `_${patternBreakingMode}` : ''}`;
const storageResult = await saveGeneratedArticleOrganic({ generatedTexts: finalContent }, csvData, {
version: saveIntermediateSteps ? 'v2.0' : '1.0',
stage: 'final_version',
source: finalSourceTag,
stageDescription: `Version finale complète avec toutes améliorations`,
parentArticleId: parentArticleId,
versionHistory: saveIntermediateSteps ? versionHistory : undefined,
useVersionedSheet: false // 🆕 Version finale → Generated_Articles (comme avant)
});
// Ajouter la version finale à l'historique
if (saveIntermediateSteps) {
versionHistory.push({
version: 'v2.0',
stage: 'final_version',
articleId: storageResult.articleId,
length: storageResult.textLength,
wordCount: storageResult.wordCount
});
logSh(`💾 SAUVEGARDE v2.0: Version finale`, 'INFO');
logSh(` 📋 Historique complet: ${versionHistory.length} versions`, 'INFO');
}
logSh(` ✅ Stocké: ${storageResult.textLength} caractères`, 'DEBUG');
// ========================================
// RÉSUMÉ FINAL
// ========================================
const totalDuration = Date.now() - startTime;
const finalStats = {
rowNumber,
selectiveStack,
adversarialMode,
humanSimulationMode,
patternBreakingMode,
totalDuration,
elementsGenerated: Object.keys(generatedContent).length,
selectiveEnhancements: selectiveResult.stats.elementsEnhanced || selectiveResult.stats.totalModifications || 0,
adversarialModifications: adversarialStats?.elementsModified || adversarialStats?.totalModifications || 0,
humanSimulationModifications: humanSimulationStats?.totalModifications || 0,
patternBreakingModifications: patternBreakingStats?.totalModifications || 0,
finalLength: storageResult.textLength,
personality: selectedPersonality.nom,
source,
versionHistory: saveIntermediateSteps ? versionHistory : undefined,
parentArticleId: parentArticleId
};
logSh(`✅ WORKFLOW MODULAIRE TERMINÉ (${totalDuration}ms)`, 'INFO');
logSh(` 📊 ${finalStats.elementsGenerated} générés | ${finalStats.selectiveEnhancements} selective | ${finalStats.adversarialModifications} adversarial | ${finalStats.humanSimulationModifications} humain | ${finalStats.patternBreakingModifications} pattern`, 'INFO');
logSh(` 🎭 Personnalité: ${finalStats.personality} | Taille finale: ${finalStats.finalLength} chars`, 'INFO');
await tracer.event('Workflow modulaire terminé', finalStats);
return {
success: true,
stats: finalStats,
content: finalContent,
assembledContent,
storageResult,
selectiveResult,
adversarialResult: adversarialStats ? { stats: adversarialStats } : null,
humanSimulationResult: humanSimulationStats ? { stats: humanSimulationStats } : null,
patternBreakingResult: patternBreakingStats ? { stats: patternBreakingStats } : null
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ WORKFLOW MODULAIRE ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
logSh(`Stack trace: ${error.stack}`, 'ERROR');
await tracer.event('Workflow modulaire échoué', {
error: error.message,
duration,
rowNumber,
selectiveStack,
adversarialMode,
humanSimulationMode,
patternBreakingMode
});
throw error;
}
}, { config });
}
/**
* BENCHMARK COMPARATIF STACKS
*/
async function benchmarkStacks(rowNumber = 2) {
console.log('\n⚡ === BENCHMARK STACKS MODULAIRES ===\n');
const stacks = getAvailableStacks();
const adversarialModes = ['none', 'light', 'standard'];
const humanSimulationModes = ['none', 'lightSimulation', 'standardSimulation'];
const patternBreakingModes = ['none', 'lightPatternBreaking', 'standardPatternBreaking'];
const results = [];
for (const stack of stacks.slice(0, 2)) { // Tester 2 stacks principaux
for (const advMode of adversarialModes.slice(0, 2)) { // 2 modes adversarial
for (const humanMode of humanSimulationModes.slice(0, 2)) { // 2 modes human simulation
for (const patternMode of patternBreakingModes.slice(0, 2)) { // 2 modes pattern breaking
console.log(`🧪 Test: ${stack.name} + adversarial ${advMode} + human ${humanMode} + pattern ${patternMode}`);
try {
const startTime = Date.now();
const result = await handleModularWorkflow({
rowNumber,
selectiveStack: stack.name,
adversarialMode: advMode,
humanSimulationMode: humanMode,
patternBreakingMode: patternMode,
source: 'benchmark'
});
const duration = Date.now() - startTime;
results.push({
stack: stack.name,
adversarial: advMode,
humanSimulation: humanMode,
patternBreaking: patternMode,
duration,
success: true,
selectiveEnhancements: result.stats.selectiveEnhancements,
adversarialModifications: result.stats.adversarialModifications,
humanSimulationModifications: result.stats.humanSimulationModifications,
patternBreakingModifications: result.stats.patternBreakingModifications,
finalLength: result.stats.finalLength
});
console.log(`${duration}ms | ${result.stats.selectiveEnhancements} selective | ${result.stats.adversarialModifications} adversarial | ${result.stats.humanSimulationModifications} humain | ${result.stats.patternBreakingModifications} pattern`);
} catch (error) {
results.push({
stack: stack.name,
adversarial: advMode,
humanSimulation: humanMode,
patternBreaking: patternMode,
success: false,
error: error.message
});
console.log(` ❌ Échoué: ${error.message}`);
}
}
}
}
}
// Résumé benchmark
console.log('\n📊 RÉSUMÉ BENCHMARK:');
const successful = results.filter(r => r.success);
if (successful.length > 0) {
const avgDuration = successful.reduce((sum, r) => sum + r.duration, 0) / successful.length;
const bestPerf = successful.reduce((best, r) => r.duration < best.duration ? r : best);
const mostEnhancements = successful.reduce((best, r) => {
const rTotal = r.selectiveEnhancements + r.adversarialModifications + (r.humanSimulationModifications || 0) + (r.patternBreakingModifications || 0);
const bestTotal = best.selectiveEnhancements + best.adversarialModifications + (best.humanSimulationModifications || 0) + (best.patternBreakingModifications || 0);
return rTotal > bestTotal ? r : best;
});
console.log(` ⚡ Durée moyenne: ${avgDuration.toFixed(0)}ms`);
console.log(` 🏆 Meilleure perf: ${bestPerf.stack} + ${bestPerf.adversarial} + ${bestPerf.humanSimulation} + ${bestPerf.patternBreaking} (${bestPerf.duration}ms)`);
console.log(` 🔥 Plus d'améliorations: ${mostEnhancements.stack} + ${mostEnhancements.adversarial} + ${mostEnhancements.humanSimulation} + ${mostEnhancements.patternBreaking} (${mostEnhancements.selectiveEnhancements + mostEnhancements.adversarialModifications + (mostEnhancements.humanSimulationModifications || 0) + (mostEnhancements.patternBreakingModifications || 0)})`);
}
return results;
}
/**
* INTERFACE LIGNE DE COMMANDE
*/
async function main() {
const args = process.argv.slice(2);
const command = args[0] || 'workflow';
try {
switch (command) {
case 'workflow':
const rowNumber = parseInt(args[1]) || 2;
const selectiveStack = args[2] || 'standardEnhancement';
const adversarialMode = args[3] || 'light';
const humanSimulationMode = args[4] || 'none';
const patternBreakingMode = args[5] || 'none';
console.log(`\n🚀 Exécution workflow modulaire:`);
console.log(` 📊 Ligne: ${rowNumber}`);
console.log(` 🔧 Stack selective: ${selectiveStack}`);
console.log(` 🎯 Mode adversarial: ${adversarialMode}`);
console.log(` 🧠 Mode human simulation: ${humanSimulationMode}`);
console.log(` 🔧 Mode pattern breaking: ${patternBreakingMode}`);
const result = await handleModularWorkflow({
rowNumber,
selectiveStack,
adversarialMode,
humanSimulationMode,
patternBreakingMode,
source: 'cli'
});
console.log('\n✅ WORKFLOW MODULAIRE RÉUSSI');
console.log(`📈 Stats: ${JSON.stringify(result.stats, null, 2)}`);
break;
case 'benchmark':
const benchRowNumber = parseInt(args[1]) || 2;
console.log(`\n⚡ Benchmark stacks (ligne ${benchRowNumber})`);
const benchResults = await benchmarkStacks(benchRowNumber);
console.log('\n📊 Résultats complets:');
console.table(benchResults);
break;
case 'stacks':
console.log('\n📦 STACKS SELECTIVE DISPONIBLES:');
const availableStacks = getAvailableStacks();
availableStacks.forEach(stack => {
console.log(`\n 🔧 ${stack.name}:`);
console.log(` 📝 ${stack.description}`);
console.log(` 📊 ${stack.layersCount} couches`);
console.log(` 🎯 Couches: ${stack.layers ? stack.layers.map(l => `${l.type}(${l.llm})`).join(' → ') : 'N/A'}`);
});
console.log('\n🎯 MODES ADVERSARIAL DISPONIBLES:');
console.log(' - none: Pas d\'adversarial');
console.log(' - light: Défense légère');
console.log(' - standard: Défense standard');
console.log(' - heavy: Défense intensive');
console.log(' - adaptive: Adaptatif intelligent');
console.log('\n🧠 MODES HUMAN SIMULATION DISPONIBLES:');
const humanStacks = getAvailableSimulationStacks();
humanStacks.forEach(stack => {
console.log(`\n 🎭 ${stack.name}:`);
console.log(` 📝 ${stack.description}`);
console.log(` 📊 ${stack.layersCount} couches`);
console.log(`${stack.expectedImpact.modificationsPerElement} modifs | ${stack.expectedImpact.detectionReduction} anti-détection`);
});
break;
case 'help':
default:
console.log('\n🔧 === MAIN MODULAIRE - USAGE ===');
console.log('\nCommandes disponibles:');
console.log(' workflow [ligne] [stack] [adversarial] [human] - Exécuter workflow complet');
console.log(' benchmark [ligne] - Benchmark stacks');
console.log(' stacks - Lister stacks disponibles');
console.log(' help - Afficher cette aide');
console.log('\nExemples:');
console.log(' node main_modulaire.js workflow 2 standardEnhancement light standardSimulation');
console.log(' node main_modulaire.js workflow 3 adaptive standard heavySimulation');
console.log(' node main_modulaire.js workflow 2 fullEnhancement none personalityFocus');
console.log(' node main_modulaire.js benchmark 2');
console.log(' node main_modulaire.js stacks');
break;
}
} catch (error) {
console.error('\n❌ ERREUR MAIN MODULAIRE:', error.message);
console.error(error.stack);
process.exit(1);
}
}
// Export pour usage programmatique (compatibilité avec l'ancien Main.js)
module.exports = {
// ✨ NOUVEAU: Interface modulaire principale
handleModularWorkflow,
benchmarkStacks,
// 🔄 COMPATIBILITÉ: Alias pour l'ancien handleFullWorkflow
handleFullWorkflow: (data) => {
// Mapper l'ancien format vers le nouveau format modulaire
const config = {
rowNumber: data.rowNumber,
source: data.source || 'compatibility_mode',
selectiveStack: 'standardEnhancement', // Configuration par défaut
adversarialMode: 'light',
humanSimulationMode: 'none',
patternBreakingMode: 'none',
saveIntermediateSteps: false
};
// Si des données CSV sont fournies directement (Make.com style)
if (data.csvData && data.xmlTemplate) {
return handleModularWorkflowWithData(data, config);
}
// Sinon utiliser le workflow normal
return handleModularWorkflow(config);
},
// 🔄 COMPATIBILITÉ: Autres exports utilisés par l'ancien système
testMainWorkflow: () => {
return handleModularWorkflow({
rowNumber: 2,
selectiveStack: 'standardEnhancement',
source: 'test_main_nodejs'
});
},
launchLogViewer: () => {
// La fonction launchLogViewer est maintenant intégrée dans handleModularWorkflow
console.log('✅ Log viewer sera lancé automatiquement avec le workflow');
}
};
// Exécution CLI si appelé directement
if (require.main === module) {
main().catch(error => {
console.error('❌ ERREUR FATALE:', error.message);
process.exit(1);
});
}
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/test-manual.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: test-manual.js - ENTRY POINT MANUEL
// Description: Test workflow ligne 2 Google Sheets
// Usage: node test-manual.js
// ========================================
require('./polyfills/fetch.cjs');
require('dotenv').config();
const { handleFullWorkflow } = require('./Main');
const { logSh } = require('./ErrorReporting');
/**
* TEST MANUEL LIGNE 2
*/
async function testWorkflowLigne2() {
logSh('🚀 === DÉMARRAGE TEST MANUEL LIGNE 2 ===', 'INFO'); // Using logSh instead of console.log
const startTime = Date.now();
try {
// DONNÉES DE TEST POUR LIGNE 2
const testData = {
rowNumber: 2, // Ligne 2 Google Sheets
source: 'test_manual_nodejs'
};
logSh('📊 Configuration test:', 'INFO'); // Using logSh instead of console.log
logSh(` • Ligne: ${testData.rowNumber}`, 'INFO'); // Using logSh instead of console.log
logSh(` • Source: ${testData.source}`, 'INFO'); // Using logSh instead of console.log
logSh(` • Timestamp: ${new Date().toISOString()}`, 'INFO'); // Using logSh instead of console.log
// LANCER LE WORKFLOW
logSh('\n🎯 Lancement workflow principal...', 'INFO'); // Using logSh instead of console.log
const result = await handleFullWorkflow(testData);
// AFFICHER RÉSULTATS
const duration = Date.now() - startTime;
logSh('\n🏆 === WORKFLOW TERMINÉ AVEC SUCCÈS ===', 'INFO'); // Using logSh instead of console.log
logSh(`⏱️ Durée: ${Math.round(duration/1000)}s`, 'INFO'); // Using logSh instead of console.log
logSh(`📊 Status: ${result.success ? '✅ SUCCESS' : '❌ ERROR'}`, 'INFO'); // Using logSh instead of console.log
if (result.success) {
logSh(`📝 Éléments générés: ${result.elementsGenerated}`, 'INFO'); // Using logSh instead of console.log
logSh(`👤 Personnalité: ${result.personality}`, 'INFO'); // Using logSh instead of console.log
logSh(`🎯 MC0: ${result.csvData?.mc0 || 'N/A'}`, 'INFO'); // Using logSh instead of console.log
logSh(`📄 XML length: ${result.stats?.xmlLength || 'N/A'} chars`, 'INFO'); // Using logSh instead of console.log
logSh(`🔤 Mots total: ${result.stats?.wordCount || 'N/A'}`, 'INFO'); // Using logSh instead of console.log
logSh(`🧠 LLMs utilisés: ${result.llmsUsed?.join(', ') || 'N/A'}`, 'INFO'); // Using logSh instead of console.log
if (result.articleStorage) {
logSh(`💾 Article sauvé: ID ${result.articleStorage.articleId}`, 'INFO'); // Using logSh instead of console.log
}
}
logSh('\n📋 Résultat complet:', 'DEBUG'); // Using logSh instead of console.log
logSh(JSON.stringify(result, null, 2), 'DEBUG'); // Using logSh instead of console.log
return result;
} catch (error) {
const duration = Date.now() - startTime;
logSh('\n❌ === ERREUR WORKFLOW ===', 'ERROR'); // Using logSh instead of console.error
logSh(`❌ Message: ${error.message}`, 'ERROR'); // Using logSh instead of console.error
logSh(`❌ Durée avant échec: ${Math.round(duration/1000)}s`, 'ERROR'); // Using logSh instead of console.error
if (process.env.NODE_ENV === 'development') {
logSh(`❌ Stack: ${error.stack}`, 'ERROR'); // Using logSh instead of console.error
}
// Afficher conseils de debug
logSh('\n🔧 CONSEILS DE DEBUG:', 'INFO'); // Using logSh instead of console.log
logSh('1. Vérifiez vos variables d\'environnement (.env)', 'INFO'); // Using logSh instead of console.log
logSh('2. Vérifiez la connexion Google Sheets', 'INFO'); // Using logSh instead of console.log
logSh('3. Vérifiez les API keys LLM', 'INFO'); // Using logSh instead of console.log
logSh('4. Regardez les logs détaillés dans ./logs/', 'INFO'); // Using logSh instead of console.log
process.exit(1);
}
}
/**
* VÉRIFICATIONS PRÉALABLES
*/
function checkEnvironment() {
logSh('🔍 Vérification environnement...', 'INFO'); // Using logSh instead of console.log
const required = [
'GOOGLE_SHEETS_ID',
'OPENAI_API_KEY'
];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
logSh('❌ Variables d\'environnement manquantes:', 'ERROR'); // Using logSh instead of console.error
missing.forEach(key => logSh(`${key}`, 'ERROR')); // Using logSh instead of console.error
logSh('\n💡 Créez un fichier .env avec ces variables', 'ERROR'); // Using logSh instead of console.error
process.exit(1);
}
logSh('✅ Variables d\'environnement OK', 'INFO'); // Using logSh instead of console.log
// Info sur les variables configurées
logSh('📋 Configuration détectée:', 'INFO'); // Using logSh instead of console.log
logSh(` • Google Sheets ID: ${process.env.GOOGLE_SHEETS_ID}`, 'INFO'); // Using logSh instead of console.log
logSh(` • OpenAI: ${process.env.OPENAI_API_KEY ? '✅ Configuré' : '❌ Manquant'}`, 'INFO'); // Using logSh instead of console.log
logSh(` • Claude: ${process.env.CLAUDE_API_KEY ? '✅ Configuré' : '⚠️ Optionnel'}`, 'INFO'); // Using logSh instead of console.log
logSh(` • Gemini: ${process.env.GEMINI_API_KEY ? '✅ Configuré' : '⚠️ Optionnel'}`, 'INFO'); // Using logSh instead of console.log
}
/**
* POINT D'ENTRÉE PRINCIPAL
*/
async function main() {
try {
// Vérifications préalables
checkEnvironment();
// Test workflow
await testWorkflowLigne2();
logSh('\n🎉 Test manuel terminé avec succès !', 'INFO'); // Using logSh instead of console.log
process.exit(0);
} catch (error) {
logSh('\n💥 Erreur fatale: ' + error.message, 'ERROR'); // Using logSh instead of console.error
process.exit(1);
}
}
// Lancer si exécuté directement
if (require.main === module) {
main();
}
module.exports = { testWorkflowLigne2 };
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/DigitalOceanWorkflow.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: DigitalOceanWorkflow.js - REFACTORISÉ POUR NODE.JS
// RESPONSABILITÉ: Orchestration + Interface Digital Ocean UNIQUEMENT
// ========================================
const crypto = require('crypto');
const axios = require('axios');
const { GoogleSpreadsheet } = require('google-spreadsheet');
const { JWT } = require('google-auth-library');
// Import des autres modules du projet (à adapter selon votre structure)
const { logSh } = require('./ErrorReporting');
const { handleFullWorkflow } = require('./Main');
const { getPersonalities, selectPersonalityWithAI } = require('./BrainConfig');
// ============= CONFIGURATION DIGITAL OCEAN =============
const DO_CONFIG = {
endpoint: 'https://autocollant.fra1.digitaloceanspaces.com',
bucketName: 'autocollant',
accessKeyId: 'DO801XTYPE968NZGAQM3',
secretAccessKey: '5aCCBiS9K+J8gsAe3M3/0GlliHCNjtLntwla1itCN1s',
region: 'fra1'
};
// Configuration Google Sheets
const SHEET_CONFIG = {
sheetId: '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c',
serviceAccountEmail: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
privateKey: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
// Alternative: utiliser fichier JSON directement
keyFile: './seo-generator-470715-85d4a971c1af.json'
};
async function deployArticle({ path, html, dryRun = false, ...rest }) {
if (!path || typeof html !== 'string') {
const err = new Error('deployArticle: invalid payload (requires {path, html})');
err.code = 'E_PAYLOAD';
throw err;
}
if (dryRun) {
return {
ok: true,
dryRun: true,
length: html.length,
path,
meta: rest || {}
};
}
// --- Impl réelle à toi ici (upload DO Spaces / API / SSH etc.) ---
// return await realDeploy({ path, html, ...rest });
// Placeholder pour ne pas casser l'appel si pas encore implémenté
return { ok: true, dryRun: false, path, length: html.length };
}
module.exports.deployArticle = module.exports.deployArticle || deployArticle;
// ============= TRIGGER PRINCIPAL REMPLACÉ PAR WEBHOOK/API =============
/**
* Point d'entrée pour déclencher le workflow
* Remplace le trigger onEdit d'Apps Script
* @param {number} rowNumber - Numéro de ligne à traiter
* @returns {Promise<object>} - Résultat du workflow
*/
async function triggerAutonomousWorkflow(rowNumber) {
try {
logSh('🚀 TRIGGER AUTONOME DÉCLENCHÉ (Digital Ocean)', 'INFO');
// Anti-bouncing simulé
await new Promise(resolve => setTimeout(resolve, 2000));
return await runAutonomousWorkflowFromTrigger(rowNumber);
} catch (error) {
logSh(`❌ Erreur trigger autonome DO: ${error.toString()}`, 'ERROR');
throw error;
}
}
/**
* ORCHESTRATEUR: Prépare les données et délègue à Main.js
*/
async function runAutonomousWorkflowFromTrigger(rowNumber) {
const startTime = Date.now();
try {
logSh(`🎬 ORCHESTRATION AUTONOME - LIGNE ${rowNumber}`, 'INFO');
// 1. LIRE DONNÉES CSV + XML FILENAME
const csvData = await readCSVDataWithXMLFileName(rowNumber);
logSh(`✅ CSV: ${csvData.mc0}, XML: ${csvData.xmlFileName}`, 'INFO');
// 2. RÉCUPÉRER XML DEPUIS DIGITAL OCEAN
const xmlTemplate = await fetchXMLFromDigitalOceanSimple(csvData.xmlFileName);
logSh(`✅ XML récupéré: ${xmlTemplate.length} caractères`, 'INFO');
// 3. 🎯 DÉLÉGUER LE WORKFLOW À MAIN.JS
const workflowData = {
rowNumber: rowNumber,
xmlTemplate: Buffer.from(xmlTemplate).toString('base64'), // Encoder comme Make.com
csvData: csvData,
source: 'digital_ocean_autonomous'
};
const result = await handleFullWorkflow(workflowData);
const duration = Date.now() - startTime;
logSh(`🏆 ORCHESTRATION TERMINÉE en ${Math.round(duration/1000)}s`, 'INFO');
// 4. MARQUER LIGNE COMME TRAITÉE
await markRowAsProcessed(rowNumber, result);
return result;
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ERREUR ORCHESTRATION: ${error.toString()}`, 'ERROR');
await markRowAsError(rowNumber, error.toString());
throw error;
}
}
// ============= INTERFACE DIGITAL OCEAN =============
async function fetchXMLFromDigitalOceanSimple(fileName) {
const filePath = `wp-content/XML/${fileName}`;
const fileUrl = `${DO_CONFIG.endpoint}/${filePath}`;
try {
const response = await axios.get(fileUrl); // Sans auth
return response.data;
} catch (error) {
throw new Error(`Fichier non accessible: ${error.message}`);
}
}
/**
* Récupérer XML depuis Digital Ocean Spaces avec authentification
*/
async function fetchXMLFromDigitalOcean(fileName) {
if (!fileName) {
throw new Error('Nom de fichier XML requis');
}
const filePath = `wp-content/XML/${fileName}`;
logSh(`🌊 Récupération XML: ${fileName} , ${filePath}`, 'DEBUG');
const fileUrl = `${DO_CONFIG.endpoint}/${filePath}`;
logSh(`🔗 URL complète: ${fileUrl}`, 'DEBUG');
const signature = generateAWSSignature(filePath);
try {
const response = await axios.get(fileUrl, {
headers: signature.headers
});
logSh(`📡 Response code: ${response.status}`, 'DEBUG');
logSh(`📄 Response: ${response.data.toString()}`, 'DEBUG');
if (response.status === 200) {
return response.data;
} else {
throw new Error(`HTTP ${response.status}: ${response.data}`);
}
} catch (error) {
logSh(`❌ Erreur DO complète: ${error.toString()}`, 'ERROR');
throw error;
}
}
/**
* Lire données CSV avec nom fichier XML (colonne J)
*/
async function readCSVDataWithXMLFileName(rowNumber) {
try {
// Configuration Google Sheets - avec fallback sur fichier JSON
let serviceAccountAuth;
if (SHEET_CONFIG.serviceAccountEmail && SHEET_CONFIG.privateKey) {
// Utiliser variables d'environnement
serviceAccountAuth = new JWT({
email: SHEET_CONFIG.serviceAccountEmail,
key: SHEET_CONFIG.privateKey,
scopes: ['https://www.googleapis.com/auth/spreadsheets']
});
} else {
// Utiliser fichier JSON
serviceAccountAuth = new JWT({
keyFile: SHEET_CONFIG.keyFile,
scopes: ['https://www.googleapis.com/auth/spreadsheets']
});
}
const doc = new GoogleSpreadsheet(SHEET_CONFIG.sheetId, serviceAccountAuth);
await doc.loadInfo();
const sheet = doc.sheetsByTitle['instructions'];
if (!sheet) {
throw new Error('Sheet "instructions" non trouvée');
}
await sheet.loadCells(`A${rowNumber}:I${rowNumber}`);
const slug = sheet.getCell(rowNumber - 1, 0).value;
const t0 = sheet.getCell(rowNumber - 1, 1).value;
const mc0 = sheet.getCell(rowNumber - 1, 2).value;
const tMinus1 = sheet.getCell(rowNumber - 1, 3).value;
const lMinus1 = sheet.getCell(rowNumber - 1, 4).value;
const mcPlus1 = sheet.getCell(rowNumber - 1, 5).value;
const tPlus1 = sheet.getCell(rowNumber - 1, 6).value;
const lPlus1 = sheet.getCell(rowNumber - 1, 7).value;
const xmlFileName = sheet.getCell(rowNumber - 1, 8).value;
if (!xmlFileName || xmlFileName.toString().trim() === '') {
throw new Error(`Nom fichier XML manquant colonne I, ligne ${rowNumber}`);
}
let cleanFileName = xmlFileName.toString().trim();
if (!cleanFileName.endsWith('.xml')) {
cleanFileName += '.xml';
}
// Récupérer personnalité (délègue au système existant BrainConfig.js)
const personalities = await getPersonalities(); // Pas de paramètre, lit depuis JSON
const selectedPersonality = await selectPersonalityWithAI(mc0, t0, personalities);
return {
rowNumber: rowNumber,
slug: slug,
t0: t0,
mc0: mc0,
tMinus1: tMinus1,
lMinus1: lMinus1,
mcPlus1: mcPlus1,
tPlus1: tPlus1,
lPlus1: lPlus1,
xmlFileName: cleanFileName,
personality: selectedPersonality
};
} catch (error) {
logSh(`❌ Erreur lecture CSV: ${error.toString()}`, 'ERROR');
throw error;
}
}
// ============= STATUTS ET VALIDATION =============
/**
* Vérifier si le workflow doit être déclenché
* En Node.js, cette logique sera adaptée selon votre stratégie (webhook, polling, etc.)
*/
function shouldTriggerWorkflow(rowNumber, xmlFileName) {
if (!rowNumber || rowNumber <= 1) {
return false;
}
if (!xmlFileName || xmlFileName.toString().trim() === '') {
logSh('⚠️ Pas de fichier XML (colonne J), workflow ignoré', 'WARNING');
return false;
}
return true;
}
async function markRowAsProcessed(rowNumber, result) {
try {
// Configuration Google Sheets - avec fallback sur fichier JSON
let serviceAccountAuth;
if (SHEET_CONFIG.serviceAccountEmail && SHEET_CONFIG.privateKey) {
serviceAccountAuth = new JWT({
email: SHEET_CONFIG.serviceAccountEmail,
key: SHEET_CONFIG.privateKey,
scopes: ['https://www.googleapis.com/auth/spreadsheets']
});
} else {
serviceAccountAuth = new JWT({
keyFile: SHEET_CONFIG.keyFile,
scopes: ['https://www.googleapis.com/auth/spreadsheets']
});
}
const doc = new GoogleSpreadsheet(SHEET_CONFIG.sheetId, serviceAccountAuth);
await doc.loadInfo();
const sheet = doc.sheetsByTitle['instructions'];
// Vérifier et ajouter headers si nécessaire
await sheet.loadCells('K1:N1');
if (!sheet.getCell(0, 10).value) {
sheet.getCell(0, 10).value = 'Status';
sheet.getCell(0, 11).value = 'Processed_At';
sheet.getCell(0, 12).value = 'Article_ID';
sheet.getCell(0, 13).value = 'Source';
await sheet.saveUpdatedCells();
}
// Marquer la ligne
await sheet.loadCells(`K${rowNumber}:N${rowNumber}`);
sheet.getCell(rowNumber - 1, 10).value = '✅ DO_SUCCESS';
sheet.getCell(rowNumber - 1, 11).value = new Date().toISOString();
sheet.getCell(rowNumber - 1, 12).value = result.articleStorage?.articleId || '';
sheet.getCell(rowNumber - 1, 13).value = 'Digital Ocean';
await sheet.saveUpdatedCells();
logSh(`✅ Ligne ${rowNumber} marquée comme traitée`, 'INFO');
} catch (error) {
logSh(`⚠️ Erreur marquage statut: ${error.toString()}`, 'WARNING');
}
}
async function markRowAsError(rowNumber, errorMessage) {
try {
// Configuration Google Sheets - avec fallback sur fichier JSON
let serviceAccountAuth;
if (SHEET_CONFIG.serviceAccountEmail && SHEET_CONFIG.privateKey) {
serviceAccountAuth = new JWT({
email: SHEET_CONFIG.serviceAccountEmail,
key: SHEET_CONFIG.privateKey,
scopes: ['https://www.googleapis.com/auth/spreadsheets']
});
} else {
serviceAccountAuth = new JWT({
keyFile: SHEET_CONFIG.keyFile,
scopes: ['https://www.googleapis.com/auth/spreadsheets']
});
}
const doc = new GoogleSpreadsheet(SHEET_CONFIG.sheetId, serviceAccountAuth);
await doc.loadInfo();
const sheet = doc.sheetsByTitle['instructions'];
await sheet.loadCells(`K${rowNumber}:N${rowNumber}`);
sheet.getCell(rowNumber - 1, 10).value = '❌ DO_ERROR';
sheet.getCell(rowNumber - 1, 11).value = new Date().toISOString();
sheet.getCell(rowNumber - 1, 12).value = errorMessage.substring(0, 100);
sheet.getCell(rowNumber - 1, 13).value = 'DO Error';
await sheet.saveUpdatedCells();
} catch (error) {
logSh(`⚠️ Erreur marquage erreur: ${error.toString()}`, 'WARNING');
}
}
// ============= SIGNATURE AWS V4 =============
function generateAWSSignature(filePath) {
const now = new Date();
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '');
const timeStamp = now.toISOString().replace(/[-:]/g, '').slice(0, -5) + 'Z';
const headers = {
'Host': DO_CONFIG.endpoint.replace('https://', ''),
'X-Amz-Date': timeStamp,
'X-Amz-Content-Sha256': 'UNSIGNED-PAYLOAD'
};
const credentialScope = `${dateStamp}/${DO_CONFIG.region}/s3/aws4_request`;
const canonicalHeaders = Object.keys(headers)
.sort()
.map(key => `${key.toLowerCase()}:${headers[key]}`)
.join('\n');
const signedHeaders = Object.keys(headers)
.map(key => key.toLowerCase())
.sort()
.join(';');
const canonicalRequest = [
'GET',
`/${filePath}`,
'',
canonicalHeaders + '\n',
signedHeaders,
'UNSIGNED-PAYLOAD'
].join('\n');
const stringToSign = [
'AWS4-HMAC-SHA256',
timeStamp,
credentialScope,
crypto.createHash('sha256').update(canonicalRequest).digest('hex')
].join('\n');
// Calculs HMAC étape par étape
const kDate = crypto.createHmac('sha256', 'AWS4' + DO_CONFIG.secretAccessKey).update(dateStamp).digest();
const kRegion = crypto.createHmac('sha256', kDate).update(DO_CONFIG.region).digest();
const kService = crypto.createHmac('sha256', kRegion).update('s3').digest();
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest();
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex');
headers['Authorization'] = `AWS4-HMAC-SHA256 Credential=${DO_CONFIG.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
return { headers: headers };
}
// ============= SETUP ET TEST =============
/**
* Configuration du trigger autonome - Remplacé par webhook ou polling en Node.js
*/
function setupAutonomousTrigger() {
logSh('⚙️ Configuration trigger autonome Digital Ocean...', 'INFO');
// En Node.js, vous pourriez utiliser:
// - Express.js avec webhooks
// - Cron jobs avec node-cron
// - Polling de la Google Sheet
// - WebSocket connections
logSh('✅ Configuration prête pour webhooks/polling Node.js', 'INFO');
logSh('🎯 Mode: Webhook/API → Digital Ocean → Main.js', 'INFO');
}
async function testDigitalOceanConnection() {
logSh('🧪 Test connexion Digital Ocean...', 'INFO');
try {
const testFiles = ['template1.xml', 'plaque-rue.xml', 'test.xml'];
for (const fileName of testFiles) {
try {
const content = await fetchXMLFromDigitalOceanSimple(fileName);
logSh(`✅ Fichier '${fileName}' accessible (${content.length} chars)`, 'INFO');
return true;
} catch (error) {
logSh(`⚠️ '${fileName}' non accessible: ${error.toString()}`, 'DEBUG');
}
}
logSh('❌ Aucun fichier test accessible dans DO', 'ERROR');
return false;
} catch (error) {
logSh(`❌ Test DO échoué: ${error.toString()}`, 'ERROR');
return false;
}
}
// ============= EXPORTS =============
module.exports = {
triggerAutonomousWorkflow,
runAutonomousWorkflowFromTrigger,
fetchXMLFromDigitalOcean,
fetchXMLFromDigitalOceanSimple,
readCSVDataWithXMLFileName,
markRowAsProcessed,
markRowAsError,
testDigitalOceanConnection,
setupAutonomousTrigger,
DO_CONFIG
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/ManualTrigger.js │
└────────────────────────────────────────────────────────────────────┘
*/
const { logSh } = require('./ErrorReporting'); // Using unified logSh from ErrorReporting
/**
* 🚀 TRIGGER MANUEL - Lit ligne 2 et lance le workflow
* Exécute cette fonction depuis l'éditeur Apps Script
*/
function runWorkflowLigne(numeroLigne = 2) {
cleanLogSheet(); // Nettoie les logs pour ce test
try {
logSh('🎬 >>> DÉMARRAGE WORKFLOW MANUEL <<<', 'INFO');
// 1. LIRE AUTOMATIQUEMENT LA LIGNE INDIQUÉ
const csvData = readCSVDataFromRow(numeroLigne);
logSh(`✅ Données lues - MC0: ${csvData.mc0}`, 'INFO');
logSh(`✅ Titre: ${csvData.t0}`, 'INFO');
logSh(`✅ Personnalité: ${csvData.personality.nom}`, 'INFO');
// 2. XML TEMPLATE SIMPLE POUR TEST (ou lit depuis Digital Ocean si configuré)
const xmlTemplate = getXMLTemplateForTest(csvData);
logSh(`✅ XML template: ${xmlTemplate.length} caractères`, 'INFO');
// 3. 🎯 LANCER LE WORKFLOW PRINCIPAL
const workflowData = {
csvData: csvData,
xmlTemplate: Utilities.base64Encode(xmlTemplate),
source: 'manuel_ligne2'
};
const result = handleFullWorkflow(workflowData);
logSh('🏆 === WORKFLOW MANUEL TERMINÉ ===', 'INFO');
// ← EXTRAIRE LES VRAIES DONNÉES
let actualData;
if (result && result.getContentText) {
// C'est un ContentService, extraire le JSON
actualData = JSON.parse(result.getContentText());
} else {
actualData = result;
}
logSh(`Type result: ${typeof result}`, 'DEBUG');
logSh(`Result keys: ${Object.keys(result || {})}`, 'DEBUG');
logSh(`ActualData keys: ${Object.keys(actualData || {})}`, 'DEBUG');
logSh(`ActualData: ${JSON.stringify(actualData)}`, 'DEBUG');
if (actualData && actualData.stats) {
logSh(`📊 Éléments générés: ${actualData.stats.contentPieces}`, 'INFO');
logSh(`📝 Nombre de mots: ${actualData.stats.wordCount}`, 'INFO');
} else {
logSh('⚠️ Format résultat inattendu', 'WARNING');
logSh('ActualData: ' + JSON.stringify(actualData, null, 2), 'DEBUG'); // Using logSh instead of console.log
}
return actualData;
} catch (error) {
logSh(`❌ ERREUR WORKFLOW MANUEL: ${error.toString()}`, 'ERROR');
logSh(`Stack: ${error.stack}`, 'ERROR');
throw error;
}
}
/**
* HELPER - Lire CSV depuis une ligne spécifique
*/
function readCSVDataFromRow(rowNumber) {
const sheetId = '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
const spreadsheet = SpreadsheetApp.openById(sheetId);
const articlesSheet = spreadsheet.getSheetByName('instructions');
// Lire la ligne complète (colonnes A à H)
const range = articlesSheet.getRange(rowNumber, 1, 1, 9);
const [slug, t0, mc0, tMinus1, lMinus1, mcPlus1, tPlus1, lPlus1, xmlFileName] = range.getValues()[0];
logSh(`📖 Lecture ligne ${rowNumber}: ${slug}`, 'DEBUG');
// Récupérer personnalités et sélectionner automatiquement
const personalitiesSheet = spreadsheet.getSheetByName('Personnalites');
const personalities = getPersonalities(personalitiesSheet);
const selectedPersonality = selectPersonalityWithAI(mc0, t0, personalities);
return {
rowNumber: rowNumber,
slug: slug || 'test-slug',
t0: t0 || 'Titre par défaut',
mc0: mc0 || 'mot-clé test',
tMinus1: tMinus1 || 'parent',
lMinus1: lMinus1 || '/parent',
mcPlus1: mcPlus1 || 'mot1,mot2,mot3,mot4',
tPlus1: tPlus1 || 'Titre1,Titre2,Titre3,Titre4',
lPlus1: lPlus1 || '/lien1,/lien2,/lien3,/lien4',
personality: selectedPersonality,
xmlFileName: xmlFileName ? xmlFileName.toString().trim() : null
};
}
/**
* HELPER - XML Template simple pour test (ou depuis Digital Ocean)
*/
function getXMLTemplateForTest(csvData) {
logSh("csvData.xmlFileName: " + csvData.xmlFileName, 'DEBUG'); // Using logSh instead of console.log
if (csvData.xmlFileName) {
logSh("Tentative Digital Ocean...", 'INFO'); // Using logSh instead of console.log
try {
return fetchXMLFromDigitalOceanSimple(csvData.xmlFileName);
} catch (error) {
// ← ENLÈVE LE CATCH SILENCIEUX
logSh("Erreur DO: " + error.toString(), 'WARNING'); // Using logSh instead of console.log
logSh(`❌ ERREUR DO DÉTAILLÉE: ${error.toString()}`, 'ERROR');
// Continue sans Digital Ocean
}
}
logSh("❌ FATAL: Aucun template XML disponible", 'ERROR');
throw new Error("FATAL: Template XML indisponible (Digital Ocean inaccessible + pas de fallback) - arrêt du workflow");
}
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/SelectiveEnhancement.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: SelectiveEnhancement.js - Node.js Version
// Description: Enhancement par batch pour éviter timeouts
// ========================================
const { callLLM } = require('./LLMManager');
const { logSh } = require('./ErrorReporting');
const { tracer } = require('./trace.js');
const { selectMultiplePersonalitiesWithAI, getPersonalities } = require('./BrainConfig');
// Utilitaire pour les délais
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* NOUVELLE APPROCHE - Multi-Personnalités Batch Enhancement
* 4 personnalités différentes utilisées dans le pipeline pour maximum d'anti-détection
*/
async function generateWithBatchEnhancement(hierarchy, csvData) {
const totalElements = Object.keys(hierarchy).length;
// NOUVEAU: Sélection de 4 personnalités complémentaires
const personalities = await tracer.run('SelectiveEnhancement.selectMultiplePersonalities()', async () => {
const allPersonalities = await getPersonalities();
const selectedPersonalities = await selectMultiplePersonalitiesWithAI(csvData.mc0, csvData.t0, allPersonalities);
await tracer.event(`4 personnalités sélectionnées: ${selectedPersonalities.map(p => p.nom).join(', ')}`);
return selectedPersonalities;
}, { mc0: csvData.mc0, t0: csvData.t0 });
await tracer.annotate({
totalElements,
personalities: personalities.map(p => `${p.nom}(${p.style})`).join(', '),
mc0: csvData.mc0
});
// ÉTAPE 1 : Génération base avec IA configurée + Personnalité 1
const baseContents = await tracer.run('SelectiveEnhancement.generateAllContentBase()', async () => {
const csvDataWithPersonality1 = { ...csvData, personality: personalities[0] };
const aiProvider1 = personalities[0].aiEtape1Base;
const result = await generateAllContentBase(hierarchy, csvDataWithPersonality1, aiProvider1);
await tracer.event(`${Object.keys(result).length} éléments générés avec ${personalities[0].nom} via ${aiProvider1.toUpperCase()}`);
return result;
}, { hierarchyElements: Object.keys(hierarchy).length, personality1: personalities[0].nom, llmProvider: personalities[0].aiEtape1Base, mc0: csvData.mc0 });
// ÉTAPE 2 : Enhancement technique avec IA configurée + Personnalité 2
const technicalEnhanced = await tracer.run('SelectiveEnhancement.enhanceAllTechnicalTerms()', async () => {
const csvDataWithPersonality2 = { ...csvData, personality: personalities[1] };
const aiProvider2 = personalities[1].aiEtape2Technique;
const result = await enhanceAllTechnicalTerms(baseContents, csvDataWithPersonality2, aiProvider2);
const enhancedCount = Object.keys(result).filter(k => result[k] !== baseContents[k]).length;
await tracer.event(`${enhancedCount}/${Object.keys(result).length} éléments techniques améliorés avec ${personalities[1].nom} via ${aiProvider2.toUpperCase()}`);
return result;
}, { baseElements: Object.keys(baseContents).length, personality2: personalities[1].nom, llmProvider: personalities[1].aiEtape2Technique, mc0: csvData.mc0 });
// ÉTAPE 3 : Enhancement transitions avec IA configurée + Personnalité 3
const transitionsEnhanced = await tracer.run('SelectiveEnhancement.enhanceAllTransitions()', async () => {
const csvDataWithPersonality3 = { ...csvData, personality: personalities[2] };
const aiProvider3 = personalities[2].aiEtape3Transitions;
const result = await enhanceAllTransitions(technicalEnhanced, csvDataWithPersonality3, aiProvider3);
const enhancedCount = Object.keys(result).filter(k => result[k] !== technicalEnhanced[k]).length;
await tracer.event(`${enhancedCount}/${Object.keys(result).length} transitions fluidifiées avec ${personalities[2].nom} via ${aiProvider3.toUpperCase()}`);
return result;
}, { technicalElements: Object.keys(technicalEnhanced).length, personality3: personalities[2].nom, llmProvider: personalities[2].aiEtape3Transitions });
// ÉTAPE 4 : Enhancement style avec IA configurée + Personnalité 4
const finalContents = await tracer.run('SelectiveEnhancement.enhanceAllPersonalityStyle()', async () => {
const csvDataWithPersonality4 = { ...csvData, personality: personalities[3] };
const aiProvider4 = personalities[3].aiEtape4Style;
const result = await enhanceAllPersonalityStyle(transitionsEnhanced, csvDataWithPersonality4, aiProvider4);
const enhancedCount = Object.keys(result).filter(k => result[k] !== transitionsEnhanced[k]).length;
const avgWords = Math.round(Object.values(result).reduce((acc, content) => acc + content.split(' ').length, 0) / Object.keys(result).length);
await tracer.event(`${enhancedCount}/${Object.keys(result).length} éléments stylisés avec ${personalities[3].nom} via ${aiProvider4.toUpperCase()}`, { avgWordsPerElement: avgWords });
return result;
}, { transitionElements: Object.keys(transitionsEnhanced).length, personality4: personalities[3].nom, llmProvider: personalities[3].aiEtape4Style });
// Log final du DNA Mixing réussi avec IA configurables
const aiChain = personalities.map((p, i) => `${p.aiEtape1Base || p.aiEtape2Technique || p.aiEtape3Transitions || p.aiEtape4Style}`.toUpperCase()).join(' → ');
logSh(`✅ DNA MIXING MULTI-PERSONNALITÉS TERMINÉ:`, 'INFO');
logSh(` 🎭 4 personnalités utilisées: ${personalities.map(p => p.nom).join(' → ')}`, 'INFO');
logSh(` 🤖 IA configurées: ${personalities[0].aiEtape1Base.toUpperCase()}${personalities[1].aiEtape2Technique.toUpperCase()}${personalities[2].aiEtape3Transitions.toUpperCase()}${personalities[3].aiEtape4Style.toUpperCase()}`, 'INFO');
logSh(` 📝 ${Object.keys(finalContents).length} éléments avec style hybride généré`, 'INFO');
return finalContents;
}
/**
* ÉTAPE 1 - Génération base TOUS éléments avec IA configurable
*/
async function generateAllContentBase(hierarchy, csvData, aiProvider) {
logSh('🔍 === DEBUG GÉNÉRATION BASE ===', 'DEBUG');
// Debug: logger la hiérarchie complète
logSh(`🔍 Hiérarchie reçue: ${Object.keys(hierarchy).length} sections`, 'DEBUG');
Object.keys(hierarchy).forEach((path, i) => {
const section = hierarchy[path];
logSh(`🔍 Section ${i+1} [${path}]:`, 'DEBUG');
logSh(`🔍 - title: ${section.title ? section.title.originalElement?.originalTag : 'AUCUN'}`, 'DEBUG');
logSh(`🔍 - text: ${section.text ? section.text.originalElement?.originalTag : 'AUCUN'}`, 'DEBUG');
logSh(`🔍 - questions: ${section.questions?.length || 0}`, 'DEBUG');
});
const allElements = collectAllElements(hierarchy);
logSh(`🔍 Éléments collectés: ${allElements.length}`, 'DEBUG');
// Debug: logger tous les éléments collectés
allElements.forEach((element, i) => {
logSh(`🔍 Élément ${i+1}: tag="${element.tag}", type="${element.type}"`, 'DEBUG');
});
// NOUVELLE LOGIQUE : SÉPARER PAIRES FAQ ET AUTRES ÉLÉMENTS
const results = {};
logSh(`🔍 === GÉNÉRATION INTELLIGENTE DE ${allElements.length} ÉLÉMENTS ===`, 'DEBUG');
logSh(`🔍 Ordre respecté: ${allElements.map(el => el.tag.replace(/\|/g, '')).join(' → ')}`, 'DEBUG');
// 1. IDENTIFIER les paires FAQ
const { faqPairs, otherElements } = separateFAQPairsAndOthers(allElements);
logSh(`🔍 ${faqPairs.length} paires FAQ trouvées, ${otherElements.length} autres éléments`, 'INFO');
// 2. GÉNÉRER les autres éléments EN BATCH ORDONNÉ (titres d'abord, puis textes avec contexte)
const groupedElements = groupElementsByType(otherElements);
// ORDRE DE GÉNÉRATION : TITRES → TEXTES → INTRO → AUTRES
const orderedTypes = ['titre', 'texte', 'intro'];
for (const type of orderedTypes) {
const elements = groupedElements[type];
if (!elements || elements.length === 0) continue;
// DÉCOUPER EN CHUNKS DE MAX 4 ÉLÉMENTS POUR ÉVITER TIMEOUTS
const chunks = chunkArray(elements, 4);
logSh(`🚀 BATCH ${type.toUpperCase()}: ${elements.length} éléments en ${chunks.length} chunks`, 'INFO');
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
logSh(` Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
try {
// Passer les résultats déjà générés pour contexte (titres → textes)
const batchPrompt = createBatchBasePrompt(chunk, type, csvData, results);
const batchResponse = await callLLM(aiProvider, batchPrompt, {
temperature: 0.7,
maxTokens: 2000 * chunk.length
}, csvData.personality);
const batchResults = parseBatchResponse(batchResponse, chunk);
Object.assign(results, batchResults);
logSh(`✅ Chunk ${chunkIndex + 1}: ${Object.keys(batchResults).length}/${chunk.length} éléments générés`, 'INFO');
} catch (error) {
logSh(`❌ FATAL: Chunk ${chunkIndex + 1} de ${type} échoué: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Génération chunk ${chunkIndex + 1} de ${type} échouée - arrêt du workflow: ${error.message}`);
}
// Délai entre chunks pour éviter rate limiting
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
}
logSh(`✅ BATCH ${type.toUpperCase()} COMPLET: ${elements.length} éléments générés en ${chunks.length} chunks`, 'INFO');
}
// TRAITER les types restants (autres que titre/texte/intro)
for (const [type, elements] of Object.entries(groupedElements)) {
if (orderedTypes.includes(type) || elements.length === 0) continue;
// DÉCOUPER EN CHUNKS DE MAX 4 ÉLÉMENTS POUR ÉVITER TIMEOUTS
const chunks = chunkArray(elements, 4);
logSh(`🚀 BATCH ${type.toUpperCase()}: ${elements.length} éléments en ${chunks.length} chunks`, 'INFO');
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
logSh(` Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
try {
const batchPrompt = createBatchBasePrompt(chunk, type, csvData, results);
const batchResponse = await callLLM(aiProvider, batchPrompt, {
temperature: 0.7,
maxTokens: 2000 * chunk.length
}, csvData.personality);
const batchResults = parseBatchResponse(batchResponse, chunk);
Object.assign(results, batchResults);
logSh(`✅ Chunk ${chunkIndex + 1}: ${Object.keys(batchResults).length}/${chunk.length} éléments générés`, 'INFO');
} catch (error) {
logSh(`❌ FATAL: Chunk ${chunkIndex + 1} de ${type} échoué: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Génération chunk ${chunkIndex + 1} de ${type} échouée - arrêt du workflow: ${error.message}`);
}
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
}
logSh(`✅ BATCH ${type.toUpperCase()} COMPLET: ${elements.length} éléments générés en ${chunks.length} chunks`, 'INFO');
}
// 3. GÉNÉRER les paires FAQ ensemble (RESTAURÉ depuis .gs)
if (faqPairs.length > 0) {
logSh(`🔍 === GÉNÉRATION PAIRES FAQ (${faqPairs.length} paires) ===`, 'INFO');
const faqResults = await generateFAQPairsRestored(faqPairs, csvData, aiProvider);
Object.assign(results, faqResults);
}
logSh(`🔍 === RÉSULTATS FINAUX GÉNÉRATION BASE ===`, 'DEBUG');
logSh(`🔍 Total généré: ${Object.keys(results).length} éléments`, 'DEBUG');
Object.keys(results).forEach(tag => {
logSh(`🔍 [${tag}]: "${results[tag]}"`, 'DEBUG');
});
return results;
}
/**
* ÉTAPE 2 - Enhancement technique ÉLÉMENT PAR ÉLÉMENT avec IA configurable
* NOUVEAU : Traitement individuel pour fiabilité maximale et debug précis
*/
async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) {
logSh('🔧 === DÉBUT ENHANCEMENT TECHNIQUE ===', 'INFO');
logSh('Enhancement technique BATCH TOTAL...', 'DEBUG');
const allElements = Object.keys(baseContents);
if (allElements.length === 0) {
logSh('⚠️ Aucun élément à analyser techniquement', 'WARNING');
return baseContents;
}
const analysisStart = Date.now();
logSh(`📊 Analyse démarrée: ${allElements.length} éléments à examiner`, 'INFO');
try {
// ÉTAPE 1 : Extraction batch TOUS les termes techniques (1 seul appel)
logSh(`🔍 Analyse technique batch: ${allElements.length} éléments`, 'INFO');
const technicalAnalysis = await extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider);
const analysisEnd = Date.now();
// ÉTAPE 2 : Enhancement batch TOUS les éléments qui en ont besoin (1 seul appel)
const elementsNeedingEnhancement = technicalAnalysis.filter(item => item.needsEnhancement);
logSh(`📋 Analyse terminée (${analysisEnd - analysisStart}ms):`, 'INFO');
logSh(`${elementsNeedingEnhancement.length}/${allElements.length} éléments nécessitent enhancement`, 'INFO');
if (elementsNeedingEnhancement.length === 0) {
logSh('✅ Aucun élément ne nécessite enhancement technique - contenu déjà optimal', 'INFO');
return baseContents;
}
// Log détaillé des éléments à améliorer
elementsNeedingEnhancement.forEach((item, i) => {
logSh(` ${i+1}. [${item.tag}]: ${item.technicalTerms.join(', ')}`, 'DEBUG');
});
const enhancementStart = Date.now();
logSh(`🔧 Enhancement technique: ${elementsNeedingEnhancement.length}/${allElements.length} éléments`, 'INFO');
const enhancedContents = await enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider);
const enhancementEnd = Date.now();
// ÉTAPE 3 : Merger résultats
const results = { ...baseContents };
let actuallyEnhanced = 0;
Object.keys(enhancedContents).forEach(tag => {
if (enhancedContents[tag] !== baseContents[tag]) {
results[tag] = enhancedContents[tag];
actuallyEnhanced++;
}
});
logSh(`⚡ Enhancement terminé (${enhancementEnd - enhancementStart}ms):`, 'INFO');
logSh(`${actuallyEnhanced} éléments réellement améliorés`, 'INFO');
logSh(` • Termes intégrés: dibond, impression UV, fraisage, etc.`, 'DEBUG');
logSh(`✅ Enhancement technique terminé avec succès`, 'INFO');
return results;
} catch (error) {
const analysisTotal = Date.now() - analysisStart;
logSh(`❌ FATAL: Enhancement technique échoué après ${analysisTotal}ms`, 'ERROR');
logSh(`❌ Message: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Enhancement technique impossible - arrêt du workflow: ${error.message}`);
}
}
/**
* Analyser un seul élément pour détecter les termes techniques
*/
async function analyzeSingleElementTechnicalTerms(tag, content, csvData, aiProvider) {
const prompt = `MISSION: Analyser ce contenu et déterminer s'il contient des termes techniques.
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTENU À ANALYSER:
TAG: ${tag}
CONTENU: "${content}"
CONSIGNES:
- Cherche UNIQUEMENT des vrais termes techniques métier/industrie
- Évite mots génériques (qualité, service, pratique, personnalisé, etc.)
- Focus: matériaux, procédés, normes, dimensions, technologies spécifiques
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé, anodisation
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique, haute performance
RÉPONSE REQUISE:
- Si termes techniques trouvés: "OUI - termes: [liste des termes séparés par virgules]"
- Si aucun terme technique: "NON"
EXEMPLE:
OUI - termes: aluminium composite, impression numérique, gravure laser`;
try {
const response = await callLLM(aiProvider, prompt, { temperature: 0.3 });
if (response.toUpperCase().startsWith('OUI')) {
// Extraire les termes de la réponse
const termsMatch = response.match(/termes:\s*(.+)/i);
const terms = termsMatch ? termsMatch[1].trim() : '';
logSh(`✅ [${tag}] Termes techniques détectés: ${terms}`, 'DEBUG');
return true;
} else {
logSh(`⏭️ [${tag}] Pas de termes techniques`, 'DEBUG');
return false;
}
} catch (error) {
logSh(`❌ ERREUR analyse ${tag}: ${error.message}`, 'ERROR');
return false; // En cas d'erreur, on skip l'enhancement
}
}
/**
* Enhancer un seul élément techniquement
*/
async function enhanceSingleElementTechnical(tag, content, csvData, aiProvider) {
const prompt = `MISSION: Améliore ce contenu en intégrant des termes techniques précis.
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTENU À AMÉLIORER:
TAG: ${tag}
CONTENU: "${content}"
OBJECTIFS:
- Remplace les termes génériques par des termes techniques précis
- Ajoute des spécifications techniques réalistes
- Maintient le même style et longueur
- Intègre naturellement: matériaux (dibond, aluminium composite), procédés (impression UV, gravure laser), dimensions, normes
EXEMPLE DE TRANSFORMATION:
"matériaux haute performance" → "dibond 3mm ou aluminium composite"
"impression moderne" → "impression UV haute définition"
"fixation solide" → "fixation par chevilles inox Ø6mm"
CONTRAINTES:
- GARDE la même structure
- MÊME longueur approximative
- Style cohérent avec l'original
- RÉPONDS DIRECTEMENT par le contenu amélioré, sans préfixe`;
try {
const enhancedContent = await callLLM(aiProvider, prompt, { temperature: 0.7 });
return enhancedContent.trim();
} catch (error) {
logSh(`❌ ERREUR enhancement ${tag}: ${error.message}`, 'ERROR');
return content; // En cas d'erreur, on retourne le contenu original
}
}
// ANCIENNES FONCTIONS BATCH SUPPRIMÉES - REMPLACÉES PAR TRAITEMENT INDIVIDUEL
/**
* NOUVELLE FONCTION : Enhancement batch TOUS les éléments
*/
// FONCTION SUPPRIMÉE : enhanceAllElementsTechnicalBatch() - Remplacée par traitement individuel
/**
* ÉTAPE 3 - Enhancement transitions BATCH avec IA configurable
*/
async function enhanceAllTransitions(baseContents, csvData, aiProvider) {
logSh('🔗 === DÉBUT ENHANCEMENT TRANSITIONS ===', 'INFO');
logSh('Enhancement transitions batch...', 'DEBUG');
const transitionStart = Date.now();
const allElements = Object.keys(baseContents);
logSh(`📊 Analyse transitions: ${allElements.length} éléments à examiner`, 'INFO');
// Sélectionner éléments longs qui bénéficient d'amélioration transitions
const transitionElements = [];
let analyzedCount = 0;
Object.keys(baseContents).forEach(tag => {
const content = baseContents[tag];
analyzedCount++;
if (content.length > 150) {
const needsTransitions = analyzeTransitionNeed(content);
logSh(` [${tag}]: ${content.length}c, transitions=${needsTransitions ? '✅' : '❌'}`, 'DEBUG');
if (needsTransitions) {
transitionElements.push({
tag: tag,
content: content
});
}
} else {
logSh(` [${tag}]: ${content.length}c - trop court, ignoré`, 'DEBUG');
}
});
logSh(`📋 Analyse transitions terminée:`, 'INFO');
logSh(`${analyzedCount} éléments analysés`, 'INFO');
logSh(`${transitionElements.length} nécessitent amélioration`, 'INFO');
if (transitionElements.length === 0) {
logSh('✅ Pas d\'éléments nécessitant enhancement transitions - fluidité déjà optimale', 'INFO');
return baseContents;
}
logSh(`${transitionElements.length} éléments à améliorer (transitions)`, 'INFO');
const chunks = chunkArray(transitionElements, 6); // Plus petit pour Gemini
const results = { ...baseContents };
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(`Chunk transitions ${chunkIndex + 1}/${chunks.length} (${chunk.length} éléments)`, 'DEBUG');
const batchTransitionsPrompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.
CONTEXTE: Article SEO professionnel pour site web commercial
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} adapté web)
CONNECTEURS PRÉFÉRÉS: ${csvData.personality?.connecteursPref}
CONTENUS:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
"${item.content}"`).join('\n\n')}
OBJECTIFS:
- Connecteurs plus naturels et variés issus de: ${csvData.personality?.connecteursPref}
- Transitions fluides entre idées
- ÉVITE répétitions excessives ("franchement", "du coup", "vraiment", "par ailleurs")
- Style cohérent ${csvData.personality?.style}
CONTRAINTES STRICTES:
- NE CHANGE PAS le fond du message
- GARDE la même structure et longueur approximative
- Améliore SEULEMENT la fluidité des transitions
- RESPECTE le style ${csvData.personality?.nom}
- RÉPONDS DIRECTEMENT PAR LE CONTENU AMÉLIORÉ, sans préfixe ni tag XML
FORMAT DE RÉPONSE:
[1] Contenu avec transitions améliorées selon ${csvData.personality?.nom}
[2] Contenu avec transitions améliorées selon ${csvData.personality?.nom}
etc...`;
const improved = await callLLM(aiProvider, batchTransitionsPrompt, {
temperature: 0.6,
maxTokens: 2500
}, csvData.personality);
const parsedImprovements = parseTransitionsBatchResponse(improved, chunk);
Object.keys(parsedImprovements).forEach(tag => {
results[tag] = parsedImprovements[tag];
});
} catch (error) {
logSh(`❌ Erreur chunk transitions ${chunkIndex + 1}: ${error.message}`, 'ERROR');
}
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
}
return results;
}
/**
* ÉTAPE 4 - Enhancement style personnalité BATCH avec IA configurable
*/
async function enhanceAllPersonalityStyle(baseContents, csvData, aiProvider) {
const personality = csvData.personality;
if (!personality) {
logSh('Pas de personnalité, skip enhancement style', 'DEBUG');
return baseContents;
}
logSh(`Enhancement style ${personality.nom} batch...`, 'DEBUG');
// Tous les éléments bénéficient de l'adaptation personnalité
const styleElements = Object.keys(baseContents).map(tag => ({
tag: tag,
content: baseContents[tag]
}));
const chunks = chunkArray(styleElements, 8);
const results = { ...baseContents };
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(`Chunk style ${chunkIndex + 1}/${chunks.length} (${chunk.length} éléments)`, 'DEBUG');
const batchStylePrompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}.
CONTEXTE: Finalisation article SEO pour site e-commerce professionnel
PERSONNALITÉ: ${personality.nom}
DESCRIPTION: ${personality.description}
STYLE CIBLE: ${personality.style} adapté au web professionnel
VOCABULAIRE: ${personality.vocabulairePref}
CONNECTEURS: ${personality.connecteursPref}
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
LONGUEUR PHRASES: ${personality.longueurPhrases}
CONTENUS À STYLISER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
"${item.content}"`).join('\n\n')}
CONSIGNES STRICTES:
- GARDE le même contenu informatif et technique
- Adapte SEULEMENT le ton, les expressions et le vocabulaire selon ${personality.nom}
- RESPECTE la longueur approximative (même nombre de mots ±20%)
- ÉVITE les répétitions excessives ("franchement", "du coup", "vraiment")
- VARIE les expressions et connecteurs selon: ${personality.connecteursPref}
- Style ${personality.nom} reconnaissable mais NATUREL
- RÉPONDS DIRECTEMENT PAR LE CONTENU STYLISÉ, sans préfixe ni tag XML
- PAS de messages d'excuse ou d'incapacité
FORMAT DE RÉPONSE:
[1] Contenu stylisé selon ${personality.nom} (${personality.style})
[2] Contenu stylisé selon ${personality.nom} (${personality.style})
etc...`;
const styled = await callLLM(aiProvider, batchStylePrompt, {
temperature: 0.8,
maxTokens: 3000
}, personality);
const parsedStyles = parseStyleBatchResponse(styled, chunk);
Object.keys(parsedStyles).forEach(tag => {
results[tag] = parsedStyles[tag];
});
} catch (error) {
logSh(`❌ Erreur chunk style ${chunkIndex + 1}: ${error.message}`, 'ERROR');
}
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
}
return results;
}
// ============= HELPER FUNCTIONS =============
/**
* Sleep function replacement for Utilities.sleep
*/
// FONCTION SUPPRIMÉE : sleep() dupliquée - déjà définie ligne 12
/**
* RESTAURÉ DEPUIS .GS : Génération des paires FAQ cohérentes
*/
async function generateFAQPairsRestored(faqPairs, csvData, aiProvider) {
logSh(`🔍 === GÉNÉRATION PAIRES FAQ (logique .gs restaurée) ===`, 'INFO');
if (faqPairs.length === 0) return {};
const batchPrompt = createBatchFAQPairsPrompt(faqPairs, csvData);
logSh(`🔍 Prompt FAQ paires (${batchPrompt.length} chars): "${batchPrompt.substring(0, 300)}..."`, 'DEBUG');
try {
const batchResponse = await callLLM(aiProvider, batchPrompt, {
temperature: 0.8,
maxTokens: 3000 // Plus large pour les paires
}, csvData.personality);
logSh(`🔍 Réponse FAQ paires reçue: ${batchResponse.length} caractères`, 'DEBUG');
logSh(`🔍 Début réponse: "${batchResponse.substring(0, 200)}..."`, 'DEBUG');
return parseFAQPairsResponse(batchResponse, faqPairs);
} catch (error) {
logSh(`❌ FATAL: Erreur génération paires FAQ: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Génération paires FAQ échouée - arrêt du workflow: ${error.message}`);
}
}
/**
* RESTAURÉ DEPUIS .GS : Prompt pour paires FAQ cohérentes
*/
function createBatchFAQPairsPrompt(faqPairs, csvData) {
const personality = csvData.personality;
let prompt = `=== 1. CONTEXTE ===
Entreprise: Autocollant.fr - signalétique personnalisée
Sujet: ${csvData.mc0}
Section: FAQ pour article SEO commercial
=== 2. PERSONNALITÉ ===
Rédacteur: ${personality.nom}
Style: ${personality.style}
Ton: ${personality.description || 'professionnel'}
=== 3. RÈGLES GÉNÉRALES ===
- Questions naturelles de clients
- Réponses expertes et rassurantes
- Langage professionnel mais accessible
- Textes rédigés humainement et de façon authentique
- Couvrir: prix, livraison, personnalisation, installation, durabilité
- IMPÉRATIF: Respecter strictement les contraintes XML
=== 4. PAIRES FAQ À GÉNÉRER ===
`;
faqPairs.forEach((pair, index) => {
const questionTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
const answerTag = pair.answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
prompt += `${index + 1}. [${questionTag}] + [${answerTag}] - Paire FAQ naturelle
`;
});
prompt += `
FORMAT DE RÉPONSE:
PAIRE 1:
[${faqPairs[0].question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}]
Question client directe et naturelle sur ${csvData.mc0} ?
[${faqPairs[0].answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}]
Réponse utile et rassurante selon le style ${personality.style} de ${personality.nom}.
`;
if (faqPairs.length > 1) {
prompt += `PAIRE 2:
etc...
`;
}
return prompt;
}
/**
* RESTAURÉ DEPUIS .GS : Parser réponse paires FAQ
*/
function parseFAQPairsResponse(response, faqPairs) {
const results = {};
logSh(`🔍 Parsing FAQ paires: "${response.substring(0, 300)}..."`, 'DEBUG');
// Parser avec regex [TAG] contenu
const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
let content = match[2].trim().replace(/\n\s*\n/g, '\n').replace(/^\n+|\n+$/g, '');
// NOUVEAU: Appliquer le nettoyage XML pour FAQ aussi
content = cleanXMLTagsFromContent(content);
if (content && content.length > 0) {
parsedItems[tag] = content;
logSh(`🔍 Parsé [${tag}]: "${content.substring(0, 100)}..."`, 'DEBUG');
}
}
// Mapper aux vrais tags FAQ avec |
let pairesCompletes = 0;
faqPairs.forEach(pair => {
const questionCleanTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
const answerCleanTag = pair.answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
const questionContent = parsedItems[questionCleanTag];
const answerContent = parsedItems[answerCleanTag];
if (questionContent && answerContent) {
results[pair.question.tag] = questionContent;
results[pair.answer.tag] = answerContent;
pairesCompletes++;
logSh(`✅ Paire FAQ ${pair.number} complète: Q="${questionContent}" R="${answerContent.substring(0, 50)}..."`, 'INFO');
} else {
logSh(`⚠️ Paire FAQ ${pair.number} incomplète: Q=${!!questionContent} R=${!!answerContent}`, 'WARNING');
if (questionContent) results[pair.question.tag] = questionContent;
if (answerContent) results[pair.answer.tag] = answerContent;
}
});
logSh(`📊 FAQ parsing: ${pairesCompletes}/${faqPairs.length} paires complètes`, 'INFO');
// FATAL si aucune paire complète (comme dans le .gs)
if (pairesCompletes === 0 && faqPairs.length > 0) {
logSh(`❌ FATAL: Aucune paire FAQ générée correctement`, 'ERROR');
throw new Error(`FATAL: Génération FAQ incomplète (0/${faqPairs.length} paires complètes) - arrêt du workflow`);
}
return results;
}
/**
* RESTAURÉ DEPUIS .GS : Nettoyer instructions FAQ
*/
function cleanFAQInstructions(instructions, csvData) {
if (!instructions) return '';
let cleanInstructions = instructions;
// Remplacer variables
cleanInstructions = cleanInstructions.replace(/\{\{T0\}\}/g, csvData.t0 || '');
cleanInstructions = cleanInstructions.replace(/\{\{MC0\}\}/g, csvData.mc0 || '');
cleanInstructions = cleanInstructions.replace(/\{\{T-1\}\}/g, csvData.tMinus1 || '');
cleanInstructions = cleanInstructions.replace(/\{\{L-1\}\}/g, csvData.lMinus1 || '');
// Variables multiples MC+1_X, T+1_X, L+1_X
if (csvData.mcPlus1) {
const mcPlus1 = csvData.mcPlus1.split(',').map(s => s.trim());
for (let i = 1; i <= 6; i++) {
const mcValue = mcPlus1[i-1] || `[MC+1_${i} non défini]`;
cleanInstructions = cleanInstructions.replace(new RegExp(`\\{\\{MC\\+1_${i}\\}\\}`, 'g'), mcValue);
}
}
if (csvData.tPlus1) {
const tPlus1 = csvData.tPlus1.split(',').map(s => s.trim());
for (let i = 1; i <= 6; i++) {
const tValue = tPlus1[i-1] || `[T+1_${i} non défini]`;
cleanInstructions = cleanInstructions.replace(new RegExp(`\\{\\{T\\+1_${i}\\}\\}`, 'g'), tValue);
}
}
// Nettoyer HTML
cleanInstructions = cleanInstructions.replace(/<\/?[^>]+>/g, '');
cleanInstructions = cleanInstructions.replace(/\s+/g, ' ').trim();
return cleanInstructions;
}
/**
* Collecter tous les éléments dans l'ordre XML original
* CORRECTION: Suit l'ordre séquentiel XML au lieu de grouper par section
*/
function collectAllElements(hierarchy) {
const allElements = [];
const tagToElementMap = {};
// 1. Créer un mapping de tous les éléments disponibles
Object.keys(hierarchy).forEach(path => {
const section = hierarchy[path];
if (section.title) {
tagToElementMap[section.title.originalElement.originalTag] = {
tag: section.title.originalElement.originalTag,
element: section.title.originalElement,
type: 'titre'
};
}
if (section.text) {
tagToElementMap[section.text.originalElement.originalTag] = {
tag: section.text.originalElement.originalTag,
element: section.text.originalElement,
type: 'texte'
};
}
section.questions.forEach(q => {
tagToElementMap[q.originalElement.originalTag] = {
tag: q.originalElement.originalTag,
element: q.originalElement,
type: q.originalElement.type
};
});
});
// 2. Récupérer l'ordre XML original depuis le template global
logSh(`🔍 Global XML Template disponible: ${!!global.currentXmlTemplate}`, 'DEBUG');
if (global.currentXmlTemplate && global.currentXmlTemplate.length > 0) {
logSh(`🔍 Template XML: ${global.currentXmlTemplate.substring(0, 200)}...`, 'DEBUG');
const regex = /\|([^|]+)\|/g;
let match;
// Parcourir le XML dans l'ordre d'apparition
while ((match = regex.exec(global.currentXmlTemplate)) !== null) {
const fullMatch = match[1];
// Extraire le nom du tag (sans variables)
const nameMatch = fullMatch.match(/^([^{]+)/);
const tagName = nameMatch ? nameMatch[1].trim() : fullMatch.split('{')[0];
const pureTag = `|${tagName}|`;
// Si cet élément existe dans notre mapping, l'ajouter dans l'ordre
if (tagToElementMap[pureTag]) {
allElements.push(tagToElementMap[pureTag]);
logSh(`🔍 Ajouté dans l'ordre: ${pureTag}`, 'DEBUG');
delete tagToElementMap[pureTag]; // Éviter les doublons
} else {
logSh(`🔍 Tag XML non trouvé dans mapping: ${pureTag}`, 'DEBUG');
}
}
}
// 3. Ajouter les éléments restants (sécurité)
const remainingElements = Object.values(tagToElementMap);
if (remainingElements.length > 0) {
logSh(`🔍 Éléments restants ajoutés: ${remainingElements.map(el => el.tag).join(', ')}`, 'DEBUG');
remainingElements.forEach(element => {
allElements.push(element);
});
}
logSh(`🔍 ORDRE FINAL: ${allElements.map(el => el.tag.replace(/\|/g, '')).join(' → ')}`, 'INFO');
return allElements;
}
/**
* RESTAURÉ DEPUIS .GS : Séparer les paires FAQ des autres éléments
*/
function separateFAQPairsAndOthers(allElements) {
const faqPairs = [];
const otherElements = [];
const faqQuestions = {};
const faqAnswers = {};
// 1. Collecter toutes les questions et réponses FAQ
allElements.forEach(element => {
if (element.type === 'faq_question') {
// Extraire le numéro : |Faq_q_1| → 1
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqQuestions[faqNumber] = element;
logSh(`🔍 Question FAQ ${faqNumber} trouvée: ${element.tag}`, 'DEBUG');
} else if (element.type === 'faq_reponse') {
// Extraire le numéro : |Faq_a_1| → 1
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqAnswers[faqNumber] = element;
logSh(`🔍 Réponse FAQ ${faqNumber} trouvée: ${element.tag}`, 'DEBUG');
} else {
// Élément normal (titre, texte, intro, etc.)
otherElements.push(element);
}
});
// 2. Créer les paires FAQ cohérentes
Object.keys(faqQuestions).forEach(number => {
const question = faqQuestions[number];
const answer = faqAnswers[number];
if (question && answer) {
faqPairs.push({
number: number,
question: question,
answer: answer
});
logSh(`✅ Paire FAQ ${number} créée: ${question.tag} + ${answer.tag}`, 'INFO');
} else if (question) {
logSh(`⚠️ Question FAQ ${number} sans réponse correspondante`, 'WARNING');
otherElements.push(question); // Traiter comme élément individuel
} else if (answer) {
logSh(`⚠️ Réponse FAQ ${number} sans question correspondante`, 'WARNING');
otherElements.push(answer); // Traiter comme élément individuel
}
});
logSh(`🔍 Séparation terminée: ${faqPairs.length} paires FAQ, ${otherElements.length} autres éléments`, 'INFO');
return { faqPairs, otherElements };
}
/**
* Grouper éléments par type
*/
function groupElementsByType(elements) {
const groups = {};
elements.forEach(element => {
const type = element.type;
if (!groups[type]) {
groups[type] = [];
}
groups[type].push(element);
});
return groups;
}
/**
* Diviser array en chunks
*/
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
/**
* Trouver le titre associé à un élément texte
*/
function findAssociatedTitle(textElement, existingResults) {
const textName = textElement.element.name || textElement.tag;
// STRATÉGIE 1: Correspondance directe (Txt_H2_1 → Titre_H2_1)
const directMatch = textName.replace(/Txt_/, 'Titre_').replace(/Text_/, 'Titre_');
const directTitle = existingResults[`|${directMatch}|`] || existingResults[directMatch];
if (directTitle) return directTitle;
// STRATÉGIE 2: Même niveau hiérarchique (H2, H3)
const levelMatch = textName.match(/(H\d)_(\d+)/);
if (levelMatch) {
const [, level, number] = levelMatch;
const titleTag = `Titre_${level}_${number}`;
const levelTitle = existingResults[`|${titleTag}|`] || existingResults[titleTag];
if (levelTitle) return levelTitle;
}
// STRATÉGIE 3: Proximité dans l'ordre (texte suivant un titre)
const allTitles = Object.entries(existingResults)
.filter(([tag]) => tag.includes('Titre'))
.sort(([a], [b]) => a.localeCompare(b));
if (allTitles.length > 0) {
// Retourner le premier titre disponible comme contexte général
return allTitles[0][1];
}
return null;
}
/**
* Créer prompt batch de base
*/
function createBatchBasePrompt(elements, type, csvData, existingResults = {}) {
const personality = csvData.personality;
let prompt = `=== 1. CONTEXTE ===
Entreprise: Autocollant.fr - signalétique personnalisée
Sujet: ${csvData.mc0}
Type d'article: SEO professionnel pour site commercial
=== 2. PERSONNALITÉ ===
Rédacteur: ${personality.nom}
Style: ${personality.style}
Ton: ${personality.description || 'professionnel'}
=== 3. RÈGLES GÉNÉRALES ===
- Contenu SEO optimisé
- Langage naturel et fluide
- Éviter répétitions
- Pas de références techniques dans le contenu
- Textes rédigés humainement et de façon authentique
- IMPÉRATIF: Respecter strictement les contraintes XML (nombre de mots, etc.)
=== 4. ÉLÉMENTS À GÉNÉRER ===
`;
// AJOUTER CONTEXTE DES TITRES POUR LES TEXTES
if (type === 'texte' && Object.keys(existingResults).length > 0) {
const generatedTitles = Object.entries(existingResults)
.filter(([tag]) => tag.includes('Titre'))
.map(([tag, title]) => `${tag.replace(/\|/g, '')}: "${title}"`)
.slice(0, 5); // Limiter à 5 titres pour éviter surcharge
if (generatedTitles.length > 0) {
prompt += `
Titres existants pour contexte:
${generatedTitles.join('\n')}
`;
}
}
elements.forEach((elementInfo, index) => {
const cleanTag = elementInfo.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
prompt += `${index + 1}. [${cleanTag}] `;
// INSTRUCTIONS PROPRES PAR ÉLÉMENT
if (type === 'titre') {
if (elementInfo.element.type === 'titre_h1') {
prompt += `Titre principal accrocheur\n`;
} else if (elementInfo.element.type === 'titre_h2') {
prompt += `Titre de section engageant\n`;
} else if (elementInfo.element.type === 'titre_h3') {
prompt += `Sous-titre spécialisé\n`;
} else {
prompt += `Titre pertinent\n`;
}
} else if (type === 'texte') {
prompt += `Paragraphe informatif\n`;
// ASSOCIER LE TITRE CORRESPONDANT AUTOMATIQUEMENT
const associatedTitle = findAssociatedTitle(elementInfo, existingResults);
if (associatedTitle) {
prompt += ` Contexte: "${associatedTitle}"\n`;
}
if (elementInfo.element.resolvedContent) {
prompt += ` Angle: "${elementInfo.element.resolvedContent}"\n`;
}
} else if (type === 'intro') {
prompt += `Introduction engageante\n`;
} else {
prompt += `Contenu pertinent\n`;
}
});
prompt += `\nSTYLE ${personality.nom.toUpperCase()} - ${personality.style}:
- Vocabulaire: ${personality.vocabulairePref}
- Connecteurs: ${personality.connecteursPref}
- Phrases: ${personality.longueurPhrases}
- Niveau technique: ${personality.niveauTechnique}
CONSIGNES STRICTES POUR ARTICLE SEO:
- CONTEXTE: Article professionnel pour site e-commerce, destiné aux clients potentiels
- STYLE: ${personality.style} de ${personality.nom} mais ADAPTÉ au web professionnel
- INTERDICTION ABSOLUE: expressions trop familières répétées ("du coup", "bon", "franchement", "nickel", "tip-top")
- VOCABULAIRE: Mélange expertise technique + accessibilité client
- SEO: Utilise naturellement "${csvData.mc0}" et termes associés
- POUR LES TITRES: Titre SEO attractif UNIQUEMENT, JAMAIS "Titre_H1_1" ou "Titre_H2_7"
- EXEMPLE TITRE: "Plaques personnalisées résistantes : guide complet 2024"
- CONTENU: Informatif, rassurant, incite à l'achat SANS être trop commercial
- RÉPONDS DIRECTEMENT par le contenu web demandé, SANS préfixe
FORMAT DE RÉPONSE ${type === 'titre' ? '(TITRES UNIQUEMENT)' : ''}:
[${elements[0].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}]
${type === 'titre' ? 'Titre réel et attractif (PAS "Titre_H1_1")' : 'Contenu rédigé selon le style ' + personality.nom}
[${elements[1] ? elements[1].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '') : 'element2'}]
${type === 'titre' ? 'Titre réel et attractif (PAS "Titre_H2_1")' : 'Contenu rédigé selon le style ' + personality.nom}
etc...`;
return prompt;
}
/**
* Parser réponse batch générique avec nettoyage des tags XML
*/
function parseBatchResponse(response, elements) {
const results = {};
// Parser avec regex [TAG] contenu
const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
let content = match[2].trim();
// NOUVEAU: Nettoyer les tags XML qui peuvent apparaître dans le contenu
content = cleanXMLTagsFromContent(content);
parsedItems[tag] = content;
}
// Mapper aux vrais tags avec |
elements.forEach(element => {
const cleanTag = element.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
if (parsedItems[cleanTag] && parsedItems[cleanTag].length > 10) {
results[element.tag] = parsedItems[cleanTag];
logSh(`✅ Parsé [${cleanTag}]: "${parsedItems[cleanTag].substring(0, 100)}..."`, 'DEBUG');
} else {
// Fallback si parsing échoue ou contenu trop court
results[element.tag] = `Contenu professionnel pour ${element.element.name}`;
logSh(`⚠️ Fallback [${cleanTag}]: parsing échoué ou contenu invalide`, 'WARNING');
}
});
return results;
}
/**
* NOUVELLE FONCTION: Nettoyer les tags XML du contenu généré
*/
function cleanXMLTagsFromContent(content) {
if (!content) return content;
// Supprimer les tags XML avec **
content = content.replace(/\*\*[^*]+\*\*/g, '');
// Supprimer les préfixes de titres indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?(voici\s+le\s+topo\s+pour\s+)?Titre_[HU]\d+_\d+[.,\s]*/gi, '');
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?pour\s+Titre_[HU]\d+_\d+[.,\s]*/gi, '');
content = content.replace(/^(Bon,?\s*)?(donc,?\s*)?Titre_[HU]\d+_\d+[.,\s]*/gi, '');
// Supprimer les messages d'excuse
content = content.replace(/Oh là là,?\s*je\s*(suis\s*)?(\w+\s*)?désolée?\s*,?\s*mais\s*je\s*n'ai\s*pas\s*l'information.*?(?=\.|$)/gi, '');
content = content.replace(/Bon,?\s*passons\s*au\s*suivant.*?(?=\.|$)/gi, '');
content = content.replace(/je\s*ne\s*sais\s*pas\s*quoi\s*vous\s*dire.*?(?=\.|$)/gi, '');
content = content.replace(/encore\s*un\s*point\s*où\s*je\s*n'ai\s*pas\s*l'information.*?(?=\.|$)/gi, '');
// Réduire les répétitions excessives d'expressions familières
content = content.replace(/(du coup[,\s]+){3,}/gi, 'du coup ');
content = content.replace(/(bon[,\s]+){3,}/gi, 'bon ');
content = content.replace(/(franchement[,\s]+){3,}/gi, 'franchement ');
content = content.replace(/(alors[,\s]+){3,}/gi, 'alors ');
content = content.replace(/(nickel[,\s]+){2,}/gi, 'nickel ');
content = content.replace(/(tip-top[,\s]+){2,}/gi, 'tip-top ');
content = content.replace(/(costaud[,\s]+){2,}/gi, 'costaud ');
// Nettoyer espaces multiples et retours ligne
content = content.replace(/\s{2,}/g, ' ');
content = content.replace(/\n{2,}/g, '\n');
content = content.trim();
return content;
}
// ============= PARSING FUNCTIONS =============
// FONCTION SUPPRIMÉE : parseAllTechnicalTermsResponse() - Parser batch défaillant remplacé par traitement individuel
// FONCTIONS SUPPRIMÉES : parseTechnicalEnhancementBatchResponse() et parseTechnicalBatchResponse() - Remplacées par traitement individuel
// Placeholder pour les fonctions de parsing conservées qui suivent
function parseTransitionsBatchResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let content = match[2].trim();
// Appliquer le nettoyage XML
content = cleanXMLTagsFromContent(content);
if (content && content.length > 10) {
results[chunk[index].tag] = content;
} else {
// Fallback si contenu invalide
results[chunk[index].tag] = chunk[index].content; // Garder contenu original
}
index++;
}
return results;
}
function parseStyleBatchResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let content = match[2].trim();
// Appliquer le nettoyage XML
content = cleanXMLTagsFromContent(content);
if (content && content.length > 10) {
results[chunk[index].tag] = content;
} else {
// Fallback si contenu invalide
results[chunk[index].tag] = chunk[index].content; // Garder contenu original
}
index++;
}
return results;
}
// ============= ANALYSIS FUNCTIONS =============
/**
* Analyser besoin d'amélioration transitions
*/
function analyzeTransitionNeed(content) {
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
// Critères multiples d'analyse
const metrics = {
repetitiveConnectors: analyzeRepetitiveConnectors(content),
abruptTransitions: analyzeAbruptTransitions(sentences),
sentenceVariety: analyzeSentenceVariety(sentences),
formalityLevel: analyzeFormalityLevel(content),
overallLength: content.length
};
// Score de besoin (0-1)
let needScore = 0;
needScore += metrics.repetitiveConnectors * 0.3;
needScore += metrics.abruptTransitions * 0.4;
needScore += (1 - metrics.sentenceVariety) * 0.2;
needScore += metrics.formalityLevel * 0.1;
// Seuil ajustable selon longueur
const threshold = metrics.overallLength > 300 ? 0.4 : 0.6;
logSh(`🔍 Analyse transitions: score=${needScore.toFixed(2)}, seuil=${threshold}`, 'DEBUG');
return needScore > threshold;
}
function analyzeRepetitiveConnectors(content) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc'];
let totalConnectors = 0;
let repetitions = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
totalConnectors += matches.length;
if (matches.length > 1) repetitions += matches.length - 1;
});
return totalConnectors > 0 ? repetitions / totalConnectors : 0;
}
function analyzeAbruptTransitions(sentences) {
if (sentences.length < 2) return 0;
let abruptCount = 0;
for (let i = 1; i < sentences.length; i++) {
const current = sentences[i].trim();
const previous = sentences[i-1].trim();
const hasConnector = hasTransitionWord(current);
const topicContinuity = calculateTopicContinuity(previous, current);
// Transition abrupte = pas de connecteur + faible continuité thématique
if (!hasConnector && topicContinuity < 0.3) {
abruptCount++;
}
}
return abruptCount / (sentences.length - 1);
}
function analyzeSentenceVariety(sentences) {
if (sentences.length < 2) return 1;
const lengths = sentences.map(s => s.trim().length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
// Calculer variance des longueurs
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
// Score de variété (0-1) - plus la variance est élevée, plus c'est varié
return Math.min(1, stdDev / avgLength);
}
function analyzeFormalityLevel(content) {
const formalIndicators = [
'il convient de', 'par conséquent', 'néanmoins', 'toutefois',
'de surcroît', 'en définitive', 'il s\'avère que', 'force est de constater'
];
let formalCount = 0;
formalIndicators.forEach(indicator => {
if (content.toLowerCase().includes(indicator)) formalCount++;
});
const sentences = content.split(/[.!?]+/).length;
return sentences > 0 ? formalCount / sentences : 0;
}
function calculateTopicContinuity(sentence1, sentence2) {
const stopWords = ['les', 'des', 'une', 'sont', 'avec', 'pour', 'dans', 'cette', 'vous', 'peut', 'tout'];
const words1 = extractSignificantWords(sentence1, stopWords);
const words2 = extractSignificantWords(sentence2, stopWords);
if (words1.length === 0 || words2.length === 0) return 0;
const commonWords = words1.filter(word => words2.includes(word));
const semanticSimilarity = commonWords.length / Math.min(words1.length, words2.length);
const technicalWords = ['plaque', 'dibond', 'aluminium', 'impression', 'signalétique'];
const commonTechnical = commonWords.filter(word => technicalWords.includes(word));
const technicalBonus = commonTechnical.length * 0.2;
return Math.min(1, semanticSimilarity + technicalBonus);
}
function extractSignificantWords(sentence, stopWords) {
return sentence.toLowerCase()
.match(/\b[a-zàâäéèêëïîôùûüÿç]{4,}\b/g) // Mots 4+ lettres avec accents
?.filter(word => !stopWords.includes(word)) || [];
}
function hasTransitionWord(sentence) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite', 'puis', 'également', 'aussi', 'toutefois', 'néanmoins', 'alors', 'enfin'];
return connectors.some(connector => sentence.toLowerCase().includes(connector));
}
/**
* Instructions de style dynamiques
*/
function getPersonalityStyleInstructions(personality) {
// CORRECTION: Utilisation des VRAIS champs Google Sheets au lieu du hardcodé
if (!personality) return "Style professionnel standard";
const instructions = `STYLE ${personality.nom.toUpperCase()} (${personality.style}):
- Description: ${personality.description}
- Vocabulaire préféré: ${personality.vocabulairePref || 'professionnel, qualité'}
- Connecteurs préférés: ${personality.connecteursPref || 'par ailleurs, en effet'}
- Mots-clés secteurs: ${personality.motsClesSecteurs || 'technique, qualité'}
- Longueur phrases: ${personality.longueurPhrases || 'Moyennes (15-25 mots)'}
- Niveau technique: ${personality.niveauTechnique || 'Accessible'}
- Style CTA: ${personality.ctaStyle || 'Professionnel'}
- Défauts simulés: ${personality.defautsSimules || 'Aucun'}
- Erreurs typiques à éviter: ${personality.erreursTypiques || 'Répétitions, généralités'}`;
return instructions;
}
/**
* Créer prompt pour élément (fonction de base nécessaire)
*/
function createPromptForElement(element, csvData) {
const personality = csvData.personality;
const styleContext = `Rédige dans le style ${personality.style} de ${personality.nom} (${personality.description}).`;
switch (element.type) {
case 'titre_h1':
return `${styleContext}
MISSION: Crée un titre H1 accrocheur pour: ${csvData.mc0}
Référence: ${csvData.t0}
CONSIGNES: 10 mots maximum, direct et impactant, optimisé SEO.
RÉPONDS UNIQUEMENT PAR LE TITRE, sans introduction.`;
case 'titre_h2':
return `${styleContext}
MISSION: Crée un titre H2 optimisé SEO pour: ${csvData.mc0}
CONSIGNES: Intègre naturellement le mot-clé, 8 mots maximum.
RÉPONDS UNIQUEMENT PAR LE TITRE, sans introduction.`;
case 'intro':
if (element.instructions) {
return `${styleContext}
MISSION: ${element.instructions}
Données contextuelles:
- MC0: ${csvData.mc0}
- T-1: ${csvData.tMinus1}
- L-1: ${csvData.lMinus1}
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
}
return `${styleContext}
MISSION: Rédige une introduction de 100 mots pour ${csvData.mc0}.
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
case 'texte':
if (element.instructions) {
return `${styleContext}
MISSION: ${element.instructions}
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
}
return `${styleContext}
MISSION: Rédige un paragraphe de 150 mots sur ${csvData.mc0}.
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
case 'faq_question':
if (element.instructions) {
return `${styleContext}
MISSION: ${element.instructions}
CONTEXTE: ${csvData.mc0} - ${csvData.t0}
STYLE: Question ${csvData.personality?.style} de ${csvData.personality?.nom}
CONSIGNES:
- Vraie question que se poserait un client intéressé par ${csvData.mc0}
- Commence par "Comment", "Quel", "Pourquoi", "Où", "Quand" ou "Est-ce que"
- Maximum 15 mots, pratique et concrète
- Vocabulaire: ${csvData.personality?.vocabulairePref || 'accessible'}
RÉPONDS UNIQUEMENT PAR LA QUESTION, sans guillemets ni introduction.`;
}
return `${styleContext}
MISSION: Génère une vraie question FAQ client sur ${csvData.mc0}.
CONSIGNES:
- Question pratique et concrète qu'un client se poserait
- Commence par "Comment", "Quel", "Pourquoi", "Combien", "Où" ou "Est-ce que"
- Maximum 15 mots, style ${csvData.personality?.style}
- Vocabulaire: ${csvData.personality?.vocabulairePref || 'accessible'}
RÉPONDS UNIQUEMENT PAR LA QUESTION, sans guillemets ni introduction.`;
case 'faq_reponse':
if (element.instructions) {
return `${styleContext}
MISSION: ${element.instructions}
CONTEXTE: ${csvData.mc0} - ${csvData.t0}
STYLE: Réponse ${csvData.personality?.style} de ${csvData.personality?.nom}
CONSIGNES:
- Réponse utile et rassurante
- 50-80 mots, ton ${csvData.personality?.style}
- Vocabulaire: ${csvData.personality?.vocabulairePref}
- Connecteurs: ${csvData.personality?.connecteursPref}
RÉPONDS UNIQUEMENT PAR LA RÉPONSE, sans introduction.`;
}
return `${styleContext}
MISSION: Réponds à une question client sur ${csvData.mc0}.
CONSIGNES:
- Réponse utile, claire et rassurante
- 50-80 mots, ton ${csvData.personality?.style} de ${csvData.personality?.nom}
- Vocabulaire: ${csvData.personality?.vocabulairePref || 'professionnel'}
- Connecteurs: ${csvData.personality?.connecteursPref || 'par ailleurs'}
RÉPONDS UNIQUEMENT PAR LA RÉPONSE, sans introduction.`;
default:
return `${styleContext}
MISSION: Génère du contenu pertinent pour ${csvData.mc0}.
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
}
}
/**
* NOUVELLE FONCTION : Extraction batch TOUS les termes techniques
*/
async function extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider) {
const contentEntries = Object.keys(baseContents);
const batchAnalysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTENUS À ANALYSER:
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
CONTENU: "${baseContents[tag]}"`).join('\n\n')}
CONSIGNES:
- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
- Évite mots génériques (qualité, service, pratique, personnalisé, etc.)
- Focus: matériaux, procédés, normes, dimensions, technologies
- Si aucun terme technique → "AUCUN"
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique
FORMAT RÉPONSE EXACT:
[1] dibond, impression UV, 3mm OU AUCUN
[2] aluminium, fraisage CNC OU AUCUN
[3] AUCUN
etc... (${contentEntries.length} lignes total)`;
try {
const analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, {
temperature: 0.3,
maxTokens: 2000
}, csvData.personality);
return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries);
} catch (error) {
logSh(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`);
}
}
/**
* NOUVELLE FONCTION : Enhancement batch TOUS les éléments
*/
async function enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider) {
if (elementsNeedingEnhancement.length === 0) return {};
const batchEnhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces ${elementsNeedingEnhancement.length} contenus.
CONTEXTE: Article SEO pour site e-commerce de signalétique
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} web professionnel)
SUJET: ${csvData.mc0} - Secteur: Signalétique/impression
VOCABULAIRE PRÉFÉRÉ: ${csvData.personality?.vocabulairePref}
CONTENUS + TERMES À AMÉLIORER:
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag}
CONTENU ACTUEL: "${item.content}"
TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')}
CONSIGNES STRICTES:
- Améliore UNIQUEMENT la précision technique, garde le style ${csvData.personality?.nom}
- GARDE la même longueur, structure et ton
- Intègre naturellement les termes techniques listés
- NE CHANGE PAS le fond du message ni le style personnel
- Utilise un vocabulaire expert mais accessible
- ÉVITE les répétitions excessives
- RESPECTE le niveau technique: ${csvData.personality?.niveauTechnique}
- Termes techniques secteur: dibond, aluminium, impression UV, fraisage, épaisseur, PMMA
FORMAT RÉPONSE:
[1] Contenu avec amélioration technique selon ${csvData.personality?.nom}
[2] Contenu avec amélioration technique selon ${csvData.personality?.nom}
etc... (${elementsNeedingEnhancement.length} éléments total)`;
try {
const enhanced = await callLLM(aiProvider, batchEnhancementPrompt, {
temperature: 0.4,
maxTokens: 5000 // Plus large pour batch total
}, csvData.personality);
return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement);
} catch (error) {
logSh(`❌ FATAL: Enhancement technique batch échoué: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Enhancement technique batch impossible - arrêt du workflow: ${error.message}`);
}
}
/**
* Parser réponse extraction termes
*/
function parseAllTechnicalTermsResponse(response, baseContents, contentEntries) {
const results = [];
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
const parsedItems = {};
// Parser la réponse
while ((match = regex.exec(response)) !== null) {
const index = parseInt(match[1]) - 1; // Convertir en 0-indexé
const termsText = match[2].trim();
parsedItems[index] = termsText;
}
// Mapper aux éléments
contentEntries.forEach((tag, index) => {
const termsText = parsedItems[index] || 'AUCUN';
const hasTerms = !termsText.toUpperCase().includes('AUCUN');
const technicalTerms = hasTerms ?
termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) :
[];
results.push({
tag: tag,
content: baseContents[tag],
technicalTerms: technicalTerms,
needsEnhancement: hasTerms && technicalTerms.length > 0
});
logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'pas de termes techniques'}`, 'DEBUG');
});
const enhancementCount = results.filter(r => r.needsEnhancement).length;
logSh(`📊 Analyse terminée: ${enhancementCount}/${contentEntries.length} éléments ont besoin d'enhancement`, 'INFO');
return results;
}
/**
* Parser réponse enhancement technique
*/
function parseTechnicalEnhancementBatchResponse(response, elementsNeedingEnhancement) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < elementsNeedingEnhancement.length) {
let content = match[2].trim();
const element = elementsNeedingEnhancement[index];
// NOUVEAU: Appliquer le nettoyage XML
content = cleanXMLTagsFromContent(content);
if (content && content.length > 10) {
results[element.tag] = content;
logSh(`✅ Enhanced [${element.tag}]: "${content.substring(0, 100)}..."`, 'DEBUG');
} else {
// Fallback si contenu invalide après nettoyage
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: contenu invalide après nettoyage`, 'WARNING');
}
index++;
}
// Vérifier si on a bien tout parsé
if (Object.keys(results).length < elementsNeedingEnhancement.length) {
logSh(`⚠️ Parsing partiel: ${Object.keys(results).length}/${elementsNeedingEnhancement.length}`, 'WARNING');
// Compléter avec contenu original pour les manquants
elementsNeedingEnhancement.forEach(element => {
if (!results[element.tag]) {
results[element.tag] = element.content;
}
});
}
return results;
}
// ============= EXPORTS =============
module.exports = {
generateWithBatchEnhancement,
generateAllContentBase,
enhanceAllTechnicalTerms,
enhanceAllTransitions,
enhanceAllPersonalityStyle,
collectAllElements,
groupElementsByType,
chunkArray,
createBatchBasePrompt,
parseBatchResponse,
cleanXMLTagsFromContent,
analyzeTransitionNeed,
getPersonalityStyleInstructions,
createPromptForElement,
sleep,
separateFAQPairsAndOthers,
generateFAQPairsRestored,
createBatchFAQPairsPrompt,
parseFAQPairsResponse,
cleanFAQInstructions,
extractAllTechnicalTermsBatch,
enhanceAllElementsTechnicalBatch,
parseAllTechnicalTermsResponse,
parseTechnicalEnhancementBatchResponse
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/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/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/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
// ========================================
/**
* 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
const contentStructure = {
'Titre_H1': `Rédige un titre H1 accrocheur et optimisé SEO sur ${inputData.mc0}`,
'Introduction': `Rédige une introduction engageante qui présente ${inputData.mc0}`,
'Contenu_Principal': `Développe le contenu principal détaillé sur ${inputData.mc0} avec des informations utiles et techniques`,
'Conclusion': `Rédige une conclusion percutante qui encourage à l'action pour ${inputData.mc0}`
};
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 { applyAllSelectiveLayers } = require('./selective-enhancement/SelectiveCore');
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
logSh('🎯 Enhancement sélectif du contenu fourni', 'DEBUG');
const result = await applyAllSelectiveLayers(contentToEnhance, {
...inputData,
...config,
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 { applyAdversarialLayer } = require('./adversarial-generation/AdversarialCore');
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
const result = await applyAdversarialLayer(contentToTransform, {
...config,
csvData: inputData,
detectorTarget: config.detectorTarget || 'general',
intensity: config.intensity || 1.0,
method: config.method || 'regeneration'
});
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 { applyHumanSimulationLayer } = require('./human-simulation/HumanSimulationCore');
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 result = await applyHumanSimulationLayer(contentToHumanize, 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 { applyPatternBreakingLayer } = require('./pattern-breaking/PatternBreakingCore');
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 result = await applyPatternBreakingLayer(contentToTransform, 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 `<?xml version="1.0" encoding="UTF-8"?>
<article>
<h1>|Titre_H1{{T0}}{Titre principal optimisé}|</h1>
<intro>|Introduction{{MC0}}{Introduction engageante}|</intro>
<content>|Contenu_Principal{{MC0,T0}}{Contenu principal détaillé}|</content>
<conclusion>|Conclusion{{T0}}{Conclusion percutante}|</conclusion>
</article>`;
}
}
module.exports = {
StepExecutor
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/Utils.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: utils.js - Conversion Node.js
// Description: Utilitaires génériques pour le workflow
// ========================================
// Import du système de logging (assumant que logSh est disponible globalement)
// const { logSh } = require('./logging'); // À décommenter si logSh est dans un module séparé
/**
* Créer une réponse de succès standardisée
* @param {Object} data - Données à retourner
* @returns {Object} Réponse formatée pour Express/HTTP
*/
function createSuccessResponse(data) {
return {
success: true,
data: data,
timestamp: new Date().toISOString()
};
}
/**
* Créer une réponse d'erreur standardisée
* @param {string|Error} error - Message d'erreur ou objet Error
* @returns {Object} Réponse d'erreur formatée
*/
function createErrorResponse(error) {
const errorMessage = error instanceof Error ? error.message : error.toString();
return {
success: false,
error: errorMessage,
timestamp: new Date().toISOString(),
stack: process.env.NODE_ENV === 'development' && error instanceof Error ? error.stack : undefined
};
}
/**
* Middleware Express pour envoyer des réponses standardisées
* Usage: res.success(data) ou res.error(error)
*/
function responseMiddleware(req, res, next) {
// Méthode pour réponse de succès
res.success = (data, statusCode = 200) => {
res.status(statusCode).json(createSuccessResponse(data));
};
// Méthode pour réponse d'erreur
res.error = (error, statusCode = 500) => {
res.status(statusCode).json(createErrorResponse(error));
};
next();
}
/**
* HELPER : Nettoyer les instructions FAQ
* Remplace les variables et nettoie le HTML
* @param {string} instructions - Instructions à nettoyer
* @param {Object} csvData - Données CSV pour remplacement variables
* @returns {string} Instructions nettoyées
*/
function cleanFAQInstructions(instructions, csvData) {
if (!instructions || !csvData) {
return instructions || '';
}
let clean = instructions.toString();
try {
// Remplacer variables simples
clean = clean.replace(/\{\{MC0\}\}/g, csvData.mc0 || '');
clean = clean.replace(/\{\{T0\}\}/g, csvData.t0 || '');
// Variables multiples si nécessaire
if (csvData.mcPlus1) {
const mcPlus1 = csvData.mcPlus1.split(',').map(s => s.trim());
for (let i = 1; i <= 6; i++) {
const mcValue = mcPlus1[i-1] || `[MC+1_${i} non défini]`;
clean = clean.replace(new RegExp(`\\{\\{MC\\+1_${i}\\}\\}`, 'g'), mcValue);
}
}
// Variables T+1 et L+1 si disponibles
if (csvData.tPlus1) {
const tPlus1 = csvData.tPlus1.split(',').map(s => s.trim());
for (let i = 1; i <= 6; i++) {
const tValue = tPlus1[i-1] || `[T+1_${i} non défini]`;
clean = clean.replace(new RegExp(`\\{\\{T\\+1_${i}\\}\\}`, 'g'), tValue);
}
}
if (csvData.lPlus1) {
const lPlus1 = csvData.lPlus1.split(',').map(s => s.trim());
for (let i = 1; i <= 6; i++) {
const lValue = lPlus1[i-1] || `[L+1_${i} non défini]`;
clean = clean.replace(new RegExp(`\\{\\{L\\+1_${i}\\}\\}`, 'g'), lValue);
}
}
// Nettoyer HTML
clean = clean.replace(/<\/?[^>]+>/g, '');
// Nettoyer espaces en trop
clean = clean.replace(/\s+/g, ' ').trim();
} catch (error) {
if (typeof logSh === 'function') {
logSh(`⚠️ Erreur nettoyage instructions FAQ: ${error.toString()}`, 'WARNING');
}
// Retourner au moins la version partiellement nettoyée
}
return clean;
}
/**
* Utilitaire pour attendre un délai (remplace Utilities.sleep de Google Apps Script)
* @param {number} ms - Millisecondes à attendre
* @returns {Promise} Promise qui se résout après le délai
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Utilitaire pour encoder en base64
* @param {string} text - Texte à encoder
* @returns {string} Texte encodé en base64
*/
function base64Encode(text) {
return Buffer.from(text, 'utf8').toString('base64');
}
/**
* Utilitaire pour décoder du base64
* @param {string} base64Text - Texte base64 à décoder
* @returns {string} Texte décodé
*/
function base64Decode(base64Text) {
return Buffer.from(base64Text, 'base64').toString('utf8');
}
/**
* Valider et nettoyer un slug/filename
* @param {string} slug - Slug à nettoyer
* @returns {string} Slug nettoyé
*/
function cleanSlug(slug) {
if (!slug) return '';
return slug
.toString()
.toLowerCase()
.replace(/[^a-z0-9\-_]/g, '-') // Remplacer caractères spéciaux par -
.replace(/-+/g, '-') // Éviter doubles tirets
.replace(/^-+|-+$/g, ''); // Enlever tirets début/fin
}
/**
* Compter les mots dans un texte
* @param {string} text - Texte à analyser
* @returns {number} Nombre de mots
*/
function countWords(text) {
if (!text || typeof text !== 'string') return 0;
return text
.trim()
.replace(/\s+/g, ' ') // Normaliser espaces
.split(' ')
.filter(word => word.length > 0)
.length;
}
/**
* Formater une durée en millisecondes en format lisible
* @param {number} ms - Durée en millisecondes
* @returns {string} Durée formatée (ex: "2.3s" ou "450ms")
*/
function formatDuration(ms) {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60000) {
return `${(ms / 1000).toFixed(1)}s`;
} else {
const minutes = Math.floor(ms / 60000);
const seconds = ((ms % 60000) / 1000).toFixed(1);
return `${minutes}m ${seconds}s`;
}
}
/**
* Utilitaire pour retry automatique d'une fonction
* @param {Function} fn - Fonction à exécuter avec retry
* @param {number} maxRetries - Nombre maximum de tentatives
* @param {number} delay - Délai entre tentatives (ms)
* @returns {Promise} Résultat de la fonction ou erreur finale
*/
async function withRetry(fn, maxRetries = 3, delay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (typeof logSh === 'function') {
logSh(`⚠️ Tentative ${attempt}/${maxRetries} échouée: ${error.toString()}`, 'WARNING');
}
if (attempt < maxRetries) {
await sleep(delay * attempt); // Exponential backoff
}
}
}
throw lastError;
}
/**
* Validation basique d'email
* @param {string} email - Email à valider
* @returns {boolean} True si email valide
*/
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* Générer un ID unique simple
* @returns {string} ID unique basé sur timestamp + random
*/
function generateId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Truncate un texte à une longueur donnée
* @param {string} text - Texte à tronquer
* @param {number} maxLength - Longueur maximale
* @param {string} suffix - Suffixe à ajouter si tronqué (défaut: '...')
* @returns {string} Texte tronqué
*/
function truncate(text, maxLength, suffix = '...') {
if (!text || text.length <= maxLength) {
return text;
}
return text.substring(0, maxLength - suffix.length) + suffix;
}
// ============= EXPORTS =============
module.exports = {
createSuccessResponse,
createErrorResponse,
responseMiddleware,
cleanFAQInstructions,
sleep,
base64Encode,
base64Decode,
cleanSlug,
countWords,
formatDuration,
withRetry,
isValidEmail,
generateId,
truncate
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialPromptEngine.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ADVERSARIAL PROMPT ENGINE - NIVEAU 3
// Responsabilité: Générer instructions anti-détection pour prompts
// Anti-détection: Prévention par modification des prompts LLM
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* INSTRUCTIONS ADVERSARIALES PAR DÉTECTEUR
* Stratégies spécialisées pour contrer chaque détecteur IA
*/
const ADVERSARIAL_INSTRUCTIONS = {
general: {
title: 'CONTRAINTES ANTI-DÉTECTION GÉNÉRALES',
rules: [
'Évite ces mots typiques IA : "optimal", "comprehensive", "seamless", "robust", "leverage"',
'Varie longueurs phrases : mélange phrases courtes (5-10 mots) ET longues (20-30 mots)',
'Utilise expressions françaises familières et tournures idiomatiques',
'Ajoute imperfections naturelles : répétitions légères, hésitations, reformulations',
'Change angle ou perspective 1-2 fois dans le texte',
'Préfère synonymes moins évidents : "bon" plutôt qu\'"excellent", "solide" plutôt qu\'optimisé"',
'Intègre connecteurs variés et naturels selon le contexte'
],
intensity: 1.0,
weight: 0.8
},
gptZero: {
title: 'SPÉCIAL ANTI-GPTZERO',
rules: [
'Surprends avec tournures inattendues et constructions atypiques',
'Évite phrases trop prévisibles ou patterns répétitifs',
'Utilise synonymes moins évidents et vocabulaire moins commun',
'Varie drastiquement la complexité syntaxique entre phrases',
'Intercale observations personnelles ou détours narratifs',
'Alterne registres de langue (soutenu → familier → technique)',
'Brise la logique linéaire avec des parenthèses, incises, apartés'
],
intensity: 1.2,
weight: 0.9,
targetMetric: 'unpredictability'
},
originality: {
title: 'SPÉCIAL ANTI-ORIGINALITY',
rules: [
'Vocabulaire TRÈS varié : évite répétitions même de synonymes',
'Structures phrases délibérément irrégulières et asymétriques',
'Changements angles fréquents : technique → personnel → général',
'Créativité sémantique : métaphores, comparaisons inattendues',
'Évite formulations académiques ou trop structurées',
'Intègre références culturelles, expressions régionales',
'Subvertis les attentes : commence par la fin, questionne l\'évidence'
],
intensity: 1.3,
weight: 0.95,
targetMetric: 'semantic_diversity'
},
copyLeaks: {
title: 'SPÉCIAL ANTI-COPYLEAKS',
rules: [
'Reformule idées communes avec angles totalement originaux',
'Évite phrases-types et formulations standard du secteur',
'Personnalise chaque assertion avec exemples spécifiques',
'Réinvente la façon de présenter informations basiques',
'Utilise analogies et métaphores plutôt que descriptions directes',
'Fragmente informations techniques en observations pratiques',
'Transforme données factuelles en récits ou témoignages'
],
intensity: 1.1,
weight: 0.85,
targetMetric: 'originality_score'
},
winston: {
title: 'SPÉCIAL ANTI-WINSTON',
rules: [
'Évite cohérence stylistique trop parfaite entre paragraphes',
'Simule variation naturelle d\'humeur et d\'énergie rédactionnelle',
'Intègre "erreurs" humaines : répétitions, corrections, précisions',
'Varie niveau de détail : parfois précis, parfois elliptique',
'Alterne registres émotionnels : enthousiaste → neutre → critique',
'Inclus hésitations et nuances : "peut-être", "généralement", "souvent"',
'Personnalise avec opinions subjectives et préférences'
],
intensity: 1.0,
weight: 0.9,
targetMetric: 'human_variation'
}
};
/**
* INSTRUCTIONS PERSONNALISÉES PAR TYPE D'ÉLÉMENT
*/
const ELEMENT_SPECIFIC_INSTRUCTIONS = {
titre_h1: {
base: 'Crée un titre percutant mais naturel',
adversarial: 'Évite formules marketing lisses, préfère authentique et direct'
},
titre_h2: {
base: 'Génère un sous-titre informatif',
adversarial: 'Varie structure : question, affirmation, exclamation selon contexte'
},
intro: {
base: 'Rédige introduction engageante',
adversarial: 'Commence par angle inattendu : anecdote, constat, question rhétorique'
},
texte: {
base: 'Développe paragraphe informatif',
adversarial: 'Mélange informations factuelles et observations personnelles'
},
faq_question: {
base: 'Formule question client naturelle',
adversarial: 'Utilise formulations vraiment utilisées par clients, pas académiques'
},
faq_reponse: {
base: 'Réponds de façon experte et rassurante',
adversarial: 'Ajoute nuances, "ça dépend", précisions contextuelles comme humain'
}
};
/**
* MAIN ENTRY POINT - GÉNÉRATEUR DE PROMPTS ADVERSARIAUX
* @param {string} basePrompt - Prompt de base
* @param {Object} config - Configuration adversariale
* @returns {string} - Prompt enrichi d'instructions anti-détection
*/
function createAdversarialPrompt(basePrompt, config = {}) {
return tracer.run('AdversarialPromptEngine.createAdversarialPrompt()', () => {
const {
detectorTarget = 'general',
intensity = 1.0,
elementType = 'generic',
personality = null,
contextualMode = true,
csvData = null,
debugMode = false
} = config;
tracer.annotate({
detectorTarget,
intensity,
elementType,
personalityStyle: personality?.style
});
try {
// 1. Sélectionner stratégie détecteur
const strategy = ADVERSARIAL_INSTRUCTIONS[detectorTarget] || ADVERSARIAL_INSTRUCTIONS.general;
// 2. Adapter intensité
const effectiveIntensity = intensity * (strategy.intensity || 1.0);
const shouldApplyStrategy = Math.random() < (strategy.weight || 0.8);
if (!shouldApplyStrategy && detectorTarget !== 'general') {
// Fallback sur stratégie générale
return createAdversarialPrompt(basePrompt, { ...config, detectorTarget: 'general' });
}
// 3. Construire instructions adversariales
const adversarialSection = buildAdversarialInstructions(strategy, {
elementType,
personality,
effectiveIntensity,
contextualMode,
csvData
});
// 4. Assembler prompt final
const enhancedPrompt = assembleEnhancedPrompt(basePrompt, adversarialSection, {
strategy,
elementType,
debugMode
});
if (debugMode) {
logSh(`🎯 Prompt adversarial généré: ${detectorTarget} (intensité: ${effectiveIntensity.toFixed(2)})`, 'DEBUG');
logSh(` Instructions: ${strategy.rules.length} règles appliquées`, 'DEBUG');
}
tracer.event('Prompt adversarial créé', {
detectorTarget,
rulesCount: strategy.rules.length,
promptLength: enhancedPrompt.length
});
return enhancedPrompt;
} catch (error) {
logSh(`❌ Erreur génération prompt adversarial: ${error.message}`, 'ERROR');
// Fallback: retourner prompt original
return basePrompt;
}
}, config);
}
/**
* Construire section instructions adversariales
*/
function buildAdversarialInstructions(strategy, config) {
const { elementType, personality, effectiveIntensity, contextualMode, csvData } = config;
let instructions = `\n\n=== ${strategy.title} ===\n`;
// Règles de base de la stratégie
const activeRules = selectActiveRules(strategy.rules, effectiveIntensity);
activeRules.forEach(rule => {
instructions += `${rule}\n`;
});
// Instructions spécifiques au type d'élément
if (ELEMENT_SPECIFIC_INSTRUCTIONS[elementType]) {
const elementInstructions = ELEMENT_SPECIFIC_INSTRUCTIONS[elementType];
instructions += `\nSPÉCIFIQUE ${elementType.toUpperCase()}:\n`;
instructions += `${elementInstructions.adversarial}\n`;
}
// Adaptations personnalité
if (personality && contextualMode) {
const personalityAdaptations = generatePersonalityAdaptations(personality, strategy);
if (personalityAdaptations) {
instructions += `\nADAPTATION PERSONNALITÉ ${personality.nom.toUpperCase()}:\n`;
instructions += personalityAdaptations;
}
}
// Contexte métier si disponible
if (csvData && contextualMode) {
const contextualInstructions = generateContextualInstructions(csvData, strategy);
if (contextualInstructions) {
instructions += `\nCONTEXTE MÉTIER:\n`;
instructions += contextualInstructions;
}
}
instructions += `\nIMPORTANT: Ces contraintes doivent sembler naturelles, pas forcées.\n`;
return instructions;
}
/**
* Sélectionner règles actives selon intensité
*/
function selectActiveRules(allRules, intensity) {
if (intensity >= 1.0) {
return allRules; // Toutes les règles
}
// Sélection proportionnelle à l'intensité
const ruleCount = Math.ceil(allRules.length * intensity);
return allRules.slice(0, ruleCount);
}
/**
* Générer adaptations personnalité
*/
function generatePersonalityAdaptations(personality, strategy) {
if (!personality) return null;
const adaptations = [];
// Style de la personnalité
if (personality.style) {
adaptations.push(`• Respecte le style ${personality.style} de ${personality.nom} tout en appliquant les contraintes`);
}
// Vocabulaire préféré
if (personality.vocabulairePref) {
adaptations.push(`• Intègre vocabulaire naturel: ${personality.vocabulairePref}`);
}
// Connecteurs préférés
if (personality.connecteursPref) {
adaptations.push(`• Utilise connecteurs variés: ${personality.connecteursPref}`);
}
// Longueur phrases selon personnalité
if (personality.longueurPhrases) {
adaptations.push(`• Longueur phrases: ${personality.longueurPhrases} mais avec variation anti-détection`);
}
return adaptations.length > 0 ? adaptations.join('\n') + '\n' : null;
}
/**
* Générer instructions contextuelles métier
*/
function generateContextualInstructions(csvData, strategy) {
if (!csvData.mc0) return null;
const instructions = [];
// Contexte sujet
instructions.push(`• Sujet: ${csvData.mc0} - utilise terminologie naturelle du domaine`);
// Éviter jargon selon détecteur
if (strategy.targetMetric === 'unpredictability') {
instructions.push(`• Évite jargon technique trop prévisible, privilégie explications accessibles`);
} else if (strategy.targetMetric === 'semantic_diversity') {
instructions.push(`• Varie façons de nommer/décrire ${csvData.mc0} - synonymes créatifs`);
}
return instructions.join('\n') + '\n';
}
/**
* Assembler prompt final
*/
function assembleEnhancedPrompt(basePrompt, adversarialSection, config) {
const { strategy, elementType, debugMode } = config;
// Structure du prompt amélioré
let enhancedPrompt = basePrompt;
// Injecter instructions adversariales
enhancedPrompt += adversarialSection;
// Rappel final selon stratégie
if (strategy.targetMetric) {
enhancedPrompt += `\nOBJECTIF PRIORITAIRE: Maximiser ${strategy.targetMetric} tout en conservant qualité.\n`;
}
// Instructions de réponse
enhancedPrompt += `\nRÉPONDS DIRECTEMENT par le contenu demandé, en appliquant naturellement ces contraintes.`;
return enhancedPrompt;
}
/**
* Analyser efficacité d'un prompt adversarial
*/
function analyzePromptEffectiveness(originalPrompt, adversarialPrompt, generatedContent) {
const analysis = {
promptEnhancement: {
originalLength: originalPrompt.length,
adversarialLength: adversarialPrompt.length,
enhancementRatio: adversarialPrompt.length / originalPrompt.length,
instructionsAdded: (adversarialPrompt.match(/•/g) || []).length
},
contentMetrics: analyzeGeneratedContent(generatedContent),
effectiveness: 0
};
// Score d'efficacité simple
analysis.effectiveness = Math.min(100,
(analysis.promptEnhancement.enhancementRatio - 1) * 50 +
analysis.contentMetrics.diversityScore
);
return analysis;
}
/**
* Analyser contenu généré
*/
function analyzeGeneratedContent(content) {
if (!content || typeof content !== 'string') {
return { diversityScore: 0, wordCount: 0, sentenceVariation: 0 };
}
const words = content.split(/\s+/).filter(w => w.length > 2);
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 5);
// Diversité vocabulaire
const uniqueWords = [...new Set(words.map(w => w.toLowerCase()))];
const diversityScore = uniqueWords.length / Math.max(1, words.length) * 100;
// Variation longueurs phrases
const sentenceLengths = sentences.map(s => s.split(/\s+/).length);
const avgLength = sentenceLengths.reduce((a, b) => a + b, 0) / Math.max(1, sentenceLengths.length);
const variance = sentenceLengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / Math.max(1, sentenceLengths.length);
const sentenceVariation = Math.sqrt(variance) / Math.max(1, avgLength) * 100;
return {
diversityScore: Math.round(diversityScore),
wordCount: words.length,
sentenceCount: sentences.length,
sentenceVariation: Math.round(sentenceVariation),
avgSentenceLength: Math.round(avgLength)
};
}
/**
* Obtenir liste des détecteurs supportés
*/
function getSupportedDetectors() {
return Object.keys(ADVERSARIAL_INSTRUCTIONS).map(key => ({
id: key,
name: ADVERSARIAL_INSTRUCTIONS[key].title,
intensity: ADVERSARIAL_INSTRUCTIONS[key].intensity,
weight: ADVERSARIAL_INSTRUCTIONS[key].weight,
rulesCount: ADVERSARIAL_INSTRUCTIONS[key].rules.length,
targetMetric: ADVERSARIAL_INSTRUCTIONS[key].targetMetric || 'general'
}));
}
module.exports = {
createAdversarialPrompt, // ← MAIN ENTRY POINT
buildAdversarialInstructions,
analyzePromptEffectiveness,
analyzeGeneratedContent,
getSupportedDetectors,
ADVERSARIAL_INSTRUCTIONS,
ELEMENT_SPECIFIC_INSTRUCTIONS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialInitialGeneration.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ÉTAPE 1: GÉNÉRATION INITIALE ADVERSARIALE
// Responsabilité: Créer le contenu de base avec Claude + anti-détection
// LLM: Claude Sonnet (température 0.7) + Prompts adversariaux
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { createAdversarialPrompt } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - GÉNÉRATION INITIALE ADVERSARIALE
* Input: { content: {}, csvData: {}, context: {}, adversarialConfig: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function generateInitialContentAdversarial(input) {
return await tracer.run('AdversarialInitialGeneration.generateInitialContentAdversarial()', async () => {
const { hierarchy, csvData, context = {}, adversarialConfig = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true,
contextualMode: adversarialConfig.contextualMode !== false,
...adversarialConfig
};
// Initialiser manager détecteur
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
await tracer.annotate({
step: '1/4',
llmProvider: 'claude',
elementsCount: Object.keys(hierarchy).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 ÉTAPE 1/4 ADVERSARIAL: Génération initiale (Claude + ${config.detectorTarget})`, 'INFO');
logSh(` 📊 ${Object.keys(hierarchy).length} éléments à générer`, 'INFO');
try {
// Collecter tous les éléments dans l'ordre XML
const allElements = collectElementsInXMLOrder(hierarchy);
// Séparer FAQ pairs et autres éléments
const { faqPairs, otherElements } = separateElementTypes(allElements);
// Générer en chunks pour éviter timeouts
const results = {};
// 1. Générer éléments normaux avec prompts adversariaux
if (otherElements.length > 0) {
const normalResults = await generateNormalElementsAdversarial(otherElements, csvData, config, detectorManager);
Object.assign(results, normalResults);
}
// 2. Générer paires FAQ adversariales si présentes
if (faqPairs.length > 0) {
const faqResults = await generateFAQPairsAdversarial(faqPairs, csvData, config, detectorManager);
Object.assign(results, faqResults);
}
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(results).length,
generated: Object.keys(results).length,
faqPairs: faqPairs.length,
duration
};
logSh(`✅ ÉTAPE 1/4 TERMINÉE: ${stats.generated} éléments générés (${duration}ms)`, 'INFO');
await tracer.event(`Génération initiale terminée`, stats);
return {
content: results,
stats,
debug: {
llmProvider: 'claude',
step: 1,
elementsGenerated: Object.keys(results),
adversarialConfig: config,
detectorTarget: config.detectorTarget,
intensity: config.intensity
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 1/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`InitialGeneration failed: ${error.message}`);
}
}, input);
}
/**
* Générer éléments normaux avec prompts adversariaux en chunks
*/
async function generateNormalElementsAdversarial(elements, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Génération éléments normaux adversariaux: ${elements.length} éléments`, 'DEBUG');
const results = {};
const chunks = chunkArray(elements, 4); // Chunks de 4 pour éviter timeouts
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
try {
const basePrompt = createBatchPrompt(chunk, csvData);
// Générer prompt adversarial
const adversarialPrompt = createAdversarialPrompt(basePrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity,
elementType: getElementTypeFromChunk(chunk),
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const response = await callLLM('claude', adversarialPrompt, {
temperature: 0.7,
maxTokens: 2000 * chunk.length
}, csvData.personality);
const chunkResults = parseBatchResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} éléments générés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
throw error;
}
}
return results;
}
/**
* Générer paires FAQ adversariales cohérentes
*/
async function generateFAQPairsAdversarial(faqPairs, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Génération paires FAQ adversariales: ${faqPairs.length} paires`, 'DEBUG');
const basePrompt = createFAQPairsPrompt(faqPairs, csvData);
// Générer prompt adversarial spécialisé FAQ
const adversarialPrompt = createAdversarialPrompt(basePrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity * 1.1, // Intensité légèrement plus élevée pour FAQ
elementType: 'faq_mixed',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const response = await callLLM('claude', adversarialPrompt, {
temperature: 0.8,
maxTokens: 3000
}, csvData.personality);
return parseFAQResponse(response, faqPairs);
}
/**
* Créer prompt batch pour éléments normaux
*/
function createBatchPrompt(elements, csvData) {
const personality = csvData.personality;
let prompt = `=== GÉNÉRATION CONTENU INITIAL ===
Entreprise: Autocollant.fr - signalétique personnalisée
Sujet: ${csvData.mc0}
Rédacteur: ${personality.nom} (${personality.style})
ÉLÉMENTS À GÉNÉRER:
`;
elements.forEach((elementInfo, index) => {
const cleanTag = elementInfo.tag.replace(/\|/g, '');
prompt += `${index + 1}. [${cleanTag}] - ${getElementDescription(elementInfo)}\n`;
});
prompt += `
STYLE ${personality.nom.toUpperCase()}:
- Vocabulaire: ${personality.vocabulairePref}
- Phrases: ${personality.longueurPhrases}
- Niveau: ${personality.niveauTechnique}
CONSIGNES:
- Contenu SEO optimisé pour ${csvData.mc0}
- Style ${personality.style} naturel
- Pas de références techniques dans contenu
- RÉPONSE DIRECTE par le contenu
FORMAT:
[${elements[0].tag.replace(/\|/g, '')}]
Contenu généré...
[${elements[1] ? elements[1].tag.replace(/\|/g, '') : 'element2'}]
Contenu généré...`;
return prompt;
}
/**
* Parser réponse batch
*/
function parseBatchResponse(response, elements) {
const results = {};
const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = cleanGeneratedContent(match[2].trim());
parsedItems[tag] = content;
}
// Mapper aux vrais tags
elements.forEach(element => {
const cleanTag = element.tag.replace(/\|/g, '');
if (parsedItems[cleanTag] && parsedItems[cleanTag].length > 10) {
results[element.tag] = parsedItems[cleanTag];
} else {
results[element.tag] = `Contenu professionnel pour ${element.element.name || cleanTag}`;
logSh(`⚠️ Fallback pour [${cleanTag}]`, 'WARNING');
}
});
return results;
}
/**
* Créer prompt pour paires FAQ
*/
function createFAQPairsPrompt(faqPairs, csvData) {
const personality = csvData.personality;
let prompt = `=== GÉNÉRATION PAIRES FAQ ===
Sujet: ${csvData.mc0}
Rédacteur: ${personality.nom} (${personality.style})
PAIRES À GÉNÉRER:
`;
faqPairs.forEach((pair, index) => {
const qTag = pair.question.tag.replace(/\|/g, '');
const aTag = pair.answer.tag.replace(/\|/g, '');
prompt += `${index + 1}. [${qTag}] + [${aTag}]\n`;
});
prompt += `
CONSIGNES:
- Questions naturelles de clients
- Réponses expertes ${personality.style}
- Couvrir: prix, livraison, personnalisation
FORMAT:
[${faqPairs[0].question.tag.replace(/\|/g, '')}]
Question client naturelle ?
[${faqPairs[0].answer.tag.replace(/\|/g, '')}]
Réponse utile et rassurante.`;
return prompt;
}
/**
* Parser réponse FAQ
*/
function parseFAQResponse(response, faqPairs) {
const results = {};
const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = cleanGeneratedContent(match[2].trim());
parsedItems[tag] = content;
}
// Mapper aux paires FAQ
faqPairs.forEach(pair => {
const qCleanTag = pair.question.tag.replace(/\|/g, '');
const aCleanTag = pair.answer.tag.replace(/\|/g, '');
if (parsedItems[qCleanTag]) results[pair.question.tag] = parsedItems[qCleanTag];
if (parsedItems[aCleanTag]) results[pair.answer.tag] = parsedItems[aCleanTag];
});
return results;
}
// ============= HELPER FUNCTIONS =============
function collectElementsInXMLOrder(hierarchy) {
const allElements = [];
Object.keys(hierarchy).forEach(path => {
const section = hierarchy[path];
if (section.title) {
allElements.push({
tag: section.title.originalElement.originalTag,
element: section.title.originalElement,
type: section.title.originalElement.type
});
}
if (section.text) {
allElements.push({
tag: section.text.originalElement.originalTag,
element: section.text.originalElement,
type: section.text.originalElement.type
});
}
section.questions.forEach(q => {
allElements.push({
tag: q.originalElement.originalTag,
element: q.originalElement,
type: q.originalElement.type
});
});
});
return allElements;
}
function separateElementTypes(allElements) {
const faqPairs = [];
const otherElements = [];
const faqQuestions = {};
const faqAnswers = {};
// Collecter FAQ questions et answers
allElements.forEach(element => {
if (element.type === 'faq_question') {
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqQuestions[faqNumber] = element;
} else if (element.type === 'faq_reponse') {
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqAnswers[faqNumber] = element;
} else {
otherElements.push(element);
}
});
// Créer paires FAQ
Object.keys(faqQuestions).forEach(number => {
const question = faqQuestions[number];
const answer = faqAnswers[number];
if (question && answer) {
faqPairs.push({ number, question, answer });
} else if (question) {
otherElements.push(question);
} else if (answer) {
otherElements.push(answer);
}
});
return { faqPairs, otherElements };
}
function getElementDescription(elementInfo) {
switch (elementInfo.type) {
case 'titre_h1': return 'Titre principal accrocheur';
case 'titre_h2': return 'Titre de section';
case 'titre_h3': return 'Sous-titre';
case 'intro': return 'Introduction engageante';
case 'texte': return 'Paragraphe informatif';
default: return 'Contenu pertinent';
}
}
function cleanGeneratedContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?Titre_[HU]\d+_\d+[.,\s]*/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Helper: Déterminer type d'élément dominant dans un chunk
*/
function getElementTypeFromChunk(chunk) {
if (!chunk || chunk.length === 0) return 'generic';
// Compter les types dans le chunk
const typeCounts = {};
chunk.forEach(element => {
const type = element.type || 'generic';
typeCounts[type] = (typeCounts[type] || 0) + 1;
});
// Retourner type le plus fréquent
return Object.keys(typeCounts).reduce((a, b) =>
typeCounts[a] > typeCounts[b] ? a : b
);
}
module.exports = {
generateInitialContentAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL
generateNormalElementsAdversarial,
generateFAQPairsAdversarial,
createBatchPrompt,
parseBatchResponse,
collectElementsInXMLOrder,
separateElementTypes,
getElementTypeFromChunk
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialStyleEnhancement.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ÉTAPE 4: ENHANCEMENT STYLE PERSONNALITÉ ADVERSARIAL
// Responsabilité: Appliquer le style personnalité avec Mistral + anti-détection
// LLM: Mistral (température 0.8) + Prompts adversariaux
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { createAdversarialPrompt } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - ENHANCEMENT STYLE
* Input: { content: {}, csvData: {}, context: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function applyPersonalityStyleAdversarial(input) {
return await tracer.run('AdversarialStyleEnhancement.applyPersonalityStyleAdversarial()', async () => {
const { content, csvData, context = {}, adversarialConfig = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true,
contextualMode: adversarialConfig.contextualMode !== false,
...adversarialConfig
};
// Initialiser manager détecteur
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
await tracer.annotate({
step: '4/4',
llmProvider: 'mistral',
elementsCount: Object.keys(content).length,
personality: csvData.personality?.nom,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 ÉTAPE 4/4 ADVERSARIAL: Enhancement style ${csvData.personality?.nom} (Mistral + ${config.detectorTarget})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à styliser`, 'INFO');
try {
const personality = csvData.personality;
if (!personality) {
logSh(`⚠️ ÉTAPE 4/4: Aucune personnalité définie, style standard`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'mistral', step: 4, personalityApplied: 'none' }
};
}
// 1. Préparer éléments pour stylisation
const styleElements = prepareElementsForStyling(content);
// 2. Appliquer style en chunks avec prompts adversariaux
const styledResults = await applyStyleInChunksAdversarial(styleElements, csvData, config, detectorManager);
// 3. Merger résultats
const finalContent = { ...content };
let actuallyStyled = 0;
Object.keys(styledResults).forEach(tag => {
if (styledResults[tag] !== content[tag]) {
finalContent[tag] = styledResults[tag];
actuallyStyled++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyStyled,
personality: personality.nom,
duration
};
logSh(`✅ ÉTAPE 4/4 TERMINÉE: ${stats.enhanced} éléments stylisés ${personality.nom} (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement style terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'mistral',
step: 4,
personalityApplied: personality.nom,
styleCharacteristics: {
vocabulaire: personality.vocabulairePref,
connecteurs: personality.connecteursPref,
style: personality.style
},
adversarialConfig: config,
detectorTarget: config.detectorTarget,
intensity: config.intensity
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 4/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original si Mistral indisponible
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration },
debug: { llmProvider: 'mistral', step: 4, error: error.message, fallback: true }
};
}
}, input);
}
/**
* Préparer éléments pour stylisation
*/
function prepareElementsForStyling(content) {
const styleElements = [];
Object.keys(content).forEach(tag => {
const text = content[tag];
// Tous les éléments peuvent bénéficier d'adaptation personnalité
// Même les courts (titres) peuvent être adaptés au style
styleElements.push({
tag,
content: text,
priority: calculateStylePriority(text, tag)
});
});
// Trier par priorité (titres d'abord, puis textes longs)
styleElements.sort((a, b) => b.priority - a.priority);
return styleElements;
}
/**
* Calculer priorité de stylisation
*/
function calculateStylePriority(text, tag) {
let priority = 1.0;
// Titres = haute priorité (plus visible)
if (tag.includes('Titre') || tag.includes('H1') || tag.includes('H2')) {
priority += 0.5;
}
// Textes longs = priorité selon longueur
if (text.length > 200) {
priority += 0.3;
} else if (text.length > 100) {
priority += 0.2;
}
// Introduction = haute priorité
if (tag.includes('intro') || tag.includes('Introduction')) {
priority += 0.4;
}
return priority;
}
/**
* Appliquer style en chunks avec prompts adversariaux
*/
async function applyStyleInChunksAdversarial(styleElements, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Stylisation adversarial: ${styleElements.length} éléments selon ${csvData.personality.nom}`, 'DEBUG');
const results = {};
const chunks = chunkArray(styleElements, 8); // Chunks de 8 pour Mistral
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const basePrompt = createStylePrompt(chunk, csvData);
// Générer prompt adversarial pour stylisation
const adversarialPrompt = createAdversarialPrompt(basePrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity * 1.1, // Intensité plus élevée pour style (plus visible)
elementType: 'style_enhancement',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const styledResponse = await callLLM('mistral', adversarialPrompt, {
temperature: 0.8,
maxTokens: 3000
}, csvData.personality);
const chunkResults = parseStyleResponse(styledResponse, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} stylisés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: garder contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* Créer prompt de stylisation
*/
function createStylePrompt(chunk, csvData) {
const personality = csvData.personality;
let prompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}.
CONTEXTE: Article SEO e-commerce ${csvData.mc0}
PERSONNALITÉ: ${personality.nom}
DESCRIPTION: ${personality.description}
STYLE: ${personality.style} adapté web professionnel
VOCABULAIRE: ${personality.vocabulairePref}
CONNECTEURS: ${personality.connecteursPref}
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
LONGUEUR PHRASES: ${personality.longueurPhrases}
CONTENUS À STYLISER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} (Priorité: ${item.priority.toFixed(1)})
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS STYLISATION ${personality.nom.toUpperCase()}:
- Adapte le TON selon ${personality.style}
- Vocabulaire: ${personality.vocabulairePref}
- Connecteurs variés: ${personality.connecteursPref}
- Phrases: ${personality.longueurPhrases}
- Niveau: ${personality.niveauTechnique}
CONSIGNES STRICTES:
- GARDE le même contenu informatif et technique
- Adapte SEULEMENT ton, expressions, vocabulaire selon ${personality.nom}
- RESPECTE longueur approximative (±20%)
- ÉVITE répétitions excessives
- Style ${personality.nom} reconnaissable mais NATUREL web
- PAS de messages d'excuse
FORMAT RÉPONSE:
[1] Contenu stylisé selon ${personality.nom}
[2] Contenu stylisé selon ${personality.nom}
etc...`;
return prompt;
}
/**
* Parser réponse stylisation
*/
function parseStyleResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let styledContent = match[2].trim();
const element = chunk[index];
// Nettoyer le contenu stylisé
styledContent = cleanStyledContent(styledContent);
if (styledContent && styledContent.length > 10) {
results[element.tag] = styledContent;
logSh(`✅ Styled [${element.tag}]: "${styledContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: stylisation invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu stylisé
*/
function cleanStyledContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?voici\s+/gi, '');
content = content.replace(/^pour\s+ce\s+contenu[,\s]*/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
// Réduire répétitions excessives mais garder le style personnalité
content = content.replace(/(du coup[,\s]+){4,}/gi, 'du coup ');
content = content.replace(/(bon[,\s]+){4,}/gi, 'bon ');
content = content.replace(/(franchement[,\s]+){3,}/gi, 'franchement ');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
/**
* Obtenir instructions de style dynamiques
*/
function getPersonalityStyleInstructions(personality) {
if (!personality) return "Style professionnel standard";
return `STYLE ${personality.nom.toUpperCase()} (${personality.style}):
- Description: ${personality.description}
- Vocabulaire: ${personality.vocabulairePref || 'professionnel'}
- Connecteurs: ${personality.connecteursPref || 'par ailleurs, en effet'}
- Mots-clés: ${personality.motsClesSecteurs || 'technique, qualité'}
- Phrases: ${personality.longueurPhrases || 'Moyennes'}
- Niveau: ${personality.niveauTechnique || 'Accessible'}
- CTA: ${personality.ctaStyle || 'Professionnel'}`;
}
// ============= HELPER FUNCTIONS =============
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
applyPersonalityStyleAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL
prepareElementsForStyling,
calculateStylePriority,
applyStyleInChunksAdversarial,
createStylePrompt,
parseStyleResponse,
getPersonalityStyleInstructions
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialTechnicalEnhancem… │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ÉTAPE 2: ENHANCEMENT TECHNIQUE ADVERSARIAL
// Responsabilité: Améliorer la précision technique avec GPT-4 + anti-détection
// LLM: GPT-4o-mini (température 0.4) + Prompts adversariaux
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { createAdversarialPrompt } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - ENHANCEMENT TECHNIQUE ADVERSARIAL
* Input: { content: {}, csvData: {}, context: {}, adversarialConfig: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function enhanceTechnicalTermsAdversarial(input) {
return await tracer.run('AdversarialTechnicalEnhancement.enhanceTechnicalTermsAdversarial()', async () => {
const { content, csvData, context = {}, adversarialConfig = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true,
contextualMode: adversarialConfig.contextualMode !== false,
...adversarialConfig
};
// Initialiser manager détecteur
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
await tracer.annotate({
step: '2/4',
llmProvider: 'gpt4',
elementsCount: Object.keys(content).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 ÉTAPE 2/4 ADVERSARIAL: Enhancement technique (GPT-4 + ${config.detectorTarget})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'INFO');
try {
// 1. Analyser tous les éléments pour détecter termes techniques (adversarial)
const technicalAnalysis = await analyzeTechnicalTermsAdversarial(content, csvData, config, detectorManager);
// 2. Filter les éléments qui ont besoin d'enhancement
const elementsNeedingEnhancement = technicalAnalysis.filter(item => item.needsEnhancement);
logSh(` 📋 Analyse: ${elementsNeedingEnhancement.length}/${Object.keys(content).length} éléments nécessitent enhancement`, 'INFO');
if (elementsNeedingEnhancement.length === 0) {
logSh(`✅ ÉTAPE 2/4: Aucun enhancement nécessaire`, 'INFO');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'gpt4', step: 2, enhancementsApplied: [] }
};
}
// 3. Améliorer les éléments sélectionnés avec prompts adversariaux
const enhancedResults = await enhanceSelectedElementsAdversarial(elementsNeedingEnhancement, csvData, config, detectorManager);
// 4. Merger avec contenu original
const finalContent = { ...content };
let actuallyEnhanced = 0;
Object.keys(enhancedResults).forEach(tag => {
if (enhancedResults[tag] !== content[tag]) {
finalContent[tag] = enhancedResults[tag];
actuallyEnhanced++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyEnhanced,
candidate: elementsNeedingEnhancement.length,
duration
};
logSh(`✅ ÉTAPE 2/4 TERMINÉE: ${stats.enhanced} éléments améliorés (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement technique terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'gpt4',
step: 2,
enhancementsApplied: Object.keys(enhancedResults),
technicalTermsFound: elementsNeedingEnhancement.map(e => e.technicalTerms),
adversarialConfig: config,
detectorTarget: config.detectorTarget,
intensity: config.intensity
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 2/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`TechnicalEnhancement failed: ${error.message}`);
}
}, input);
}
/**
* Analyser tous les éléments pour détecter termes techniques (adversarial)
*/
async function analyzeTechnicalTermsAdversarial(content, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Analyse termes techniques adversarial batch`, 'DEBUG');
const contentEntries = Object.keys(content);
const analysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTENUS À ANALYSER:
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
CONTENU: "${content[tag]}"`).join('\n\n')}
CONSIGNES:
- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
- Évite mots génériques (qualité, service, pratique, personnalisé)
- Focus: matériaux, procédés, normes, dimensions, technologies
- Si aucun terme technique → "AUCUN"
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne
FORMAT RÉPONSE:
[1] dibond, impression UV OU AUCUN
[2] AUCUN
[3] aluminium, fraisage CNC OU AUCUN
etc...`;
try {
// Générer prompt adversarial pour analyse
const adversarialAnalysisPrompt = createAdversarialPrompt(analysisPrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity * 0.8, // Intensité modérée pour analyse
elementType: 'technical_analysis',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const analysisResponse = await callLLM('gpt4', adversarialAnalysisPrompt, {
temperature: 0.3,
maxTokens: 2000
}, csvData.personality);
return parseAnalysisResponse(analysisResponse, content, contentEntries);
} catch (error) {
logSh(`❌ Analyse termes techniques échouée: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Améliorer les éléments sélectionnés avec prompts adversariaux
*/
async function enhanceSelectedElementsAdversarial(elementsNeedingEnhancement, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Enhancement adversarial ${elementsNeedingEnhancement.length} éléments`, 'DEBUG');
const enhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces contenus.
CONTEXTE: ${csvData.mc0} - Secteur signalétique/impression
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style})
CONTENUS À AMÉLIORER:
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag}
CONTENU: "${item.content}"
TERMES TECHNIQUES: ${item.technicalTerms.join(', ')}`).join('\n\n')}
CONSIGNES:
- GARDE même longueur, structure et ton ${csvData.personality?.style}
- Intègre naturellement les termes techniques listés
- NE CHANGE PAS le fond du message
- Vocabulaire expert mais accessible
- Termes secteur: dibond, aluminium, impression UV, fraisage, PMMA
FORMAT RÉPONSE:
[1] Contenu avec amélioration technique
[2] Contenu avec amélioration technique
etc...`;
try {
// Générer prompt adversarial pour enhancement
const adversarialEnhancementPrompt = createAdversarialPrompt(enhancementPrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity,
elementType: 'technical_enhancement',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const enhancedResponse = await callLLM('gpt4', adversarialEnhancementPrompt, {
temperature: 0.4,
maxTokens: 5000
}, csvData.personality);
return parseEnhancementResponse(enhancedResponse, elementsNeedingEnhancement);
} catch (error) {
logSh(`❌ Enhancement éléments échoué: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Parser réponse analyse
*/
function parseAnalysisResponse(response, content, contentEntries) {
const results = [];
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const index = parseInt(match[1]) - 1;
const termsText = match[2].trim();
parsedItems[index] = termsText;
}
contentEntries.forEach((tag, index) => {
const termsText = parsedItems[index] || 'AUCUN';
const hasTerms = !termsText.toUpperCase().includes('AUCUN');
const technicalTerms = hasTerms ?
termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) :
[];
results.push({
tag,
content: content[tag],
technicalTerms,
needsEnhancement: hasTerms && technicalTerms.length > 0
});
logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'aucun terme technique'}`, 'DEBUG');
});
return results;
}
/**
* Parser réponse enhancement
*/
function parseEnhancementResponse(response, elementsNeedingEnhancement) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < elementsNeedingEnhancement.length) {
let enhancedContent = match[2].trim();
const element = elementsNeedingEnhancement[index];
// Nettoyer le contenu généré
enhancedContent = cleanEnhancedContent(enhancedContent);
if (enhancedContent && enhancedContent.length > 10) {
results[element.tag] = enhancedContent;
logSh(`✅ Enhanced [${element.tag}]: "${enhancedContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: contenu invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < elementsNeedingEnhancement.length) {
const element = elementsNeedingEnhancement[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu amélioré
*/
function cleanEnhancedContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?pour\s+/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
module.exports = {
enhanceTechnicalTermsAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL
analyzeTechnicalTermsAdversarial,
enhanceSelectedElementsAdversarial,
parseAnalysisResponse,
parseEnhancementResponse
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialTransitionEnhance… │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ÉTAPE 3: ENHANCEMENT TRANSITIONS ADVERSARIAL
// Responsabilité: Améliorer la fluidité avec Gemini + anti-détection
// LLM: Gemini (température 0.6) + Prompts adversariaux
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { createAdversarialPrompt } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - ENHANCEMENT TRANSITIONS ADVERSARIAL
* Input: { content: {}, csvData: {}, context: {}, adversarialConfig: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function enhanceTransitionsAdversarial(input) {
return await tracer.run('AdversarialTransitionEnhancement.enhanceTransitionsAdversarial()', async () => {
const { content, csvData, context = {}, adversarialConfig = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true,
contextualMode: adversarialConfig.contextualMode !== false,
...adversarialConfig
};
// Initialiser manager détecteur
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
await tracer.annotate({
step: '3/4',
llmProvider: 'gemini',
elementsCount: Object.keys(content).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 ÉTAPE 3/4 ADVERSARIAL: Enhancement transitions (Gemini + ${config.detectorTarget})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'INFO');
try {
// 1. Analyser quels éléments ont besoin d'amélioration transitions
const elementsNeedingTransitions = analyzeTransitionNeeds(content);
logSh(` 📋 Analyse: ${elementsNeedingTransitions.length}/${Object.keys(content).length} éléments nécessitent fluidité`, 'INFO');
if (elementsNeedingTransitions.length === 0) {
logSh(`✅ ÉTAPE 3/4: Transitions déjà optimales`, 'INFO');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'gemini', step: 3, enhancementsApplied: [] }
};
}
// 2. Améliorer en chunks avec prompts adversariaux pour Gemini
const improvedResults = await improveTransitionsInChunksAdversarial(elementsNeedingTransitions, csvData, config, detectorManager);
// 3. Merger avec contenu original
const finalContent = { ...content };
let actuallyImproved = 0;
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
finalContent[tag] = improvedResults[tag];
actuallyImproved++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyImproved,
candidate: elementsNeedingTransitions.length,
duration
};
logSh(`✅ ÉTAPE 3/4 TERMINÉE: ${stats.enhanced} éléments fluidifiés (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement transitions terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'gemini',
step: 3,
enhancementsApplied: Object.keys(improvedResults),
transitionIssues: elementsNeedingTransitions.map(e => e.issues),
adversarialConfig: config,
detectorTarget: config.detectorTarget,
intensity: config.intensity
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 3/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original si Gemini indisponible
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration },
debug: { llmProvider: 'gemini', step: 3, error: error.message, fallback: true }
};
}
}, input);
}
/**
* Analyser besoin d'amélioration transitions
*/
function analyzeTransitionNeeds(content) {
const elementsNeedingTransitions = [];
Object.keys(content).forEach(tag => {
const text = content[tag];
// Filtrer les éléments longs (>150 chars) qui peuvent bénéficier d'améliorations
if (text.length > 150) {
const needsTransitions = evaluateTransitionQuality(text);
if (needsTransitions.needsImprovement) {
elementsNeedingTransitions.push({
tag,
content: text,
issues: needsTransitions.issues,
score: needsTransitions.score
});
logSh(` 🔍 [${tag}]: Score=${needsTransitions.score.toFixed(2)}, Issues: ${needsTransitions.issues.join(', ')}`, 'DEBUG');
}
} else {
logSh(` ⏭️ [${tag}]: Trop court (${text.length}c), ignoré`, 'DEBUG');
}
});
// Trier par score (plus problématique en premier)
elementsNeedingTransitions.sort((a, b) => a.score - b.score);
return elementsNeedingTransitions;
}
/**
* Évaluer qualité transitions d'un texte
*/
function evaluateTransitionQuality(text) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) {
return { needsImprovement: false, score: 1.0, issues: [] };
}
const issues = [];
let score = 1.0; // Score parfait = 1.0, problématique = 0.0
// Analyse 1: Connecteurs répétitifs
const repetitiveConnectors = analyzeRepetitiveConnectors(text);
if (repetitiveConnectors > 0.3) {
issues.push('connecteurs_répétitifs');
score -= 0.3;
}
// Analyse 2: Transitions abruptes
const abruptTransitions = analyzeAbruptTransitions(sentences);
if (abruptTransitions > 0.4) {
issues.push('transitions_abruptes');
score -= 0.4;
}
// Analyse 3: Manque de variété dans longueurs
const sentenceVariety = analyzeSentenceVariety(sentences);
if (sentenceVariety < 0.3) {
issues.push('phrases_uniformes');
score -= 0.2;
}
// Analyse 4: Trop formel ou trop familier
const formalityIssues = analyzeFormalityBalance(text);
if (formalityIssues > 0.5) {
issues.push('formalité_déséquilibrée');
score -= 0.1;
}
return {
needsImprovement: score < 0.6,
score: Math.max(0, score),
issues
};
}
/**
* Améliorer transitions en chunks avec prompts adversariaux
*/
async function improveTransitionsInChunksAdversarial(elementsNeedingTransitions, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Amélioration transitions adversarial: ${elementsNeedingTransitions.length} éléments`, 'DEBUG');
const results = {};
const chunks = chunkArray(elementsNeedingTransitions, 6); // Chunks plus petits pour Gemini
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const basePrompt = createTransitionImprovementPrompt(chunk, csvData);
// Générer prompt adversarial pour amélioration transitions
const adversarialPrompt = createAdversarialPrompt(basePrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity * 0.9, // Intensité légèrement réduite pour transitions
elementType: 'transition_enhancement',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const improvedResponse = await callLLM('gemini', adversarialPrompt, {
temperature: 0.6,
maxTokens: 2500
}, csvData.personality);
const chunkResults = parseTransitionResponse(improvedResponse, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} améliorés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: garder contenu original pour ce chunk
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* Créer prompt amélioration transitions
*/
function createTransitionImprovementPrompt(chunk, csvData) {
const personality = csvData.personality;
let prompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.
CONTEXTE: Article SEO ${csvData.mc0}
PERSONNALITÉ: ${personality?.nom} (${personality?.style} web professionnel)
CONNECTEURS PRÉFÉRÉS: ${personality?.connecteursPref}
CONTENUS À FLUIDIFIER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
PROBLÈMES: ${item.issues.join(', ')}
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS:
- Connecteurs plus naturels et variés: ${personality?.connecteursPref}
- Transitions fluides entre idées
- ÉVITE répétitions excessives ("du coup", "franchement", "par ailleurs")
- Style ${personality?.style} mais professionnel web
CONSIGNES STRICTES:
- NE CHANGE PAS le fond du message
- GARDE même structure et longueur
- Améliore SEULEMENT la fluidité
- RESPECTE le style ${personality?.nom}
FORMAT RÉPONSE:
[1] Contenu avec transitions améliorées
[2] Contenu avec transitions améliorées
etc...`;
return prompt;
}
/**
* Parser réponse amélioration transitions
*/
function parseTransitionResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let improvedContent = match[2].trim();
const element = chunk[index];
// Nettoyer le contenu amélioré
improvedContent = cleanImprovedContent(improvedContent);
if (improvedContent && improvedContent.length > 10) {
results[element.tag] = improvedContent;
logSh(`✅ Improved [${element.tag}]: "${improvedContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
// ============= HELPER FUNCTIONS =============
function analyzeRepetitiveConnectors(content) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc'];
let totalConnectors = 0;
let repetitions = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
totalConnectors += matches.length;
if (matches.length > 1) repetitions += matches.length - 1;
});
return totalConnectors > 0 ? repetitions / totalConnectors : 0;
}
function analyzeAbruptTransitions(sentences) {
if (sentences.length < 2) return 0;
let abruptCount = 0;
for (let i = 1; i < sentences.length; i++) {
const current = sentences[i].trim();
const hasConnector = hasTransitionWord(current);
if (!hasConnector && current.length > 30) {
abruptCount++;
}
}
return abruptCount / (sentences.length - 1);
}
function analyzeSentenceVariety(sentences) {
if (sentences.length < 2) return 1;
const lengths = sentences.map(s => s.trim().length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
return Math.min(1, stdDev / avgLength);
}
function analyzeFormalityBalance(content) {
const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois'];
const casualIndicators = ['du coup', 'bon', 'franchement', 'nickel'];
let formalCount = 0;
let casualCount = 0;
formalIndicators.forEach(indicator => {
if (content.toLowerCase().includes(indicator)) formalCount++;
});
casualIndicators.forEach(indicator => {
if (content.toLowerCase().includes(indicator)) casualCount++;
});
const total = formalCount + casualCount;
if (total === 0) return 0;
// Déséquilibre si trop d'un côté
const balance = Math.abs(formalCount - casualCount) / total;
return balance;
}
function hasTransitionWord(sentence) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite', 'puis', 'également', 'aussi'];
return connectors.some(connector => sentence.toLowerCase().includes(connector));
}
function cleanImprovedContent(content) {
if (!content) return content;
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?/, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
enhanceTransitionsAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL
analyzeTransitionNeeds,
evaluateTransitionQuality,
improveTransitionsInChunksAdversarial,
createTransitionImprovementPrompt,
parseTransitionResponse
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialUtils.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ADVERSARIAL UTILS - UTILITAIRES MODULAIRES
// Responsabilité: Fonctions utilitaires partagées par tous les modules adversariaux
// Architecture: Helper functions réutilisables et composables
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* ANALYSEURS DE CONTENU
*/
/**
* Analyser score de diversité lexicale
*/
function analyzeLexicalDiversity(content) {
if (!content || typeof content !== 'string') return 0;
const words = content.toLowerCase()
.split(/\s+/)
.filter(word => word.length > 2)
.map(word => word.replace(/[^\w]/g, ''));
if (words.length === 0) return 0;
const uniqueWords = [...new Set(words)];
return (uniqueWords.length / words.length) * 100;
}
/**
* Analyser variation des longueurs de phrases
*/
function analyzeSentenceVariation(content) {
if (!content || typeof content !== 'string') return 0;
const sentences = content.split(/[.!?]+/)
.map(s => s.trim())
.filter(s => s.length > 5);
if (sentences.length < 2) return 0;
const lengths = sentences.map(s => s.split(/\s+/).length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
return Math.min(100, (stdDev / avgLength) * 100);
}
/**
* Détecter mots typiques IA
*/
function detectAIFingerprints(content) {
const aiFingerprints = {
words: ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge', 'state-of-the-art', 'furthermore', 'moreover'],
phrases: ['it is important to note', 'it should be noted', 'it is worth mentioning', 'in conclusion', 'to summarize'],
connectors: ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc']
};
const results = {
words: 0,
phrases: 0,
connectors: 0,
totalScore: 0
};
const lowerContent = content.toLowerCase();
// Compter mots IA
aiFingerprints.words.forEach(word => {
const matches = (lowerContent.match(new RegExp(`\\b${word}\\b`, 'g')) || []);
results.words += matches.length;
});
// Compter phrases typiques
aiFingerprints.phrases.forEach(phrase => {
if (lowerContent.includes(phrase)) {
results.phrases += 1;
}
});
// Compter connecteurs répétitifs
aiFingerprints.connectors.forEach(connector => {
const matches = (lowerContent.match(new RegExp(`\\b${connector}\\b`, 'g')) || []);
if (matches.length > 1) {
results.connectors += matches.length - 1; // Pénalité répétition
}
});
// Score total (sur 100)
const wordCount = content.split(/\s+/).length;
results.totalScore = Math.min(100,
(results.words * 5 + results.phrases * 10 + results.connectors * 3) / Math.max(wordCount, 1) * 100
);
return results;
}
/**
* Analyser uniformité structurelle
*/
function analyzeStructuralUniformity(content) {
const sentences = content.split(/[.!?]+/)
.map(s => s.trim())
.filter(s => s.length > 5);
if (sentences.length < 3) return 0;
const structures = sentences.map(sentence => {
const words = sentence.split(/\s+/);
return {
length: words.length,
startsWithConnector: /^(par ailleurs|en effet|de plus|cependant|ainsi|donc|ensuite|puis)/i.test(sentence),
hasComma: sentence.includes(','),
hasSubordinate: /qui|que|dont|où|quand|comme|parce que|puisque|bien que/i.test(sentence)
};
});
// Calculer uniformité
const avgLength = structures.reduce((sum, s) => sum + s.length, 0) / structures.length;
const lengthVariance = structures.reduce((sum, s) => sum + Math.pow(s.length - avgLength, 2), 0) / structures.length;
const connectorRatio = structures.filter(s => s.startsWithConnector).length / structures.length;
const commaRatio = structures.filter(s => s.hasComma).length / structures.length;
// Plus c'est uniforme, plus le score est élevé (mauvais pour anti-détection)
const uniformityScore = 100 - (Math.sqrt(lengthVariance) / avgLength * 100) -
(Math.abs(0.3 - connectorRatio) * 50) - (Math.abs(0.5 - commaRatio) * 30);
return Math.max(0, Math.min(100, uniformityScore));
}
/**
* COMPARATEURS DE CONTENU
*/
/**
* Comparer deux contenus et calculer taux de modification
*/
function compareContentModification(original, modified) {
if (!original || !modified) return 0;
const originalWords = original.toLowerCase().split(/\s+/).filter(w => w.length > 2);
const modifiedWords = modified.toLowerCase().split(/\s+/).filter(w => w.length > 2);
// Calcul de distance Levenshtein approximative (par mots)
let changes = 0;
const maxLength = Math.max(originalWords.length, modifiedWords.length);
for (let i = 0; i < maxLength; i++) {
if (originalWords[i] !== modifiedWords[i]) {
changes++;
}
}
return (changes / maxLength) * 100;
}
/**
* Évaluer amélioration adversariale
*/
function evaluateAdversarialImprovement(original, modified, detectorTarget = 'general') {
const originalFingerprints = detectAIFingerprints(original);
const modifiedFingerprints = detectAIFingerprints(modified);
const originalDiversity = analyzeLexicalDiversity(original);
const modifiedDiversity = analyzeLexicalDiversity(modified);
const originalVariation = analyzeSentenceVariation(original);
const modifiedVariation = analyzeSentenceVariation(modified);
const fingerprintReduction = originalFingerprints.totalScore - modifiedFingerprints.totalScore;
const diversityIncrease = modifiedDiversity - originalDiversity;
const variationIncrease = modifiedVariation - originalVariation;
const improvementScore = (
fingerprintReduction * 0.4 +
diversityIncrease * 0.3 +
variationIncrease * 0.3
);
return {
fingerprintReduction,
diversityIncrease,
variationIncrease,
improvementScore: Math.round(improvementScore * 100) / 100,
modificationRate: compareContentModification(original, modified),
recommendation: getImprovementRecommendation(improvementScore, detectorTarget)
};
}
/**
* UTILITAIRES DE CONTENU
*/
/**
* Nettoyer contenu adversarial généré
*/
function cleanAdversarialContent(content) {
if (!content || typeof content !== 'string') return content;
let cleaned = content;
// Supprimer préfixes de génération
cleaned = cleaned.replace(/^(voici\s+)?le\s+contenu\s+(réécrit|amélioré|modifié)[:\s]*/gi, '');
cleaned = cleaned.replace(/^(bon,?\s*)?(alors,?\s*)?(pour\s+)?(ce\s+contenu[,\s]*)?/gi, '');
// Nettoyer formatage
cleaned = cleaned.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
cleaned = cleaned.replace(/\s{2,}/g, ' '); // Espaces multiples
cleaned = cleaned.replace(/([.!?])\s*([.!?])/g, '$1 '); // Double ponctuation
// Nettoyer début/fin
cleaned = cleaned.trim();
cleaned = cleaned.replace(/^[,.\s]+/, '');
cleaned = cleaned.replace(/[,\s]+$/, '');
return cleaned;
}
/**
* Valider qualité du contenu adversarial
*/
function validateAdversarialContent(content, originalContent, minLength = 10, maxModificationRate = 90) {
const validation = {
isValid: true,
issues: [],
suggestions: []
};
// Vérifier longueur minimale
if (!content || content.length < minLength) {
validation.isValid = false;
validation.issues.push('Contenu trop court');
validation.suggestions.push('Augmenter la longueur du contenu généré');
}
// Vérifier cohérence
if (originalContent) {
const modificationRate = compareContentModification(originalContent, content);
if (modificationRate > maxModificationRate) {
validation.issues.push('Modification trop importante');
validation.suggestions.push('Réduire l\'intensité adversariale pour préserver le sens');
}
if (modificationRate < 5) {
validation.issues.push('Modification insuffisante');
validation.suggestions.push('Augmenter l\'intensité adversariale');
}
}
// Vérifier empreintes IA résiduelles
const fingerprints = detectAIFingerprints(content);
if (fingerprints.totalScore > 15) {
validation.issues.push('Empreintes IA encore présentes');
validation.suggestions.push('Appliquer post-processing anti-fingerprints');
}
return validation;
}
/**
* UTILITAIRES TECHNIQUES
*/
/**
* Chunk array avec préservation des paires
*/
function chunkArraySmart(array, size, preservePairs = false) {
if (!preservePairs) {
return chunkArray(array, size);
}
const chunks = [];
for (let i = 0; i < array.length; i += size) {
let chunk = array.slice(i, i + size);
// Si on coupe au milieu d'une paire (nombre impair), ajuster
if (chunk.length % 2 !== 0 && i + size < array.length) {
chunk = array.slice(i, i + size - 1);
}
chunks.push(chunk);
}
return chunks;
}
/**
* Chunk array standard
*/
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
/**
* Sleep avec variation
*/
function sleep(ms, variation = 0.2) {
const actualMs = ms + (Math.random() - 0.5) * ms * variation;
return new Promise(resolve => setTimeout(resolve, Math.max(100, actualMs)));
}
/**
* RECOMMANDATIONS
*/
/**
* Obtenir recommandation d'amélioration
*/
function getImprovementRecommendation(score, detectorTarget) {
const recommendations = {
general: {
good: "Bon niveau d'amélioration générale",
medium: "Appliquer techniques de variation syntaxique",
poor: "Nécessite post-processing intensif"
},
gptZero: {
good: "Imprévisibilité suffisante contre GPTZero",
medium: "Ajouter plus de ruptures narratives",
poor: "Intensifier variation syntaxique et lexicale"
},
originality: {
good: "Créativité suffisante contre Originality",
medium: "Enrichir diversité sémantique",
poor: "Réinventer présentation des informations"
}
};
const category = score > 10 ? 'good' : score > 5 ? 'medium' : 'poor';
return recommendations[detectorTarget]?.[category] || recommendations.general[category];
}
/**
* MÉTRIQUES ET STATS
*/
/**
* Calculer score composite anti-détection
*/
function calculateAntiDetectionScore(content, detectorTarget = 'general') {
const diversity = analyzeLexicalDiversity(content);
const variation = analyzeSentenceVariation(content);
const fingerprints = detectAIFingerprints(content);
const uniformity = analyzeStructuralUniformity(content);
const baseScore = (diversity * 0.3 + variation * 0.3 + (100 - fingerprints.totalScore) * 0.2 + (100 - uniformity) * 0.2);
// Ajustements selon détecteur
let adjustedScore = baseScore;
switch (detectorTarget) {
case 'gptZero':
adjustedScore = baseScore * (variation / 100) * 1.2; // Favorise variation
break;
case 'originality':
adjustedScore = baseScore * (diversity / 100) * 1.2; // Favorise diversité
break;
}
return Math.min(100, Math.max(0, Math.round(adjustedScore)));
}
module.exports = {
// Analyseurs
analyzeLexicalDiversity,
analyzeSentenceVariation,
detectAIFingerprints,
analyzeStructuralUniformity,
// Comparateurs
compareContentModification,
evaluateAdversarialImprovement,
// Utilitaires contenu
cleanAdversarialContent,
validateAdversarialContent,
// Utilitaires techniques
chunkArray,
chunkArraySmart,
sleep,
// Métriques
calculateAntiDetectionScore,
getImprovementRecommendation
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/ContentGenerationAdversarial.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ORCHESTRATEUR CONTENU ADVERSARIAL - NIVEAU 3
// Responsabilité: Pipeline complet de génération anti-détection
// Architecture: 4 étapes adversariales séparées et modulaires
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
// Importation des 4 étapes adversariales
const { generateInitialContentAdversarial } = require('./AdversarialInitialGeneration');
const { enhanceTechnicalTermsAdversarial } = require('./AdversarialTechnicalEnhancement');
const { enhanceTransitionsAdversarial } = require('./AdversarialTransitionEnhancement');
const { applyPersonalityStyleAdversarial } = require('./AdversarialStyleEnhancement');
// Importation du moteur adversarial
const { createAdversarialPrompt, getSupportedDetectors, analyzePromptEffectiveness } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - PIPELINE ADVERSARIAL COMPLET
* Input: { hierarchy, csvData, adversarialConfig, context }
* Output: { content, stats, debug, adversarialMetrics }
*/
async function generateWithAdversarialContext(input) {
return await tracer.run('ContentGenerationAdversarial.generateWithAdversarialContext()', async () => {
const { hierarchy, csvData, adversarialConfig = {}, context = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy !== false,
contextualMode: adversarialConfig.contextualMode !== false,
enableAllSteps: adversarialConfig.enableAllSteps !== false,
// Configuration par étape
steps: {
initial: adversarialConfig.steps?.initial !== false,
technical: adversarialConfig.steps?.technical !== false,
transitions: adversarialConfig.steps?.transitions !== false,
style: adversarialConfig.steps?.style !== false
},
...adversarialConfig
};
await tracer.annotate({
adversarialPipeline: true,
detectorTarget: config.detectorTarget,
intensity: config.intensity,
enabledSteps: Object.keys(config.steps).filter(k => config.steps[k]),
elementsCount: Object.keys(hierarchy).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 PIPELINE ADVERSARIAL NIVEAU 3: Anti-détection ${config.detectorTarget}`, 'INFO');
logSh(` 🎚️ Intensité: ${config.intensity.toFixed(2)} | Étapes: ${Object.keys(config.steps).filter(k => config.steps[k]).join(', ')}`, 'INFO');
// Initialiser manager détecteur global
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
try {
let currentContent = {};
let pipelineStats = {
steps: {},
totalDuration: 0,
elementsProcessed: 0,
adversarialMetrics: {
promptsGenerated: 0,
detectorTarget: config.detectorTarget,
averageIntensity: config.intensity,
effectivenessScore: 0
}
};
// ========================================
// ÉTAPE 1: GÉNÉRATION INITIALE ADVERSARIALE
// ========================================
if (config.steps.initial) {
logSh(`🎯 ÉTAPE 1/4: Génération initiale adversariale`, 'INFO');
const step1Result = await generateInitialContentAdversarial({
hierarchy,
csvData,
context,
adversarialConfig: config
});
currentContent = step1Result.content;
pipelineStats.steps.initial = step1Result.stats;
pipelineStats.adversarialMetrics.promptsGenerated += Object.keys(currentContent).length;
logSh(`✅ ÉTAPE 1/4: ${step1Result.stats.generated} éléments générés (${step1Result.stats.duration}ms)`, 'INFO');
} else {
logSh(`⏭️ ÉTAPE 1/4: Ignorée (configuration)`, 'INFO');
}
// ========================================
// ÉTAPE 2: ENHANCEMENT TECHNIQUE ADVERSARIAL
// ========================================
if (config.steps.technical && Object.keys(currentContent).length > 0) {
logSh(`🎯 ÉTAPE 2/4: Enhancement technique adversarial`, 'INFO');
const step2Result = await enhanceTechnicalTermsAdversarial({
content: currentContent,
csvData,
context,
adversarialConfig: config
});
currentContent = step2Result.content;
pipelineStats.steps.technical = step2Result.stats;
pipelineStats.adversarialMetrics.promptsGenerated += step2Result.stats.enhanced;
logSh(`✅ ÉTAPE 2/4: ${step2Result.stats.enhanced} éléments améliorés (${step2Result.stats.duration}ms)`, 'INFO');
} else {
logSh(`⏭️ ÉTAPE 2/4: Ignorée (configuration ou pas de contenu)`, 'INFO');
}
// ========================================
// ÉTAPE 3: ENHANCEMENT TRANSITIONS ADVERSARIAL
// ========================================
if (config.steps.transitions && Object.keys(currentContent).length > 0) {
logSh(`🎯 ÉTAPE 3/4: Enhancement transitions adversarial`, 'INFO');
const step3Result = await enhanceTransitionsAdversarial({
content: currentContent,
csvData,
context,
adversarialConfig: config
});
currentContent = step3Result.content;
pipelineStats.steps.transitions = step3Result.stats;
pipelineStats.adversarialMetrics.promptsGenerated += step3Result.stats.enhanced;
logSh(`✅ ÉTAPE 3/4: ${step3Result.stats.enhanced} éléments fluidifiés (${step3Result.stats.duration}ms)`, 'INFO');
} else {
logSh(`⏭️ ÉTAPE 3/4: Ignorée (configuration ou pas de contenu)`, 'INFO');
}
// ========================================
// ÉTAPE 4: ENHANCEMENT STYLE ADVERSARIAL
// ========================================
if (config.steps.style && Object.keys(currentContent).length > 0 && csvData.personality) {
logSh(`🎯 ÉTAPE 4/4: Enhancement style adversarial`, 'INFO');
const step4Result = await applyPersonalityStyleAdversarial({
content: currentContent,
csvData,
context,
adversarialConfig: config
});
currentContent = step4Result.content;
pipelineStats.steps.style = step4Result.stats;
pipelineStats.adversarialMetrics.promptsGenerated += step4Result.stats.enhanced;
logSh(`✅ ÉTAPE 4/4: ${step4Result.stats.enhanced} éléments stylisés (${step4Result.stats.duration}ms)`, 'INFO');
} else {
logSh(`⏭️ ÉTAPE 4/4: Ignorée (configuration, pas de contenu ou pas de personnalité)`, 'INFO');
}
// ========================================
// FINALISATION PIPELINE
// ========================================
const totalDuration = Date.now() - startTime;
pipelineStats.totalDuration = totalDuration;
pipelineStats.elementsProcessed = Object.keys(currentContent).length;
// Calculer score d'efficacité adversarial
pipelineStats.adversarialMetrics.effectivenessScore = calculateAdversarialEffectiveness(
pipelineStats,
config,
currentContent
);
logSh(`🎯 PIPELINE ADVERSARIAL TERMINÉ: ${pipelineStats.elementsProcessed} éléments (${totalDuration}ms)`, 'INFO');
logSh(` 📊 Score efficacité: ${pipelineStats.adversarialMetrics.effectivenessScore.toFixed(2)}%`, 'INFO');
await tracer.event(`Pipeline adversarial terminé`, {
...pipelineStats,
detectorTarget: config.detectorTarget,
intensity: config.intensity
});
return {
content: currentContent,
stats: pipelineStats,
debug: {
adversarialPipeline: true,
detectorTarget: config.detectorTarget,
intensity: config.intensity,
stepsExecuted: Object.keys(config.steps).filter(k => config.steps[k]),
detectorManager: detectorManager.getStrategyInfo()
},
adversarialMetrics: pipelineStats.adversarialMetrics
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ PIPELINE ADVERSARIAL ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`AdversarialContentGeneration failed: ${error.message}`);
}
}, input);
}
/**
* MODE SIMPLE ADVERSARIAL (équivalent à generateSimple mais adversarial)
*/
async function generateSimpleAdversarial(hierarchy, csvData, adversarialConfig = {}) {
return await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget: 'general',
intensity: 0.8,
enableAllSteps: false,
steps: {
initial: true,
technical: false,
transitions: false,
style: true
},
...adversarialConfig
}
});
}
/**
* MODE AVANCÉ ADVERSARIAL (configuration personnalisée)
*/
async function generateAdvancedAdversarial(hierarchy, csvData, options = {}) {
const {
detectorTarget = 'general',
intensity = 1.0,
technical = true,
transitions = true,
style = true,
...otherConfig
} = options;
return await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget,
intensity,
enableAdaptiveStrategy: true,
contextualMode: true,
steps: {
initial: true,
technical,
transitions,
style
},
...otherConfig
}
});
}
/**
* DIAGNOSTIC PIPELINE ADVERSARIAL
*/
async function diagnosticAdversarialPipeline(hierarchy, csvData, detectorTargets = ['general', 'gptZero', 'originality']) {
logSh(`🔬 DIAGNOSTIC ADVERSARIAL: Testing ${detectorTargets.length} détecteurs`, 'INFO');
const results = {};
for (const target of detectorTargets) {
try {
logSh(` 🎯 Test détecteur: ${target}`, 'DEBUG');
const result = await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget: target,
intensity: 1.0,
enableAllSteps: true
}
});
results[target] = {
success: true,
content: result.content,
stats: result.stats,
effectivenessScore: result.adversarialMetrics.effectivenessScore
};
logSh(`${target}: Score ${result.adversarialMetrics.effectivenessScore.toFixed(2)}%`, 'DEBUG');
} catch (error) {
results[target] = {
success: false,
error: error.message,
effectivenessScore: 0
};
logSh(`${target}: Échec - ${error.message}`, 'ERROR');
}
}
return results;
}
// ============= HELPER FUNCTIONS =============
/**
* Calculer efficacité adversariale
*/
function calculateAdversarialEffectiveness(pipelineStats, config, content) {
let effectiveness = 0;
// Base score selon intensité
effectiveness += config.intensity * 30;
// Bonus selon nombre d'étapes
const stepsExecuted = Object.keys(config.steps).filter(k => config.steps[k]).length;
effectiveness += stepsExecuted * 10;
// Bonus selon prompts adversariaux générés
const promptRatio = pipelineStats.adversarialMetrics.promptsGenerated / Math.max(1, pipelineStats.elementsProcessed);
effectiveness += promptRatio * 20;
// Analyse contenu si disponible
if (Object.keys(content).length > 0) {
const contentSample = Object.values(content).join(' ').substring(0, 1000);
const diversityScore = analyzeDiversityScore(contentSample);
effectiveness += diversityScore * 0.3;
}
return Math.min(100, Math.max(0, effectiveness));
}
/**
* Analyser score de diversité
*/
function analyzeDiversityScore(content) {
if (!content || typeof content !== 'string') return 0;
const words = content.split(/\s+/).filter(w => w.length > 2);
if (words.length === 0) return 0;
const uniqueWords = [...new Set(words.map(w => w.toLowerCase()))];
const diversityRatio = uniqueWords.length / words.length;
return diversityRatio * 100;
}
/**
* Obtenir informations détecteurs supportés
*/
function getAdversarialDetectorInfo() {
return getSupportedDetectors();
}
/**
* Comparer efficacité de différents détecteurs
*/
async function compareAdversarialStrategies(hierarchy, csvData, detectorTargets = ['general', 'gptZero', 'originality', 'winston']) {
const results = await diagnosticAdversarialPipeline(hierarchy, csvData, detectorTargets);
const comparison = {
bestStrategy: null,
bestScore: 0,
strategies: [],
averageScore: 0
};
let totalScore = 0;
let successCount = 0;
detectorTargets.forEach(target => {
const result = results[target];
if (result.success) {
const strategyInfo = {
detector: target,
effectivenessScore: result.effectivenessScore,
duration: result.stats.totalDuration,
elementsProcessed: result.stats.elementsProcessed
};
comparison.strategies.push(strategyInfo);
totalScore += result.effectivenessScore;
successCount++;
if (result.effectivenessScore > comparison.bestScore) {
comparison.bestStrategy = target;
comparison.bestScore = result.effectivenessScore;
}
}
});
comparison.averageScore = successCount > 0 ? totalScore / successCount : 0;
return comparison;
}
module.exports = {
generateWithAdversarialContext, // ← MAIN ENTRY POINT
generateSimpleAdversarial,
generateAdvancedAdversarial,
diagnosticAdversarialPipeline,
compareAdversarialStrategies,
getAdversarialDetectorInfo,
calculateAdversarialEffectiveness
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/ComparisonFramework.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FRAMEWORK DE COMPARAISON ADVERSARIAL
// Responsabilité: Comparer pipelines normales vs adversariales
// Utilisation: A/B testing et validation efficacité anti-détection
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
// Pipelines à comparer
const { generateSimple } = require('../selective-enhancement/SelectiveUtils'); // Pipeline normale
const { generateWithAdversarialContext, compareAdversarialStrategies } = require('./ContentGenerationAdversarial'); // Pipeline adversariale
/**
* MAIN ENTRY POINT - COMPARAISON A/B PIPELINE
* Compare pipeline normale vs adversariale sur même input
*/
async function compareNormalVsAdversarial(input, options = {}) {
return await tracer.run('ComparisonFramework.compareNormalVsAdversarial()', async () => {
const {
hierarchy,
csvData,
adversarialConfig = {},
runBothPipelines = true,
analyzeContent = true
} = input;
const {
detectorTarget = 'general',
intensity = 1.0,
iterations = 1
} = options;
await tracer.annotate({
comparisonType: 'normal_vs_adversarial',
detectorTarget,
intensity,
iterations,
elementsCount: Object.keys(hierarchy).length
});
const startTime = Date.now();
logSh(`🆚 COMPARAISON A/B: Pipeline normale vs adversariale`, 'INFO');
logSh(` 🎯 Détecteur cible: ${detectorTarget} | Intensité: ${intensity} | Itérations: ${iterations}`, 'INFO');
const results = {
normal: null,
adversarial: null,
comparison: null,
iterations: []
};
try {
for (let i = 0; i < iterations; i++) {
logSh(`🔄 Itération ${i + 1}/${iterations}`, 'INFO');
const iterationResults = {
iteration: i + 1,
normal: null,
adversarial: null,
metrics: {}
};
// ========================================
// PIPELINE NORMALE
// ========================================
if (runBothPipelines) {
logSh(` 📊 Génération pipeline normale...`, 'DEBUG');
const normalStartTime = Date.now();
try {
const normalResult = await generateSimple(hierarchy, csvData);
iterationResults.normal = {
success: true,
content: normalResult,
duration: Date.now() - normalStartTime,
elementsCount: Object.keys(normalResult).length
};
logSh(` ✅ Pipeline normale: ${iterationResults.normal.elementsCount} éléments (${iterationResults.normal.duration}ms)`, 'DEBUG');
} catch (error) {
iterationResults.normal = {
success: false,
error: error.message,
duration: Date.now() - normalStartTime
};
logSh(` ❌ Pipeline normale échouée: ${error.message}`, 'ERROR');
}
}
// ========================================
// PIPELINE ADVERSARIALE
// ========================================
logSh(` 🎯 Génération pipeline adversariale...`, 'DEBUG');
const adversarialStartTime = Date.now();
try {
const adversarialResult = await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget,
intensity,
enableAllSteps: true,
...adversarialConfig
}
});
iterationResults.adversarial = {
success: true,
content: adversarialResult.content,
stats: adversarialResult.stats,
adversarialMetrics: adversarialResult.adversarialMetrics,
duration: Date.now() - adversarialStartTime,
elementsCount: Object.keys(adversarialResult.content).length
};
logSh(` ✅ Pipeline adversariale: ${iterationResults.adversarial.elementsCount} éléments (${iterationResults.adversarial.duration}ms)`, 'DEBUG');
logSh(` 📊 Score efficacité: ${adversarialResult.adversarialMetrics.effectivenessScore.toFixed(2)}%`, 'DEBUG');
} catch (error) {
iterationResults.adversarial = {
success: false,
error: error.message,
duration: Date.now() - adversarialStartTime
};
logSh(` ❌ Pipeline adversariale échouée: ${error.message}`, 'ERROR');
}
// ========================================
// ANALYSE COMPARATIVE ITÉRATION
// ========================================
if (analyzeContent && iterationResults.normal?.success && iterationResults.adversarial?.success) {
iterationResults.metrics = analyzeContentComparison(
iterationResults.normal.content,
iterationResults.adversarial.content
);
logSh(` 📈 Diversité: Normal=${iterationResults.metrics.diversity.normal.toFixed(2)}% | Adversarial=${iterationResults.metrics.diversity.adversarial.toFixed(2)}%`, 'DEBUG');
}
results.iterations.push(iterationResults);
}
// ========================================
// CONSOLIDATION RÉSULTATS
// ========================================
const totalDuration = Date.now() - startTime;
// Prendre les meilleurs résultats ou derniers si une seule itération
const lastIteration = results.iterations[results.iterations.length - 1];
results.normal = lastIteration.normal;
results.adversarial = lastIteration.adversarial;
// Analyse comparative globale
results.comparison = generateGlobalComparison(results.iterations, options);
logSh(`🆚 COMPARAISON TERMINÉE: ${iterations} itérations (${totalDuration}ms)`, 'INFO');
if (results.comparison.winner) {
logSh(`🏆 Gagnant: ${results.comparison.winner} (score: ${results.comparison.bestScore.toFixed(2)})`, 'INFO');
}
await tracer.event('Comparaison A/B terminée', {
iterations,
winner: results.comparison.winner,
totalDuration
});
return results;
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ COMPARAISON A/B ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`ComparisonFramework failed: ${error.message}`);
}
}, input);
}
/**
* COMPARAISON MULTI-DÉTECTEURS
*/
async function compareMultiDetectors(hierarchy, csvData, detectorTargets = ['general', 'gptZero', 'originality']) {
logSh(`🎯 COMPARAISON MULTI-DÉTECTEURS: ${detectorTargets.length} stratégies`, 'INFO');
const results = {};
const startTime = Date.now();
for (const detector of detectorTargets) {
logSh(` 🔍 Test détecteur: ${detector}`, 'DEBUG');
try {
const comparison = await compareNormalVsAdversarial({
hierarchy,
csvData,
adversarialConfig: { detectorTarget: detector }
}, {
detectorTarget: detector,
intensity: 1.0,
iterations: 1
});
results[detector] = {
success: true,
comparison,
effectivenessGain: comparison.adversarial?.adversarialMetrics?.effectivenessScore || 0
};
logSh(`${detector}: +${results[detector].effectivenessGain.toFixed(2)}% efficacité`, 'DEBUG');
} catch (error) {
results[detector] = {
success: false,
error: error.message,
effectivenessGain: 0
};
logSh(`${detector}: Échec - ${error.message}`, 'ERROR');
}
}
// Analyse du meilleur détecteur
const bestDetector = Object.keys(results).reduce((best, current) => {
if (!results[best]?.success) return current;
if (!results[current]?.success) return best;
return results[current].effectivenessGain > results[best].effectivenessGain ? current : best;
});
const totalDuration = Date.now() - startTime;
logSh(`🎯 MULTI-DÉTECTEURS TERMINÉ: Meilleur=${bestDetector} (${totalDuration}ms)`, 'INFO');
return {
results,
bestDetector,
bestScore: results[bestDetector]?.effectivenessGain || 0,
totalDuration
};
}
/**
* BENCHMARK PERFORMANCE
*/
async function benchmarkPerformance(hierarchy, csvData, configurations = []) {
const defaultConfigs = [
{ name: 'Normal', type: 'normal' },
{ name: 'Simple Adversarial', type: 'adversarial', detectorTarget: 'general', intensity: 0.5 },
{ name: 'Intense Adversarial', type: 'adversarial', detectorTarget: 'gptZero', intensity: 1.0 },
{ name: 'Max Adversarial', type: 'adversarial', detectorTarget: 'originality', intensity: 1.5 }
];
const configs = configurations.length > 0 ? configurations : defaultConfigs;
logSh(`⚡ BENCHMARK PERFORMANCE: ${configs.length} configurations`, 'INFO');
const results = [];
for (const config of configs) {
logSh(` 🔧 Test: ${config.name}`, 'DEBUG');
const startTime = Date.now();
try {
let result;
if (config.type === 'normal') {
result = await generateSimple(hierarchy, csvData);
} else {
const adversarialResult = await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget: config.detectorTarget || 'general',
intensity: config.intensity || 1.0
}
});
result = adversarialResult.content;
}
const duration = Date.now() - startTime;
results.push({
name: config.name,
type: config.type,
success: true,
duration,
elementsCount: Object.keys(result).length,
performance: Object.keys(result).length / (duration / 1000) // éléments par seconde
});
logSh(`${config.name}: ${Object.keys(result).length} éléments (${duration}ms)`, 'DEBUG');
} catch (error) {
results.push({
name: config.name,
type: config.type,
success: false,
error: error.message,
duration: Date.now() - startTime
});
logSh(`${config.name}: Échec - ${error.message}`, 'ERROR');
}
}
// Analyser les résultats
const successfulResults = results.filter(r => r.success);
const fastest = successfulResults.reduce((best, current) =>
current.duration < best.duration ? current : best, successfulResults[0]);
const mostEfficient = successfulResults.reduce((best, current) =>
current.performance > best.performance ? current : best, successfulResults[0]);
logSh(`⚡ BENCHMARK TERMINÉ: Fastest=${fastest?.name} | Most efficient=${mostEfficient?.name}`, 'INFO');
return {
results,
fastest,
mostEfficient,
summary: {
totalConfigs: configs.length,
successful: successfulResults.length,
failed: results.length - successfulResults.length
}
};
}
// ============= HELPER FUNCTIONS =============
/**
* Analyser différences de contenu entre normal et adversarial
*/
function analyzeContentComparison(normalContent, adversarialContent) {
const metrics = {
diversity: {
normal: analyzeDiversityScore(Object.values(normalContent).join(' ')),
adversarial: analyzeDiversityScore(Object.values(adversarialContent).join(' '))
},
length: {
normal: Object.values(normalContent).join(' ').length,
adversarial: Object.values(adversarialContent).join(' ').length
},
elementsCount: {
normal: Object.keys(normalContent).length,
adversarial: Object.keys(adversarialContent).length
},
differences: compareContentElements(normalContent, adversarialContent)
};
return metrics;
}
/**
* Score de diversité lexicale
*/
function analyzeDiversityScore(content) {
if (!content || typeof content !== 'string') return 0;
const words = content.split(/\s+/).filter(w => w.length > 2);
if (words.length === 0) return 0;
const uniqueWords = [...new Set(words.map(w => w.toLowerCase()))];
return (uniqueWords.length / words.length) * 100;
}
/**
* Comparer éléments de contenu
*/
function compareContentElements(normalContent, adversarialContent) {
const differences = {
modified: 0,
identical: 0,
totalElements: Math.max(Object.keys(normalContent).length, Object.keys(adversarialContent).length)
};
const allTags = [...new Set([...Object.keys(normalContent), ...Object.keys(adversarialContent)])];
allTags.forEach(tag => {
if (normalContent[tag] && adversarialContent[tag]) {
if (normalContent[tag] === adversarialContent[tag]) {
differences.identical++;
} else {
differences.modified++;
}
}
});
differences.modificationRate = differences.totalElements > 0 ?
(differences.modified / differences.totalElements) * 100 : 0;
return differences;
}
/**
* Générer analyse comparative globale
*/
function generateGlobalComparison(iterations, options) {
const successfulIterations = iterations.filter(it =>
it.normal?.success && it.adversarial?.success);
if (successfulIterations.length === 0) {
return {
winner: null,
bestScore: 0,
summary: 'Aucune itération réussie'
};
}
// Moyenner les métriques
const avgMetrics = {
diversity: {
normal: 0,
adversarial: 0
},
performance: {
normal: 0,
adversarial: 0
}
};
successfulIterations.forEach(iteration => {
if (iteration.metrics) {
avgMetrics.diversity.normal += iteration.metrics.diversity.normal;
avgMetrics.diversity.adversarial += iteration.metrics.diversity.adversarial;
}
avgMetrics.performance.normal += iteration.normal.elementsCount / (iteration.normal.duration / 1000);
avgMetrics.performance.adversarial += iteration.adversarial.elementsCount / (iteration.adversarial.duration / 1000);
});
const iterCount = successfulIterations.length;
avgMetrics.diversity.normal /= iterCount;
avgMetrics.diversity.adversarial /= iterCount;
avgMetrics.performance.normal /= iterCount;
avgMetrics.performance.adversarial /= iterCount;
// Déterminer le gagnant
const diversityGain = avgMetrics.diversity.adversarial - avgMetrics.diversity.normal;
const performanceLoss = avgMetrics.performance.normal - avgMetrics.performance.adversarial;
// Score composite (favorise diversité avec pénalité performance)
const adversarialScore = diversityGain * 2 - (performanceLoss * 0.5);
return {
winner: adversarialScore > 5 ? 'adversarial' : 'normal',
bestScore: Math.max(avgMetrics.diversity.normal, avgMetrics.diversity.adversarial),
diversityGain,
performanceLoss,
avgMetrics,
summary: `Diversité: +${diversityGain.toFixed(2)}%, Performance: ${performanceLoss > 0 ? '-' : '+'}${Math.abs(performanceLoss).toFixed(2)} elem/s`
};
}
module.exports = {
compareNormalVsAdversarial, // ← MAIN ENTRY POINT
compareMultiDetectors,
benchmarkPerformance,
analyzeContentComparison,
analyzeDiversityScore
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/demo-modulaire.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// DÉMONSTRATION ARCHITECTURE MODULAIRE
// Usage: node lib/adversarial-generation/demo-modulaire.js
// Objectif: Valider l'intégration modulaire adversariale
// ========================================
const { logSh } = require('../ErrorReporting');
// Import modules adversariaux modulaires
const { applyAdversarialLayer } = require('./AdversarialCore');
const {
applyPredefinedStack,
applyAdaptiveLayers,
getAvailableStacks
} = require('./AdversarialLayers');
const { calculateAntiDetectionScore, evaluateAdversarialImprovement } = require('./AdversarialUtils');
/**
* EXEMPLE D'UTILISATION MODULAIRE
*/
async function demoModularAdversarial() {
console.log('\n🎯 === DÉMONSTRATION ADVERSARIAL MODULAIRE ===\n');
// Contenu d'exemple (simulé contenu généré normal)
const exempleContenu = {
'|Titre_Principal_1|': 'Guide complet pour choisir votre plaque personnalisée',
'|Introduction_1|': 'La personnalisation d\'une plaque signalétique représente un enjeu optimal pour votre entreprise. Cette solution comprehensive permet de créer une identité visuelle robuste et seamless.',
'|Texte_1|': 'Il est important de noter que les matériaux utilisés sont cutting-edge. Par ailleurs, la qualité est optimal. En effet, nos solutions sont comprehensive et robust.',
'|FAQ_Question_1|': 'Quels sont les matériaux disponibles ?',
'|FAQ_Reponse_1|': 'Nos matériaux sont optimal : dibond, aluminium, PMMA. Ces solutions comprehensive garantissent une qualité robust et seamless.'
};
console.log('📊 CONTENU ORIGINAL:');
Object.entries(exempleContenu).forEach(([tag, content]) => {
console.log(` ${tag}: "${content.substring(0, 60)}..."`);
});
// Analyser contenu original
const scoreOriginal = calculateAntiDetectionScore(Object.values(exempleContenu).join(' '));
console.log(`\n📈 Score anti-détection original: ${scoreOriginal}/100`);
try {
// ========================================
// TEST 1: COUCHE SIMPLE
// ========================================
console.log('\n🔧 TEST 1: Application couche adversariale simple');
const result1 = await applyAdversarialLayer(exempleContenu, {
detectorTarget: 'general',
intensity: 0.8,
method: 'enhancement'
});
console.log(`✅ Résultat: ${result1.stats.elementsModified}/${result1.stats.elementsProcessed} éléments modifiés`);
const scoreAmeliore = calculateAntiDetectionScore(Object.values(result1.content).join(' '));
console.log(`📈 Score anti-détection amélioré: ${scoreAmeliore}/100 (+${scoreAmeliore - scoreOriginal})`);
// ========================================
// TEST 2: STACK PRÉDÉFINI
// ========================================
console.log('\n📦 TEST 2: Application stack prédéfini');
// Lister stacks disponibles
const stacks = getAvailableStacks();
console.log(' Stacks disponibles:');
stacks.forEach(stack => {
console.log(` - ${stack.name}: ${stack.description} (${stack.layersCount} couches)`);
});
const result2 = await applyPredefinedStack(exempleContenu, 'standardDefense', {
csvData: {
personality: { nom: 'Marc', style: 'technique' },
mc0: 'plaque personnalisée'
}
});
console.log(`✅ Stack standard: ${result2.stats.totalModifications} modifications totales`);
console.log(` 📊 Couches appliquées: ${result2.stats.layers.filter(l => l.success).length}/${result2.stats.layers.length}`);
const scoreStack = calculateAntiDetectionScore(Object.values(result2.content).join(' '));
console.log(`📈 Score anti-détection stack: ${scoreStack}/100 (+${scoreStack - scoreOriginal})`);
// ========================================
// TEST 3: COUCHES ADAPTATIVES
// ========================================
console.log('\n🧠 TEST 3: Application couches adaptatives');
const result3 = await applyAdaptiveLayers(exempleContenu, {
targetDetectors: ['gptZero', 'originality'],
maxIntensity: 1.2
});
if (result3.stats.adaptive) {
console.log(`✅ Adaptatif: ${result3.stats.layersApplied || result3.stats.totalModifications} modifications`);
const scoreAdaptatif = calculateAntiDetectionScore(Object.values(result3.content).join(' '));
console.log(`📈 Score anti-détection adaptatif: ${scoreAdaptatif}/100 (+${scoreAdaptatif - scoreOriginal})`);
}
// ========================================
// COMPARAISON FINALE
// ========================================
console.log('\n📊 COMPARAISON FINALE:');
const evaluation = evaluateAdversarialImprovement(
Object.values(exempleContenu).join(' '),
Object.values(result2.content).join(' '),
'general'
);
console.log(` 🔹 Réduction empreintes IA: ${evaluation.fingerprintReduction.toFixed(2)}%`);
console.log(` 🔹 Augmentation diversité: ${evaluation.diversityIncrease.toFixed(2)}%`);
console.log(` 🔹 Amélioration variation: ${evaluation.variationIncrease.toFixed(2)}%`);
console.log(` 🔹 Score amélioration global: ${evaluation.improvementScore}`);
console.log(` 🔹 Taux modification: ${evaluation.modificationRate.toFixed(2)}%`);
console.log(` 💡 Recommandation: ${evaluation.recommendation}`);
// ========================================
// EXEMPLES DE CONTENU TRANSFORMÉ
// ========================================
console.log('\n✨ EXEMPLES DE TRANSFORMATION:');
const exempleTransforme = result2.content['|Introduction_1|'] || result1.content['|Introduction_1|'];
console.log('\n📝 AVANT:');
console.log(` "${exempleContenu['|Introduction_1|']}"`);
console.log('\n📝 APRÈS:');
console.log(` "${exempleTransforme}"`);
console.log('\n✅ === DÉMONSTRATION MODULAIRE TERMINÉE ===\n');
return {
success: true,
originalScore: scoreOriginal,
improvedScore: Math.max(scoreAmeliore, scoreStack),
improvement: evaluation.improvementScore
};
} catch (error) {
console.error('\n❌ ERREUR DÉMONSTRATION:', error.message);
return { success: false, error: error.message };
}
}
/**
* EXEMPLE D'INTÉGRATION AVEC PIPELINE NORMALE
*/
async function demoIntegrationPipeline() {
console.log('\n🔗 === DÉMONSTRATION INTÉGRATION PIPELINE ===\n');
// Simuler résultat pipeline normale (Level 1)
const contenuNormal = {
'|Titre_H1_1|': 'Solutions de plaques personnalisées professionnelles',
'|Intro_1|': 'Notre expertise en signalétique permet de créer des plaques sur mesure adaptées à vos besoins spécifiques.',
'|Texte_1|': 'Les matériaux proposés incluent l\'aluminium, le dibond et le PMMA. Chaque solution présente des avantages particuliers selon l\'usage prévu.'
};
console.log('💼 SCÉNARIO: Application adversarial post-pipeline normale');
try {
// Exemple Level 6 - Post-processing adversarial
console.log('\n🎯 Étape 1: Contenu généré par pipeline normale');
console.log(' ✅ Contenu de base: qualité préservée');
console.log('\n🎯 Étape 2: Application couche adversariale modulaire');
const resultAdversarial = await applyAdversarialLayer(contenuNormal, {
detectorTarget: 'gptZero',
intensity: 0.9,
method: 'hybrid',
preserveStructure: true
});
console.log(` ✅ Couche adversariale: ${resultAdversarial.stats.elementsModified} éléments modifiés`);
console.log('\n📊 RÉSULTAT FINAL:');
Object.entries(resultAdversarial.content).forEach(([tag, content]) => {
console.log(` ${tag}:`);
console.log(` AVANT: "${contenuNormal[tag]}"`);
console.log(` APRÈS: "${content}"`);
console.log('');
});
return { success: true, result: resultAdversarial };
} catch (error) {
console.error('❌ ERREUR INTÉGRATION:', error.message);
return { success: false, error: error.message };
}
}
// Exécuter démonstrations si fichier appelé directement
if (require.main === module) {
(async () => {
await demoModularAdversarial();
await demoIntegrationPipeline();
})().catch(console.error);
}
module.exports = {
demoModularAdversarial,
demoIntegrationPipeline
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/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 `
<!DOCTYPE html>
<html>
<head>
<title>SEO Generator - Mode AUTO</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); min-height: 100vh; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); }
.header { text-align: center; margin-bottom: 40px; }
.header h1 { color: #2d3748; margin-bottom: 10px; }
.mode-badge { background: #4299e1; color: white; padding: 8px 16px; border-radius: 20px; font-weight: bold; }
.progress { background: #e2e8f0; height: 20px; border-radius: 10px; margin: 20px 0; overflow: hidden; }
.progress-bar { background: linear-gradient(90deg, #4facfe, #00f2fe); height: 100%; width: ${progress}%; transition: width 0.3s; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 40px; }
.stat-card { background: #f7fafc; padding: 20px; border-radius: 10px; text-align: center; border: 1px solid #e2e8f0; }
.stat-card.processing { background: #fef5e7; border-color: #f6ad55; }
.stat-card.completed { background: #f0fff4; border-color: #48bb78; }
.stat-card.failed { background: #fed7d7; border-color: #f56565; }
.stat-number { font-size: 2em; font-weight: bold; color: #2d3748; }
.stat-label { color: #718096; margin-top: 5px; }
.section { margin: 30px 0; padding: 25px; border: 1px solid #e2e8f0; border-radius: 10px; background: #f9f9f9; }
.button { display: inline-block; padding: 12px 24px; margin: 8px; background: linear-gradient(135deg, #4facfe, #00f2fe); color: white; text-decoration: none; border-radius: 8px; border: none; cursor: pointer; font-weight: 500; }
.alert { padding: 15px; margin: 20px 0; border-radius: 8px; border-left: 4px solid; }
.alert.info { background: #ebf8ff; border-color: #4299e1; color: #2a4365; }
.alert.warning { background: #fefcbf; border-color: #f6ad55; color: #744210; }
.current-item { background: #e6fffa; padding: 15px; border-radius: 8px; border: 1px solid #38b2ac; margin: 15px 0; }
</style>
<script>
function refreshPage() { window.location.reload(); }
function pauseProcessing() {
fetch('/api/pause', { method: 'POST' })
.then(() => setTimeout(refreshPage, 1000));
}
function resumeProcessing() {
fetch('/api/resume', { method: 'POST' })
.then(() => setTimeout(refreshPage, 1000));
}
setInterval(refreshPage, 30000); // Auto-refresh 30s
</script>
</head>
<body>
<div class="container">
<div class="header">
<h1>🤖 SEO Generator Server</h1>
<span class="mode-badge">MODE AUTO</span>
<p style="color: #718096; margin-top: 15px;">Traitement Automatique Google Sheets</p>
</div>
<div class="alert info">
<strong>🤖 Mode AUTO Actif</strong><br>
Traitement batch des Google Sheets • Interface monitoring lecture seule
</div>
<div class="progress">
<div class="progress-bar"></div>
</div>
<div style="text-align: center; margin-bottom: 20px; color: #4a5568;">
Progression: ${progress}% (${completedCount}/${this.stats.itemsQueued})
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-number">${uptime}s</div>
<div class="stat-label">Uptime</div>
</div>
<div class="stat-card">
<div class="stat-number">${pendingCount}</div>
<div class="stat-label">En Attente</div>
</div>
<div class="stat-card processing">
<div class="stat-number">${this.state.isProcessing ? '1' : '0'}</div>
<div class="stat-label">En Traitement</div>
</div>
<div class="stat-card completed">
<div class="stat-number">${completedCount}</div>
<div class="stat-label">Terminés</div>
</div>
<div class="stat-card failed">
<div class="stat-number">${failedCount}</div>
<div class="stat-label">Échecs</div>
</div>
<div class="stat-card">
<div class="stat-number">${this.stats.averageProcessingTime}ms</div>
<div class="stat-label">Temps Moyen</div>
</div>
</div>
${this.state.currentItem ? `
<div class="current-item">
<strong>🎯 Traitement en cours:</strong><br>
Ligne ${this.state.currentItem.rowNumber}: ${this.state.currentItem.data.mc0}<br>
<small>Tentative ${this.state.currentItem.attempts}/${this.config.maxRetries}</small>
</div>
` : ''}
<div class="section">
<h2>🎛️ Contrôles</h2>
${this.state.isPaused ?
'<button class="button" onclick="resumeProcessing()">▶️ Reprendre</button>' :
'<button class="button" onclick="pauseProcessing()">⏸️ Pause</button>'
}
<button class="button" onclick="refreshPage()">🔄 Actualiser</button>
<a href="/api/stats" target="_blank" class="button">📊 Stats JSON</a>
</div>
<div class="section">
<h2>📋 Configuration</h2>
<ul style="color: #4a5568; line-height: 1.6;">
<li><strong>Batch Size:</strong> ${this.config.batchSize} éléments</li>
<li><strong>Délai entre éléments:</strong> ${this.config.delayBetweenItems}ms</li>
<li><strong>Délai entre batches:</strong> ${this.config.delayBetweenBatches}ms</li>
<li><strong>Max Retries:</strong> ${this.config.maxRetries}</li>
<li><strong>Mode Auto:</strong> ${this.config.autoMode}</li>
<li><strong>Lignes:</strong> ${this.config.startRow} - ${this.config.endRow || '∞'}</li>
</ul>
</div>
</div>
</body>
</html>
`;
}
// ========================================
// 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/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');
/**
* 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;
}
// ========================================
// 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);
});
// 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()
});
}
}
// ========================================
// 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 `
<!DOCTYPE html>
<html>
<head>
<title>SEO Generator - Mode MANUAL</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); }
.header { text-align: center; margin-bottom: 40px; }
.header h1 { color: #2d3748; margin-bottom: 10px; }
.mode-badge { background: #48bb78; color: white; padding: 8px 16px; border-radius: 20px; font-weight: bold; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 40px; }
.stat-card { background: #f7fafc; padding: 20px; border-radius: 10px; text-align: center; border: 1px solid #e2e8f0; }
.stat-number { font-size: 2em; font-weight: bold; color: #2d3748; }
.stat-label { color: #718096; margin-top: 5px; }
.section { margin: 30px 0; padding: 25px; border: 1px solid #e2e8f0; border-radius: 10px; background: #f9f9f9; }
.section h2 { color: #2d3748; margin-bottom: 15px; }
.button { display: inline-block; padding: 12px 24px; margin: 8px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; text-decoration: none; border-radius: 8px; border: none; cursor: pointer; font-weight: 500; transition: all 0.2s; }
.button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); }
.button.success { background: linear-gradient(135deg, #48bb78, #38a169); }
.button.warning { background: linear-gradient(135deg, #ed8936, #dd6b20); }
.alert { padding: 15px; margin: 20px 0; border-radius: 8px; border-left: 4px solid; }
.alert.success { background: #f0fff4; border-color: #48bb78; color: #22543d; }
.alert.info { background: #ebf8ff; border-color: #4299e1; color: #2a4365; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎯 SEO Generator Server</h1>
<span class="mode-badge">MODE MANUAL</span>
<p style="color: #718096; margin-top: 15px;">Interface Client + API + Tests Modulaires</p>
</div>
<div class="alert success">
<strong>✅ Mode MANUAL Actif</strong><br>
Interface complète disponible • WebSocket temps réel • API complète
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-number">${uptime}s</div>
<div class="stat-label">Uptime</div>
</div>
<div class="stat-card">
<div class="stat-number">${this.stats.requests}</div>
<div class="stat-label">Requêtes</div>
</div>
<div class="stat-card">
<div class="stat-number">${this.activeClients.size}</div>
<div class="stat-label">Clients WebSocket</div>
</div>
<div class="stat-card">
<div class="stat-number">${this.stats.testsExecuted}</div>
<div class="stat-label">Tests Exécutés</div>
</div>
</div>
<div class="section">
<h2>🧪 Interface Test Modulaire</h2>
<p>Interface avancée pour tester toutes les combinaisons modulaires avec logs temps réel.</p>
<a href="/test-modulaire.html" target="_blank" class="button">🚀 Ouvrir Interface Test</a>
<a href="/step-by-step" target="_blank" class="button">⚡ Interface Step-by-Step</a>
<a href="/api/modulaire-config" target="_blank" class="button success">📋 Configuration API</a>
</div>
<div class="section">
<h2>📊 Monitoring & API</h2>
<p>Endpoints disponibles en mode MANUAL.</p>
<a href="/api/status" target="_blank" class="button">📊 Status API</a>
<a href="/api/stats" target="_blank" class="button">📈 Statistiques</a>
<button onclick="testConnection()" class="button success">🔍 Test Connexion</button>
</div>
<div class="section">
<h2>🌐 WebSocket Logs</h2>
<p>Logs temps réel sur <strong>ws://localhost:${this.config.wsPort}</strong></p>
<button onclick="startLogViewer()" class="button warning">🔍 Ouvrir Log Viewer</button>
<div id="wsStatus" style="margin-top: 10px; padding: 10px; background: #e2e8f0; border-radius: 5px;">
Status: <span id="wsStatusText">Déconnecté</span>
</div>
</div>
<div class="section">
<h2>💡 Informations Mode MANUAL</h2>
<ul style="color: #4a5568; line-height: 1.6;">
<li><strong>Interface Client</strong> : Dashboard complet et interface de test</li>
<li><strong>API Complète</strong> : Tests individuels, benchmarks, configuration</li>
<li><strong>WebSocket</strong> : Logs temps réel sur port ${this.config.wsPort}</li>
<li><strong>Multi-Client</strong> : Plusieurs utilisateurs simultanés</li>
<li><strong>Pas de GSheets</strong> : Données test simulées ou fournies</li>
</ul>
</div>
</div>
<script>
let ws = null;
function testConnection() {
fetch('/api/status')
.then(res => res.json())
.then(data => {
alert('✅ Connexion OK: ' + (data.mode || 'Mode MANUAL actif') + ' - Uptime: ' + Math.floor(data.uptime/1000) + 's');
})
.catch(err => {
alert('❌ Erreur connexion: ' + err.message);
});
}
function startLogViewer() {
fetch('/api/start-log-viewer', { method: 'POST' })
.then(res => res.json())
.then(data => {
if (data.success) {
alert('✅ Log Viewer lancé! WebSocket sur port ' + data.wsPort);
// Reconnecter le WebSocket pour voir les logs
connectWebSocket();
} else {
alert('❌ Erreur: ' + data.message);
}
})
.catch(err => {
alert('❌ Erreur lancement: ' + err.message);
});
}
let reconnectAttempts = 0;
const maxReconnectDelay = 30000; // 30s max
function connectWebSocket() {
try {
ws = new WebSocket('ws://localhost:${this.config.wsPort}');
ws.onopen = () => {
reconnectAttempts = 0; // Reset compteur
document.getElementById('wsStatusText').textContent = 'Connecté ✅';
document.getElementById('wsStatus').style.background = '#c6f6d5';
};
ws.onclose = () => {
document.getElementById('wsStatusText').textContent = 'Déconnecté ❌';
document.getElementById('wsStatus').style.background = '#fed7d7';
// Backoff exponentiel pour éviter spam
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), maxReconnectDelay);
console.log('WebSocket fermé, reconnexion dans ' + (delay/1000) + 's (tentative ' + reconnectAttempts + ')');
setTimeout(connectWebSocket, delay);
};
} catch (error) {
console.warn('WebSocket non disponible:', error.message);
// Retry avec backoff en cas d'erreur de connexion
reconnectAttempts++;
const delay = Math.min(5000 * reconnectAttempts, maxReconnectDelay);
setTimeout(connectWebSocket, delay);
}
}
// Auto-connect WebSocket
connectWebSocket();
// Refresh stats every 30s
setInterval(() => {
window.location.reload();
}, 30000);
</script>
</body>
</html>
`;
}
// ========================================
// 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 };
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/post-processing/LLMFingerprintRemoval.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// PATTERN BREAKING - TECHNIQUE 2: LLM FINGERPRINT REMOVAL
// Responsabilité: Remplacer mots/expressions typiques des LLMs
// Anti-détection: Éviter vocabulaire détectable par les analyseurs IA
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* DICTIONNAIRE ANTI-DÉTECTION
* Mots/expressions LLM → Alternatives humaines naturelles
*/
const LLM_FINGERPRINTS = {
// Mots techniques/corporate typiques IA
'optimal': ['idéal', 'parfait', 'adapté', 'approprié', 'convenable'],
'optimale': ['idéale', 'parfaite', 'adaptée', 'appropriée', 'convenable'],
'comprehensive': ['complet', 'détaillé', 'exhaustif', 'approfondi', 'global'],
'seamless': ['fluide', 'naturel', 'sans accroc', 'harmonieux', 'lisse'],
'robust': ['solide', 'fiable', 'résistant', 'costaud', 'stable'],
'robuste': ['solide', 'fiable', 'résistant', 'costaud', 'stable'],
// Expressions trop formelles/IA
'il convient de noter': ['on remarque', 'il faut savoir', 'à noter', 'important'],
'il convient de': ['il faut', 'on doit', 'mieux vaut', 'il est bon de'],
'par conséquent': ['du coup', 'donc', 'résultat', 'ainsi'],
'néanmoins': ['cependant', 'mais', 'pourtant', 'malgré tout'],
'toutefois': ['cependant', 'mais', 'pourtant', 'quand même'],
'de surcroît': ['de plus', 'en plus', 'aussi', 'également'],
// Superlatifs excessifs typiques IA
'extrêmement': ['très', 'super', 'vraiment', 'particulièrement'],
'particulièrement': ['très', 'vraiment', 'spécialement', 'surtout'],
'remarquablement': ['très', 'vraiment', 'sacrément', 'fichement'],
'exceptionnellement': ['très', 'vraiment', 'super', 'incroyablement'],
// Mots de liaison trop mécaniques
'en définitive': ['au final', 'finalement', 'bref', 'en gros'],
'il s\'avère que': ['on voit que', 'il se trouve que', 'en fait'],
'force est de constater': ['on constate', 'on voit bien', 'c\'est clair'],
// Expressions commerciales robotiques
'solution innovante': ['nouveauté', 'innovation', 'solution moderne', 'nouvelle approche'],
'approche holistique': ['approche globale', 'vision d\'ensemble', 'approche complète'],
'expérience utilisateur': ['confort d\'utilisation', 'facilité d\'usage', 'ergonomie'],
'retour sur investissement': ['rentabilité', 'bénéfices', 'profits'],
// Adjectifs surutilisés par IA
'révolutionnaire': ['nouveau', 'moderne', 'innovant', 'original'],
'game-changer': ['nouveauté', 'innovation', 'changement', 'révolution'],
'cutting-edge': ['moderne', 'récent', 'nouveau', 'avancé'],
'state-of-the-art': ['moderne', 'récent', 'performant', 'haut de gamme']
};
/**
* EXPRESSIONS CONTEXTUELLES SECTEUR SIGNALÉTIQUE
* Adaptées au domaine métier pour plus de naturel
*/
const CONTEXTUAL_REPLACEMENTS = {
'solution': {
'signalétique': ['plaque', 'panneau', 'support', 'réalisation'],
'impression': ['tirage', 'print', 'production', 'fabrication'],
'default': ['option', 'possibilité', 'choix', 'alternative']
},
'produit': {
'signalétique': ['plaque', 'panneau', 'enseigne', 'support'],
'default': ['article', 'réalisation', 'création']
},
'service': {
'signalétique': ['prestation', 'réalisation', 'travail', 'création'],
'default': ['prestation', 'travail', 'aide']
}
};
/**
* MAIN ENTRY POINT - SUPPRESSION EMPREINTES LLM
* @param {Object} input - { content: {}, config: {}, context: {} }
* @returns {Object} - { content: {}, stats: {}, debug: {} }
*/
async function removeLLMFingerprints(input) {
return await tracer.run('LLMFingerprintRemoval.removeLLMFingerprints()', async () => {
const { content, config = {}, context = {} } = input;
const {
intensity = 1.0, // Probabilité de remplacement (100%)
preserveKeywords = true, // Préserver mots-clés SEO
contextualMode = true, // Mode contextuel métier
csvData = null // Pour contexte métier
} = config;
await tracer.annotate({
technique: 'fingerprint_removal',
intensity,
elementsCount: Object.keys(content).length,
contextualMode
});
const startTime = Date.now();
logSh(`🔍 TECHNIQUE 2/3: Suppression empreintes LLM (intensité: ${intensity})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à nettoyer`, 'DEBUG');
try {
const results = {};
let totalProcessed = 0;
let totalReplacements = 0;
let replacementDetails = [];
// Préparer contexte métier
const businessContext = extractBusinessContext(csvData);
// Traiter chaque élément de contenu
for (const [tag, text] of Object.entries(content)) {
totalProcessed++;
if (text.length < 20) {
results[tag] = text;
continue;
}
// Appliquer suppression des empreintes
const cleaningResult = cleanTextFingerprints(text, {
intensity,
preserveKeywords,
contextualMode,
businessContext,
tag
});
results[tag] = cleaningResult.text;
if (cleaningResult.replacements.length > 0) {
totalReplacements += cleaningResult.replacements.length;
replacementDetails.push({
tag,
replacements: cleaningResult.replacements,
fingerprintsFound: cleaningResult.fingerprintsDetected
});
logSh(` 🧹 [${tag}]: ${cleaningResult.replacements.length} remplacements`, 'DEBUG');
} else {
logSh(` ✅ [${tag}]: Aucune empreinte détectée`, 'DEBUG');
}
}
const duration = Date.now() - startTime;
const stats = {
processed: totalProcessed,
totalReplacements,
avgReplacementsPerElement: Math.round(totalReplacements / totalProcessed * 100) / 100,
elementsWithFingerprints: replacementDetails.length,
duration,
technique: 'fingerprint_removal'
};
logSh(`✅ NETTOYAGE EMPREINTES: ${stats.totalReplacements} remplacements sur ${stats.elementsWithFingerprints}/${stats.processed} éléments en ${duration}ms`, 'INFO');
await tracer.event('Fingerprint removal terminée', stats);
return {
content: results,
stats,
debug: {
technique: 'fingerprint_removal',
config: { intensity, preserveKeywords, contextualMode },
replacements: replacementDetails,
businessContext
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ NETTOYAGE EMPREINTES échoué après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`LLMFingerprintRemoval failed: ${error.message}`);
}
}, input);
}
/**
* Nettoyer les empreintes LLM d'un texte
*/
function cleanTextFingerprints(text, config) {
const { intensity, preserveKeywords, contextualMode, businessContext, tag } = config;
let cleanedText = text;
const replacements = [];
const fingerprintsDetected = [];
// PHASE 1: Remplacements directs du dictionnaire
for (const [fingerprint, alternatives] of Object.entries(LLM_FINGERPRINTS)) {
const regex = new RegExp(`\\b${escapeRegex(fingerprint)}\\b`, 'gi');
const matches = text.match(regex);
if (matches) {
fingerprintsDetected.push(fingerprint);
// Appliquer remplacement selon intensité
if (Math.random() <= intensity) {
const alternative = selectBestAlternative(alternatives, businessContext, contextualMode);
cleanedText = cleanedText.replace(regex, (match) => {
// Préserver la casse originale
return preserveCase(match, alternative);
});
replacements.push({
type: 'direct',
original: fingerprint,
replacement: alternative,
occurrences: matches.length
});
}
}
}
// PHASE 2: Remplacements contextuels
if (contextualMode && businessContext) {
const contextualReplacements = applyContextualReplacements(cleanedText, businessContext);
cleanedText = contextualReplacements.text;
replacements.push(...contextualReplacements.replacements);
}
// PHASE 3: Détection patterns récurrents
const patternReplacements = replaceRecurringPatterns(cleanedText, intensity);
cleanedText = patternReplacements.text;
replacements.push(...patternReplacements.replacements);
return {
text: cleanedText,
replacements,
fingerprintsDetected
};
}
/**
* Sélectionner la meilleure alternative selon le contexte
*/
function selectBestAlternative(alternatives, businessContext, contextualMode) {
if (!contextualMode || !businessContext) {
// Mode aléatoire simple
return alternatives[Math.floor(Math.random() * alternatives.length)];
}
// Mode contextuel : privilégier alternatives adaptées au métier
const contextualAlternatives = alternatives.filter(alt =>
isContextuallyAppropriate(alt, businessContext)
);
const finalAlternatives = contextualAlternatives.length > 0 ? contextualAlternatives : alternatives;
return finalAlternatives[Math.floor(Math.random() * finalAlternatives.length)];
}
/**
* Vérifier si une alternative est contextuelle appropriée
*/
function isContextuallyAppropriate(alternative, businessContext) {
const { sector, vocabulary } = businessContext;
// Signalétique : privilégier vocabulaire technique/artisanal
if (sector === 'signalétique') {
const technicalWords = ['solide', 'fiable', 'costaud', 'résistant', 'adapté'];
return technicalWords.includes(alternative);
}
return true; // Par défaut accepter
}
/**
* Appliquer remplacements contextuels
*/
function applyContextualReplacements(text, businessContext) {
let processedText = text;
const replacements = [];
for (const [word, contexts] of Object.entries(CONTEXTUAL_REPLACEMENTS)) {
const regex = new RegExp(`\\b${word}\\b`, 'gi');
const matches = processedText.match(regex);
if (matches) {
const contextAlternatives = contexts[businessContext.sector] || contexts.default;
const replacement = contextAlternatives[Math.floor(Math.random() * contextAlternatives.length)];
processedText = processedText.replace(regex, (match) => {
return preserveCase(match, replacement);
});
replacements.push({
type: 'contextual',
original: word,
replacement,
occurrences: matches.length,
context: businessContext.sector
});
}
}
return { text: processedText, replacements };
}
/**
* Remplacer patterns récurrents
*/
function replaceRecurringPatterns(text, intensity) {
let processedText = text;
const replacements = [];
// Pattern 1: "très + adjectif" → variantes
const veryPattern = /\btrès\s+(\w+)/gi;
const veryMatches = [...text.matchAll(veryPattern)];
if (veryMatches.length > 2 && Math.random() < intensity) {
// Remplacer certains "très" par des alternatives
const alternatives = ['super', 'vraiment', 'particulièrement', 'assez'];
veryMatches.slice(1).forEach((match, index) => {
if (Math.random() < 0.5) {
const alternative = alternatives[Math.floor(Math.random() * alternatives.length)];
const fullMatch = match[0];
const adjective = match[1];
const replacement = `${alternative} ${adjective}`;
processedText = processedText.replace(fullMatch, replacement);
replacements.push({
type: 'pattern',
pattern: '"très + adjectif"',
original: fullMatch,
replacement
});
}
});
}
return { text: processedText, replacements };
}
/**
* Extraire contexte métier des données CSV
*/
function extractBusinessContext(csvData) {
if (!csvData) {
return { sector: 'general', vocabulary: [] };
}
const mc0 = csvData.mc0?.toLowerCase() || '';
// Détection secteur
let sector = 'general';
if (mc0.includes('plaque') || mc0.includes('panneau') || mc0.includes('enseigne')) {
sector = 'signalétique';
} else if (mc0.includes('impression') || mc0.includes('print')) {
sector = 'impression';
}
// Extraction vocabulaire clé
const vocabulary = [csvData.mc0, csvData.t0, csvData.tMinus1].filter(Boolean);
return { sector, vocabulary };
}
/**
* Préserver la casse originale
*/
function preserveCase(original, replacement) {
if (original === original.toUpperCase()) {
return replacement.toUpperCase();
} else if (original[0] === original[0].toUpperCase()) {
return replacement.charAt(0).toUpperCase() + replacement.slice(1).toLowerCase();
} else {
return replacement.toLowerCase();
}
}
/**
* Échapper caractères regex
*/
function escapeRegex(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Analyser les empreintes LLM dans un texte
*/
function analyzeLLMFingerprints(text) {
const detectedFingerprints = [];
let totalMatches = 0;
for (const fingerprint of Object.keys(LLM_FINGERPRINTS)) {
const regex = new RegExp(`\\b${escapeRegex(fingerprint)}\\b`, 'gi');
const matches = text.match(regex);
if (matches) {
detectedFingerprints.push({
fingerprint,
occurrences: matches.length,
category: categorizefingerprint(fingerprint)
});
totalMatches += matches.length;
}
}
return {
hasFingerprints: detectedFingerprints.length > 0,
fingerprints: detectedFingerprints,
totalMatches,
riskLevel: calculateRiskLevel(detectedFingerprints, text.length)
};
}
/**
* Catégoriser une empreinte LLM
*/
function categorizefingerprint(fingerprint) {
const categories = {
'technical': ['optimal', 'comprehensive', 'robust', 'seamless'],
'formal': ['il convient de', 'néanmoins', 'par conséquent'],
'superlative': ['extrêmement', 'particulièrement', 'remarquablement'],
'commercial': ['solution innovante', 'game-changer', 'révolutionnaire']
};
for (const [category, words] of Object.entries(categories)) {
if (words.some(word => fingerprint.includes(word))) {
return category;
}
}
return 'other';
}
/**
* Calculer niveau de risque de détection
*/
function calculateRiskLevel(fingerprints, textLength) {
if (fingerprints.length === 0) return 'low';
const fingerprintDensity = fingerprints.reduce((sum, fp) => sum + fp.occurrences, 0) / (textLength / 100);
if (fingerprintDensity > 3) return 'high';
if (fingerprintDensity > 1.5) return 'medium';
return 'low';
}
module.exports = {
removeLLMFingerprints, // ← MAIN ENTRY POINT
cleanTextFingerprints,
analyzeLLMFingerprints,
LLM_FINGERPRINTS,
CONTEXTUAL_REPLACEMENTS,
extractBusinessContext
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/post-processing/SentenceVariation.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// PATTERN BREAKING - TECHNIQUE 1: SENTENCE VARIATION
// Responsabilité: Varier les longueurs de phrases pour casser l'uniformité
// Anti-détection: Éviter patterns syntaxiques réguliers des LLMs
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - VARIATION LONGUEUR PHRASES
* @param {Object} input - { content: {}, config: {}, context: {} }
* @returns {Object} - { content: {}, stats: {}, debug: {} }
*/
async function applySentenceVariation(input) {
return await tracer.run('SentenceVariation.applySentenceVariation()', async () => {
const { content, config = {}, context = {} } = input;
const {
intensity = 0.3, // Probabilité de modification (30%)
splitThreshold = 100, // Chars pour split
mergeThreshold = 30, // Chars pour merge
preserveQuestions = true, // Préserver questions FAQ
preserveTitles = true // Préserver titres
} = config;
await tracer.annotate({
technique: 'sentence_variation',
intensity,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`📐 TECHNIQUE 1/3: Variation longueur phrases (intensité: ${intensity})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'DEBUG');
try {
const results = {};
let totalProcessed = 0;
let totalModified = 0;
let modificationsDetails = [];
// Traiter chaque élément de contenu
for (const [tag, text] of Object.entries(content)) {
totalProcessed++;
// Skip certains éléments selon config
if (shouldSkipElement(tag, text, { preserveQuestions, preserveTitles })) {
results[tag] = text;
logSh(` ⏭️ [${tag}]: Préservé (${getSkipReason(tag, text)})`, 'DEBUG');
continue;
}
// Appliquer variation si éligible
const variationResult = varyTextStructure(text, {
intensity,
splitThreshold,
mergeThreshold,
tag
});
results[tag] = variationResult.text;
if (variationResult.modified) {
totalModified++;
modificationsDetails.push({
tag,
modifications: variationResult.modifications,
originalLength: text.length,
newLength: variationResult.text.length
});
logSh(` ✏️ [${tag}]: ${variationResult.modifications.length} modifications`, 'DEBUG');
} else {
logSh(` ➡️ [${tag}]: Aucune modification`, 'DEBUG');
}
}
const duration = Date.now() - startTime;
const stats = {
processed: totalProcessed,
modified: totalModified,
modificationRate: Math.round((totalModified / totalProcessed) * 100),
duration,
technique: 'sentence_variation'
};
logSh(`✅ VARIATION PHRASES: ${stats.modified}/${stats.processed} éléments modifiés (${stats.modificationRate}%) en ${duration}ms`, 'INFO');
await tracer.event('Sentence variation terminée', stats);
return {
content: results,
stats,
debug: {
technique: 'sentence_variation',
config: { intensity, splitThreshold, mergeThreshold },
modifications: modificationsDetails
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ VARIATION PHRASES échouée après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`SentenceVariation failed: ${error.message}`);
}
}, input);
}
/**
* Appliquer variation structure à un texte
*/
function varyTextStructure(text, config) {
const { intensity, splitThreshold, mergeThreshold, tag } = config;
if (text.length < 50) {
return { text, modified: false, modifications: [] };
}
// Séparer en phrases
const sentences = splitIntoSentences(text);
if (sentences.length < 2) {
return { text, modified: false, modifications: [] };
}
let modifiedSentences = [...sentences];
const modifications = [];
// TECHNIQUE 1: SPLIT des phrases longues
for (let i = 0; i < modifiedSentences.length; i++) {
const sentence = modifiedSentences[i];
if (sentence.length > splitThreshold && Math.random() < intensity) {
const splitResult = splitLongSentence(sentence);
if (splitResult.success) {
modifiedSentences.splice(i, 1, splitResult.part1, splitResult.part2);
modifications.push({
type: 'split',
original: sentence.substring(0, 50) + '...',
result: `${splitResult.part1.substring(0, 25)}... | ${splitResult.part2.substring(0, 25)}...`
});
i++; // Skip la phrase suivante (qui est notre part2)
}
}
}
// TECHNIQUE 2: MERGE des phrases courtes
for (let i = 0; i < modifiedSentences.length - 1; i++) {
const current = modifiedSentences[i];
const next = modifiedSentences[i + 1];
if (current.length < mergeThreshold && next.length < mergeThreshold && Math.random() < intensity) {
const merged = mergeSentences(current, next);
if (merged.success) {
modifiedSentences.splice(i, 2, merged.result);
modifications.push({
type: 'merge',
original: `${current.substring(0, 20)}... + ${next.substring(0, 20)}...`,
result: merged.result.substring(0, 50) + '...'
});
}
}
}
const finalText = modifiedSentences.join(' ').trim();
return {
text: finalText,
modified: modifications.length > 0,
modifications
};
}
/**
* Diviser texte en phrases
*/
function splitIntoSentences(text) {
// Regex plus sophistiquée pour gérer les abréviations
const sentences = text.split(/(?<![A-Z][a-z]\.)\s*[.!?]+\s+/)
.map(s => s.trim())
.filter(s => s.length > 5);
return sentences;
}
/**
* Diviser une phrase longue en deux
*/
function splitLongSentence(sentence) {
// Points de rupture naturels
const breakPoints = [
', et ',
', mais ',
', car ',
', donc ',
', ainsi ',
', alors ',
', tandis que ',
', bien que '
];
// Chercher le meilleur point de rupture proche du milieu
const idealBreak = sentence.length / 2;
let bestBreak = null;
let bestDistance = Infinity;
for (const breakPoint of breakPoints) {
const index = sentence.indexOf(breakPoint, idealBreak - 50);
if (index > 0 && index < sentence.length - 20) {
const distance = Math.abs(index - idealBreak);
if (distance < bestDistance) {
bestDistance = distance;
bestBreak = { index, breakPoint };
}
}
}
if (bestBreak) {
const part1 = sentence.substring(0, bestBreak.index + 1).trim();
const part2 = sentence.substring(bestBreak.index + bestBreak.breakPoint.length).trim();
// Assurer que part2 commence par une majuscule
const capitalizedPart2 = part2.charAt(0).toUpperCase() + part2.slice(1);
return {
success: true,
part1,
part2: capitalizedPart2
};
}
return { success: false };
}
/**
* Fusionner deux phrases courtes
*/
function mergeSentences(sentence1, sentence2) {
// Connecteurs pour fusion naturelle
const connectors = [
'et',
'puis',
'aussi',
'également',
'de plus'
];
// Choisir connecteur aléatoire
const connector = connectors[Math.floor(Math.random() * connectors.length)];
// Nettoyer les phrases
let cleaned1 = sentence1.replace(/[.!?]+$/, '').trim();
let cleaned2 = sentence2.trim();
// Mettre sentence2 en minuscule sauf si nom propre
if (!/^[A-Z][a-z]*\s+[A-Z]/.test(cleaned2)) {
cleaned2 = cleaned2.charAt(0).toLowerCase() + cleaned2.slice(1);
}
const merged = `${cleaned1}, ${connector} ${cleaned2}`;
return {
success: merged.length < 200, // Éviter phrases trop longues
result: merged
};
}
/**
* Déterminer si un élément doit être skippé
*/
function shouldSkipElement(tag, text, config) {
// Skip titres si demandé
if (config.preserveTitles && (tag.includes('Titre') || tag.includes('H1') || tag.includes('H2'))) {
return true;
}
// Skip questions FAQ si demandé
if (config.preserveQuestions && (tag.includes('Faq_q') || text.includes('?'))) {
return true;
}
// Skip textes très courts
if (text.length < 50) {
return true;
}
return false;
}
/**
* Obtenir raison du skip pour debug
*/
function getSkipReason(tag, text) {
if (tag.includes('Titre') || tag.includes('H1') || tag.includes('H2')) return 'titre';
if (tag.includes('Faq_q') || text.includes('?')) return 'question';
if (text.length < 50) return 'trop court';
return 'autre';
}
/**
* Analyser les patterns de phrases d'un texte
*/
function analyzeSentencePatterns(text) {
const sentences = splitIntoSentences(text);
if (sentences.length < 2) {
return { needsVariation: false, patterns: [] };
}
const lengths = sentences.map(s => s.length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
// Calculer uniformité (variance faible = uniformité élevée)
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const uniformity = 1 / (1 + Math.sqrt(variance) / avgLength); // 0-1, 1 = très uniforme
return {
needsVariation: uniformity > 0.7, // Seuil d'uniformité problématique
patterns: {
avgLength: Math.round(avgLength),
uniformity: Math.round(uniformity * 100),
sentenceCount: sentences.length,
variance: Math.round(variance)
}
};
}
module.exports = {
applySentenceVariation, // ← MAIN ENTRY POINT
varyTextStructure,
splitIntoSentences,
splitLongSentence,
mergeSentences,
analyzeSentencePatterns
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/post-processing/TransitionHumanization.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// PATTERN BREAKING - TECHNIQUE 3: TRANSITION HUMANIZATION
// Responsabilité: Remplacer connecteurs mécaniques par transitions naturelles
// Anti-détection: Éviter patterns de liaison typiques des LLMs
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* DICTIONNAIRE CONNECTEURS HUMANISÉS
* Connecteurs LLM → Alternatives naturelles par contexte
*/
const TRANSITION_REPLACEMENTS = {
// Connecteurs trop formels → versions naturelles
'par ailleurs': {
alternatives: ['d\'ailleurs', 'au fait', 'soit dit en passant', 'à propos', 'sinon'],
weight: 0.8,
contexts: ['casual', 'conversational']
},
'en effet': {
alternatives: ['effectivement', 'c\'est vrai', 'tout à fait', 'absolument', 'exactement'],
weight: 0.9,
contexts: ['confirmative', 'agreement']
},
'de plus': {
alternatives: ['aussi', 'également', 'qui plus est', 'en plus', 'et puis'],
weight: 0.7,
contexts: ['additive', 'continuation']
},
'cependant': {
alternatives: ['mais', 'pourtant', 'néanmoins', 'malgré tout', 'quand même'],
weight: 0.6,
contexts: ['contrast', 'opposition']
},
'ainsi': {
alternatives: ['donc', 'du coup', 'comme ça', 'par conséquent', 'résultat'],
weight: 0.8,
contexts: ['consequence', 'result']
},
'donc': {
alternatives: ['du coup', 'alors', 'par conséquent', 'ainsi', 'résultat'],
weight: 0.5,
contexts: ['consequence', 'logical']
},
// Connecteurs de séquence
'ensuite': {
alternatives: ['puis', 'après', 'et puis', 'alors', 'du coup'],
weight: 0.6,
contexts: ['sequence', 'temporal']
},
'puis': {
alternatives: ['ensuite', 'après', 'et puis', 'alors'],
weight: 0.4,
contexts: ['sequence', 'temporal']
},
// Connecteurs d'emphase
'également': {
alternatives: ['aussi', 'de même', 'pareillement', 'en plus'],
weight: 0.6,
contexts: ['similarity', 'addition']
},
'aussi': {
alternatives: ['également', 'de même', 'en plus', 'pareillement'],
weight: 0.3,
contexts: ['similarity', 'addition']
},
// Connecteurs de conclusion
'enfin': {
alternatives: ['finalement', 'au final', 'pour finir', 'en dernier'],
weight: 0.5,
contexts: ['conclusion', 'final']
},
'finalement': {
alternatives: ['au final', 'en fin de compte', 'pour finir', 'enfin'],
weight: 0.4,
contexts: ['conclusion', 'final']
}
};
/**
* PATTERNS DE TRANSITION NATURELLE
* Selon le style de personnalité
*/
const PERSONALITY_TRANSITIONS = {
'décontracté': {
preferred: ['du coup', 'alors', 'bon', 'après', 'sinon'],
avoided: ['par conséquent', 'néanmoins', 'toutefois']
},
'technique': {
preferred: ['donc', 'ainsi', 'par conséquent', 'résultat'],
avoided: ['du coup', 'bon', 'franchement']
},
'commercial': {
preferred: ['aussi', 'de plus', 'également', 'qui plus est'],
avoided: ['du coup', 'bon', 'franchement']
},
'familier': {
preferred: ['du coup', 'bon', 'alors', 'après', 'franchement'],
avoided: ['par conséquent', 'néanmoins', 'de surcroît']
}
};
/**
* MAIN ENTRY POINT - HUMANISATION TRANSITIONS
* @param {Object} input - { content: {}, config: {}, context: {} }
* @returns {Object} - { content: {}, stats: {}, debug: {} }
*/
async function humanizeTransitions(input) {
return await tracer.run('TransitionHumanization.humanizeTransitions()', async () => {
const { content, config = {}, context = {} } = input;
const {
intensity = 0.6, // Probabilité de remplacement (60%)
personalityStyle = null, // Style de personnalité pour guidage
avoidRepetition = true, // Éviter répétitions excessives
preserveFormal = false, // Préserver style formel
csvData = null // Données pour personnalité
} = config;
await tracer.annotate({
technique: 'transition_humanization',
intensity,
personalityStyle: personalityStyle || csvData?.personality?.style,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🔗 TECHNIQUE 3/3: Humanisation transitions (intensité: ${intensity})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à humaniser`, 'DEBUG');
try {
const results = {};
let totalProcessed = 0;
let totalReplacements = 0;
let humanizationDetails = [];
// Extraire style de personnalité
const effectivePersonalityStyle = personalityStyle || csvData?.personality?.style || 'neutral';
// Analyser patterns globaux pour éviter répétitions
const globalPatterns = analyzeGlobalTransitionPatterns(content);
// Traiter chaque élément de contenu
for (const [tag, text] of Object.entries(content)) {
totalProcessed++;
if (text.length < 30) {
results[tag] = text;
continue;
}
// Appliquer humanisation des transitions
const humanizationResult = humanizeTextTransitions(text, {
intensity,
personalityStyle: effectivePersonalityStyle,
avoidRepetition,
preserveFormal,
globalPatterns,
tag
});
results[tag] = humanizationResult.text;
if (humanizationResult.replacements.length > 0) {
totalReplacements += humanizationResult.replacements.length;
humanizationDetails.push({
tag,
replacements: humanizationResult.replacements,
transitionsDetected: humanizationResult.transitionsFound
});
logSh(` 🔄 [${tag}]: ${humanizationResult.replacements.length} transitions humanisées`, 'DEBUG');
} else {
logSh(` ➡️ [${tag}]: Transitions déjà naturelles`, 'DEBUG');
}
}
const duration = Date.now() - startTime;
const stats = {
processed: totalProcessed,
totalReplacements,
avgReplacementsPerElement: Math.round(totalReplacements / totalProcessed * 100) / 100,
elementsWithTransitions: humanizationDetails.length,
personalityStyle: effectivePersonalityStyle,
duration,
technique: 'transition_humanization'
};
logSh(`✅ HUMANISATION TRANSITIONS: ${stats.totalReplacements} remplacements sur ${stats.elementsWithTransitions}/${stats.processed} éléments en ${duration}ms`, 'INFO');
await tracer.event('Transition humanization terminée', stats);
return {
content: results,
stats,
debug: {
technique: 'transition_humanization',
config: { intensity, personalityStyle: effectivePersonalityStyle, avoidRepetition },
humanizations: humanizationDetails,
globalPatterns
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ HUMANISATION TRANSITIONS échouée après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`TransitionHumanization failed: ${error.message}`);
}
}, input);
}
/**
* Humaniser les transitions d'un texte
*/
function humanizeTextTransitions(text, config) {
const { intensity, personalityStyle, avoidRepetition, preserveFormal, globalPatterns, tag } = config;
let humanizedText = text;
const replacements = [];
const transitionsFound = [];
// Statistiques usage pour éviter répétitions
const usageStats = {};
// Traiter chaque connecteur du dictionnaire
for (const [transition, transitionData] of Object.entries(TRANSITION_REPLACEMENTS)) {
const { alternatives, weight, contexts } = transitionData;
// Rechercher occurrences (insensible à la casse, mais préserver limites mots)
const regex = new RegExp(`\\b${escapeRegex(transition)}\\b`, 'gi');
const matches = [...text.matchAll(regex)];
if (matches.length > 0) {
transitionsFound.push(transition);
// Décider si on remplace selon intensité et poids
const shouldReplace = Math.random() < (intensity * weight);
if (shouldReplace && !preserveFormal) {
// Sélectionner meilleure alternative
const selectedAlternative = selectBestTransitionAlternative(
alternatives,
personalityStyle,
usageStats,
avoidRepetition
);
// Appliquer remplacement en préservant la casse
humanizedText = humanizedText.replace(regex, (match) => {
return preserveCase(match, selectedAlternative);
});
// Enregistrer usage
usageStats[selectedAlternative] = (usageStats[selectedAlternative] || 0) + matches.length;
replacements.push({
original: transition,
replacement: selectedAlternative,
occurrences: matches.length,
contexts,
personalityMatch: isPersonalityAppropriate(selectedAlternative, personalityStyle)
});
}
}
}
// Post-processing : éviter accumulations
if (avoidRepetition) {
const repetitionCleaned = reduceTransitionRepetition(humanizedText, usageStats);
humanizedText = repetitionCleaned.text;
replacements.push(...repetitionCleaned.additionalChanges);
}
return {
text: humanizedText,
replacements,
transitionsFound
};
}
/**
* Sélectionner meilleure alternative de transition
*/
function selectBestTransitionAlternative(alternatives, personalityStyle, usageStats, avoidRepetition) {
// Filtrer selon personnalité
const personalityFiltered = alternatives.filter(alt =>
isPersonalityAppropriate(alt, personalityStyle)
);
const candidateList = personalityFiltered.length > 0 ? personalityFiltered : alternatives;
if (!avoidRepetition) {
return candidateList[Math.floor(Math.random() * candidateList.length)];
}
// Éviter les alternatives déjà trop utilisées
const lessUsedAlternatives = candidateList.filter(alt =>
(usageStats[alt] || 0) < 2
);
const finalList = lessUsedAlternatives.length > 0 ? lessUsedAlternatives : candidateList;
return finalList[Math.floor(Math.random() * finalList.length)];
}
/**
* Vérifier si alternative appropriée pour personnalité
*/
function isPersonalityAppropriate(alternative, personalityStyle) {
if (!personalityStyle || personalityStyle === 'neutral') return true;
const styleMapping = {
'décontracté': PERSONALITY_TRANSITIONS.décontracté,
'technique': PERSONALITY_TRANSITIONS.technique,
'commercial': PERSONALITY_TRANSITIONS.commercial,
'familier': PERSONALITY_TRANSITIONS.familier
};
const styleConfig = styleMapping[personalityStyle.toLowerCase()];
if (!styleConfig) return true;
// Éviter les connecteurs inappropriés
if (styleConfig.avoided.includes(alternative)) return false;
// Privilégier les connecteurs préférés
if (styleConfig.preferred.includes(alternative)) return true;
return true;
}
/**
* Réduire répétitions excessives de transitions
*/
function reduceTransitionRepetition(text, usageStats) {
let processedText = text;
const additionalChanges = [];
// Identifier connecteurs surutilisés (>3 fois)
const overusedTransitions = Object.entries(usageStats)
.filter(([transition, count]) => count > 3)
.map(([transition]) => transition);
for (const overusedTransition of overusedTransitions) {
// Remplacer quelques occurrences par des alternatives
const regex = new RegExp(`\\b${escapeRegex(overusedTransition)}\\b`, 'g');
let replacements = 0;
processedText = processedText.replace(regex, (match, offset) => {
// Remplacer 1 occurrence sur 3 environ
if (Math.random() < 0.33 && replacements < 2) {
replacements++;
const alternatives = findAlternativesFor(overusedTransition);
const alternative = alternatives[Math.floor(Math.random() * alternatives.length)];
additionalChanges.push({
type: 'repetition_reduction',
original: overusedTransition,
replacement: alternative,
reason: 'overuse'
});
return preserveCase(match, alternative);
}
return match;
});
}
return { text: processedText, additionalChanges };
}
/**
* Trouver alternatives pour un connecteur donné
*/
function findAlternativesFor(transition) {
// Chercher dans le dictionnaire
for (const [key, data] of Object.entries(TRANSITION_REPLACEMENTS)) {
if (data.alternatives.includes(transition)) {
return data.alternatives.filter(alt => alt !== transition);
}
}
// Alternatives génériques
const genericAlternatives = {
'du coup': ['alors', 'donc', 'ainsi'],
'alors': ['du coup', 'donc', 'ensuite'],
'donc': ['du coup', 'alors', 'ainsi'],
'aussi': ['également', 'de plus', 'en plus'],
'mais': ['cependant', 'pourtant', 'néanmoins']
};
return genericAlternatives[transition] || ['donc', 'alors'];
}
/**
* Analyser patterns globaux de transitions
*/
function analyzeGlobalTransitionPatterns(content) {
const allText = Object.values(content).join(' ');
const transitionCounts = {};
const repetitionPatterns = [];
// Compter occurrences globales
for (const transition of Object.keys(TRANSITION_REPLACEMENTS)) {
const regex = new RegExp(`\\b${escapeRegex(transition)}\\b`, 'gi');
const matches = allText.match(regex);
if (matches) {
transitionCounts[transition] = matches.length;
}
}
// Identifier patterns de répétition problématiques
const sortedTransitions = Object.entries(transitionCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 5); // Top 5 plus utilisées
sortedTransitions.forEach(([transition, count]) => {
if (count > 5) {
repetitionPatterns.push({
transition,
count,
severity: count > 10 ? 'high' : count > 7 ? 'medium' : 'low'
});
}
});
return {
transitionCounts,
repetitionPatterns,
diversityScore: Object.keys(transitionCounts).length / Math.max(1, Object.values(transitionCounts).reduce((a,b) => a+b, 0))
};
}
/**
* Préserver la casse originale
*/
function preserveCase(original, replacement) {
if (original === original.toUpperCase()) {
return replacement.toUpperCase();
} else if (original[0] === original[0].toUpperCase()) {
return replacement.charAt(0).toUpperCase() + replacement.slice(1).toLowerCase();
} else {
return replacement.toLowerCase();
}
}
/**
* Échapper caractères regex
*/
function escapeRegex(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Analyser qualité des transitions d'un texte
*/
function analyzeTransitionQuality(text) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 5);
if (sentences.length < 2) {
return { score: 100, issues: [], naturalness: 'high' };
}
let mechanicalTransitions = 0;
let totalTransitions = 0;
const issues = [];
// Analyser chaque transition
sentences.forEach((sentence, index) => {
if (index === 0) return;
const trimmed = sentence.trim();
const startsWithTransition = Object.keys(TRANSITION_REPLACEMENTS).some(transition =>
trimmed.toLowerCase().startsWith(transition.toLowerCase())
);
if (startsWithTransition) {
totalTransitions++;
// Vérifier si transition mécanique
const transition = Object.keys(TRANSITION_REPLACEMENTS).find(t =>
trimmed.toLowerCase().startsWith(t.toLowerCase())
);
if (transition && TRANSITION_REPLACEMENTS[transition].weight > 0.7) {
mechanicalTransitions++;
issues.push({
type: 'mechanical_transition',
transition,
suggestion: TRANSITION_REPLACEMENTS[transition].alternatives[0]
});
}
}
});
const mechanicalRatio = totalTransitions > 0 ? mechanicalTransitions / totalTransitions : 0;
const score = Math.max(0, 100 - (mechanicalRatio * 100));
let naturalness = 'high';
if (mechanicalRatio > 0.5) naturalness = 'low';
else if (mechanicalRatio > 0.25) naturalness = 'medium';
return { score: Math.round(score), issues, naturalness, mechanicalRatio };
}
module.exports = {
humanizeTransitions, // ← MAIN ENTRY POINT
humanizeTextTransitions,
analyzeTransitionQuality,
analyzeGlobalTransitionPatterns,
TRANSITION_REPLACEMENTS,
PERSONALITY_TRANSITIONS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/post-processing/PatternBreaking.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ORCHESTRATEUR PATTERN BREAKING - NIVEAU 2
// Responsabilité: Coordonner les 3 techniques anti-détection
// Objectif: -20% détection IA vs Niveau 1
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
// Import des 3 techniques Pattern Breaking
const { applySentenceVariation } = require('./SentenceVariation');
const { removeLLMFingerprints } = require('./LLMFingerprintRemoval');
const { humanizeTransitions } = require('./TransitionHumanization');
/**
* MAIN ENTRY POINT - PATTERN BREAKING COMPLET
* @param {Object} input - { content: {}, csvData: {}, options: {} }
* @returns {Object} - { content: {}, stats: {}, debug: {} }
*/
async function applyPatternBreaking(input) {
return await tracer.run('PatternBreaking.applyPatternBreaking()', async () => {
const { content, csvData, options = {} } = input;
const config = {
// Configuration globale
intensity: 0.6, // Intensité générale (60%)
// Contrôle par technique
sentenceVariation: true, // Activer variation phrases
fingerprintRemoval: true, // Activer suppression empreintes
transitionHumanization: true, // Activer humanisation transitions
// Configuration spécifique par technique
sentenceVariationConfig: {
intensity: 0.3,
splitThreshold: 100,
mergeThreshold: 30,
preserveQuestions: true,
preserveTitles: true
},
fingerprintRemovalConfig: {
intensity: 1.0,
preserveKeywords: true,
contextualMode: true,
csvData
},
transitionHumanizationConfig: {
intensity: 0.6,
personalityStyle: csvData?.personality?.style,
avoidRepetition: true,
preserveFormal: false,
csvData
},
// Options avancées
qualityPreservation: true, // Préserver qualité contenu
seoIntegrity: true, // Maintenir intégrité SEO
readabilityCheck: true, // Vérifier lisibilité
...options // Override avec options fournies
};
await tracer.annotate({
level: 2,
technique: 'pattern_breaking',
elementsCount: Object.keys(content).length,
personality: csvData?.personality?.nom,
config: {
sentenceVariation: config.sentenceVariation,
fingerprintRemoval: config.fingerprintRemoval,
transitionHumanization: config.transitionHumanization,
intensity: config.intensity
}
});
const startTime = Date.now();
logSh(`🎯 NIVEAU 2: PATTERN BREAKING (3 techniques)`, 'INFO');
logSh(` 🎭 Personnalité: ${csvData?.personality?.nom} (${csvData?.personality?.style})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à traiter`, 'INFO');
logSh(` ⚙️ Techniques actives: ${[config.sentenceVariation && 'Variation', config.fingerprintRemoval && 'Empreintes', config.transitionHumanization && 'Transitions'].filter(Boolean).join(' + ')}`, 'INFO');
try {
let currentContent = { ...content };
const pipelineStats = {
techniques: [],
totalDuration: 0,
qualityMetrics: {}
};
// Analyse initiale de qualité
if (config.qualityPreservation) {
pipelineStats.qualityMetrics.initial = analyzeContentQuality(currentContent);
}
// TECHNIQUE 1: VARIATION LONGUEUR PHRASES
if (config.sentenceVariation) {
const step1Result = await applySentenceVariation({
content: currentContent,
config: config.sentenceVariationConfig,
context: { step: 1, totalSteps: 3 }
});
currentContent = step1Result.content;
pipelineStats.techniques.push({
name: 'SentenceVariation',
...step1Result.stats,
qualityImpact: calculateQualityImpact(content, step1Result.content)
});
logSh(` ✅ 1/3: Variation phrases - ${step1Result.stats.modified}/${step1Result.stats.processed} éléments`, 'INFO');
}
// TECHNIQUE 2: SUPPRESSION EMPREINTES LLM
if (config.fingerprintRemoval) {
const step2Result = await removeLLMFingerprints({
content: currentContent,
config: config.fingerprintRemovalConfig,
context: { step: 2, totalSteps: 3 }
});
currentContent = step2Result.content;
pipelineStats.techniques.push({
name: 'FingerprintRemoval',
...step2Result.stats,
qualityImpact: calculateQualityImpact(content, step2Result.content)
});
logSh(` ✅ 2/3: Suppression empreintes - ${step2Result.stats.totalReplacements} remplacements`, 'INFO');
}
// TECHNIQUE 3: HUMANISATION TRANSITIONS
if (config.transitionHumanization) {
const step3Result = await humanizeTransitions({
content: currentContent,
config: config.transitionHumanizationConfig,
context: { step: 3, totalSteps: 3 }
});
currentContent = step3Result.content;
pipelineStats.techniques.push({
name: 'TransitionHumanization',
...step3Result.stats,
qualityImpact: calculateQualityImpact(content, step3Result.content)
});
logSh(` ✅ 3/3: Humanisation transitions - ${step3Result.stats.totalReplacements} améliorations`, 'INFO');
}
// POST-PROCESSING: Vérifications qualité
if (config.qualityPreservation || config.readabilityCheck) {
const qualityCheck = performQualityChecks(content, currentContent, config);
pipelineStats.qualityMetrics.final = qualityCheck;
// Rollback si qualité trop dégradée
if (qualityCheck.shouldRollback) {
logSh(`⚠️ ROLLBACK: Qualité dégradée, retour contenu original`, 'WARNING');
currentContent = content;
pipelineStats.rollback = true;
}
}
// RÉSULTATS FINAUX
const totalDuration = Date.now() - startTime;
pipelineStats.totalDuration = totalDuration;
const totalModifications = pipelineStats.techniques.reduce((sum, tech) => {
return sum + (tech.modified || tech.totalReplacements || 0);
}, 0);
const stats = {
level: 2,
technique: 'pattern_breaking',
processed: Object.keys(content).length,
totalModifications,
techniquesUsed: pipelineStats.techniques.length,
duration: totalDuration,
techniques: pipelineStats.techniques,
qualityPreserved: !pipelineStats.rollback,
rollback: pipelineStats.rollback || false
};
logSh(`🎯 NIVEAU 2 TERMINÉ: ${totalModifications} modifications sur ${stats.processed} éléments (${totalDuration}ms)`, 'INFO');
// Log détaillé par technique
pipelineStats.techniques.forEach(tech => {
const modificationsCount = tech.modified || tech.totalReplacements || 0;
logSh(`${tech.name}: ${modificationsCount} modifications (${tech.duration}ms)`, 'DEBUG');
});
await tracer.event('Pattern breaking terminé', stats);
return {
content: currentContent,
stats,
debug: {
level: 2,
technique: 'pattern_breaking',
config,
pipeline: pipelineStats,
qualityMetrics: pipelineStats.qualityMetrics
}
};
} catch (error) {
const totalDuration = Date.now() - startTime;
logSh(`❌ NIVEAU 2 ÉCHOUÉ après ${totalDuration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
await tracer.event('Pattern breaking échoué', {
error: error.message,
duration: totalDuration,
fallback: true
});
return {
content,
stats: {
level: 2,
technique: 'pattern_breaking',
processed: Object.keys(content).length,
totalModifications: 0,
duration: totalDuration,
error: error.message,
fallback: true
},
debug: { error: error.message, fallback: true }
};
}
}, input);
}
/**
* MODE DIAGNOSTIC - Test individuel des techniques
*/
async function diagnosticPatternBreaking(content, csvData) {
logSh(`🔬 DIAGNOSTIC NIVEAU 2: Test individuel des techniques`, 'INFO');
const diagnostics = {
techniques: [],
errors: [],
performance: {},
recommendations: []
};
const techniques = [
{ name: 'SentenceVariation', func: applySentenceVariation },
{ name: 'FingerprintRemoval', func: removeLLMFingerprints },
{ name: 'TransitionHumanization', func: humanizeTransitions }
];
for (const technique of techniques) {
try {
const startTime = Date.now();
const result = await technique.func({
content,
config: { csvData },
context: { diagnostic: true }
});
diagnostics.techniques.push({
name: technique.name,
success: true,
duration: Date.now() - startTime,
stats: result.stats,
effectivenessScore: calculateEffectivenessScore(result.stats)
});
} catch (error) {
diagnostics.errors.push({
technique: technique.name,
error: error.message
});
diagnostics.techniques.push({
name: technique.name,
success: false,
error: error.message
});
}
}
// Générer recommandations
diagnostics.recommendations = generateRecommendations(diagnostics.techniques);
const successfulTechniques = diagnostics.techniques.filter(t => t.success);
diagnostics.performance.totalDuration = diagnostics.techniques.reduce((sum, t) => sum + (t.duration || 0), 0);
diagnostics.performance.successRate = Math.round((successfulTechniques.length / techniques.length) * 100);
logSh(`🔬 DIAGNOSTIC TERMINÉ: ${successfulTechniques.length}/${techniques.length} techniques opérationnelles`, 'INFO');
return diagnostics;
}
/**
* Analyser qualité du contenu
*/
function analyzeContentQuality(content) {
const allText = Object.values(content).join(' ');
const wordCount = allText.split(/\s+/).length;
const avgWordsPerElement = wordCount / Object.keys(content).length;
// Métrique de lisibilité approximative (Flesch simplifié)
const sentences = allText.split(/[.!?]+/).filter(s => s.trim().length > 5);
const avgWordsPerSentence = wordCount / Math.max(1, sentences.length);
const readabilityScore = Math.max(0, 100 - (avgWordsPerSentence * 1.5));
return {
wordCount,
elementCount: Object.keys(content).length,
avgWordsPerElement: Math.round(avgWordsPerElement),
avgWordsPerSentence: Math.round(avgWordsPerSentence),
readabilityScore: Math.round(readabilityScore),
sentenceCount: sentences.length
};
}
/**
* Calculer impact qualité entre avant/après
*/
function calculateQualityImpact(originalContent, modifiedContent) {
const originalQuality = analyzeContentQuality(originalContent);
const modifiedQuality = analyzeContentQuality(modifiedContent);
const wordCountChange = ((modifiedQuality.wordCount - originalQuality.wordCount) / originalQuality.wordCount) * 100;
const readabilityChange = modifiedQuality.readabilityScore - originalQuality.readabilityScore;
return {
wordCountChange: Math.round(wordCountChange * 100) / 100,
readabilityChange: Math.round(readabilityChange),
severe: Math.abs(wordCountChange) > 10 || Math.abs(readabilityChange) > 15
};
}
/**
* Effectuer vérifications qualité
*/
function performQualityChecks(originalContent, modifiedContent, config) {
const originalQuality = analyzeContentQuality(originalContent);
const modifiedQuality = analyzeContentQuality(modifiedContent);
const qualityThresholds = {
maxWordCountChange: 15, // % max changement nombre mots
minReadabilityScore: 50, // Score lisibilité minimum
maxReadabilityDrop: 20 // Baisse max lisibilité
};
const issues = [];
// Vérification nombre de mots
const wordCountChange = Math.abs(modifiedQuality.wordCount - originalQuality.wordCount) / originalQuality.wordCount * 100;
if (wordCountChange > qualityThresholds.maxWordCountChange) {
issues.push({
type: 'word_count_change',
severity: 'high',
change: wordCountChange,
threshold: qualityThresholds.maxWordCountChange
});
}
// Vérification lisibilité
if (modifiedQuality.readabilityScore < qualityThresholds.minReadabilityScore) {
issues.push({
type: 'low_readability',
severity: 'medium',
score: modifiedQuality.readabilityScore,
threshold: qualityThresholds.minReadabilityScore
});
}
const readabilityDrop = originalQuality.readabilityScore - modifiedQuality.readabilityScore;
if (readabilityDrop > qualityThresholds.maxReadabilityDrop) {
issues.push({
type: 'readability_drop',
severity: 'high',
drop: readabilityDrop,
threshold: qualityThresholds.maxReadabilityDrop
});
}
// Décision rollback
const highSeverityIssues = issues.filter(issue => issue.severity === 'high');
const shouldRollback = highSeverityIssues.length > 0 && config.qualityPreservation;
return {
originalQuality,
modifiedQuality,
issues,
shouldRollback,
qualityScore: calculateOverallQualityScore(issues, modifiedQuality)
};
}
/**
* Calculer score de qualité global
*/
function calculateOverallQualityScore(issues, quality) {
let baseScore = 100;
issues.forEach(issue => {
const penalty = issue.severity === 'high' ? 30 : issue.severity === 'medium' ? 15 : 5;
baseScore -= penalty;
});
// Bonus pour bonne lisibilité
if (quality.readabilityScore > 70) baseScore += 10;
return Math.max(0, Math.min(100, baseScore));
}
/**
* Calculer score d'efficacité d'une technique
*/
function calculateEffectivenessScore(stats) {
if (!stats) return 0;
const modificationsCount = stats.modified || stats.totalReplacements || 0;
const processedCount = stats.processed || 1;
const modificationRate = (modificationsCount / processedCount) * 100;
// Score basé sur taux de modification et durée
const baseScore = Math.min(100, modificationRate * 2); // Max 50% modification = score 100
const durationPenalty = Math.max(0, (stats.duration - 1000) / 100); // Pénalité si > 1s
return Math.max(0, Math.round(baseScore - durationPenalty));
}
/**
* Générer recommandations basées sur diagnostic
*/
function generateRecommendations(techniqueResults) {
const recommendations = [];
techniqueResults.forEach(tech => {
if (!tech.success) {
recommendations.push({
type: 'error',
technique: tech.name,
message: `${tech.name} a échoué: ${tech.error}`,
action: 'Vérifier configuration et dépendances'
});
return;
}
const effectiveness = tech.effectivenessScore || 0;
if (effectiveness < 30) {
recommendations.push({
type: 'low_effectiveness',
technique: tech.name,
message: `${tech.name} peu efficace (score: ${effectiveness})`,
action: 'Augmenter intensité ou réviser configuration'
});
} else if (effectiveness > 80) {
recommendations.push({
type: 'high_effectiveness',
technique: tech.name,
message: `${tech.name} très efficace (score: ${effectiveness})`,
action: 'Configuration optimale'
});
}
if (tech.duration > 3000) {
recommendations.push({
type: 'performance',
technique: tech.name,
message: `${tech.name} lent (${tech.duration}ms)`,
action: 'Considérer réduction intensité ou optimisation'
});
}
});
return recommendations;
}
module.exports = {
applyPatternBreaking, // ← MAIN ENTRY POINT
diagnosticPatternBreaking, // ← Mode diagnostic
analyzeContentQuality,
performQualityChecks,
calculateQualityImpact,
calculateEffectivenessScore
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/demo-modulaire.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// DÉMONSTRATION ARCHITECTURE MODULAIRE SELECTIVE
// Usage: node lib/selective-enhancement/demo-modulaire.js
// Objectif: Valider l'intégration modulaire selective enhancement
// ========================================
const { logSh } = require('../ErrorReporting');
// Import modules selective modulaires
const { applySelectiveLayer } = require('./SelectiveCore');
const {
applyPredefinedStack,
applyAdaptiveLayers,
getAvailableStacks
} = require('./SelectiveLayers');
const {
analyzeTechnicalQuality,
analyzeTransitionFluidity,
analyzeStyleConsistency,
generateImprovementReport
} = require('./SelectiveUtils');
/**
* EXEMPLE D'UTILISATION MODULAIRE SELECTIVE
*/
async function demoModularSelective() {
console.log('\n🔧 === DÉMONSTRATION SELECTIVE MODULAIRE ===\n');
// Contenu d'exemple avec problèmes de qualité
const exempleContenu = {
'|Titre_Principal_1|': 'Guide complet pour choisir votre plaque personnalisée',
'|Introduction_1|': 'La personnalisation d\'une plaque signalétique représente un enjeu important pour votre entreprise. Cette solution permet de créer une identité visuelle.',
'|Texte_1|': 'Il est important de noter que les matériaux utilisés sont de qualité. Par ailleurs, la qualité est bonne. En effet, nos solutions sont bonnes et robustes. Par ailleurs, cela fonctionne bien.',
'|FAQ_Question_1|': 'Quels sont les matériaux disponibles ?',
'|FAQ_Reponse_1|': 'Nos matériaux sont de qualité : ils conviennent parfaitement. Ces solutions garantissent une qualité et un rendu optimal.'
};
console.log('📊 CONTENU ORIGINAL:');
Object.entries(exempleContenu).forEach(([tag, content]) => {
console.log(` ${tag}: "${content}"`);
});
// Analyser qualité originale
const fullOriginal = Object.values(exempleContenu).join(' ');
const qualiteOriginale = {
technical: analyzeTechnicalQuality(fullOriginal, ['dibond', 'aluminium', 'pmma', 'impression']),
transitions: analyzeTransitionFluidity(fullOriginal),
style: analyzeStyleConsistency(fullOriginal)
};
console.log(`\n📈 QUALITÉ ORIGINALE:`);
console.log(` 🔧 Technique: ${qualiteOriginale.technical.score}/100`);
console.log(` 🔗 Transitions: ${qualiteOriginale.transitions.score}/100`);
console.log(` 🎨 Style: ${qualiteOriginale.style.score}/100`);
try {
// ========================================
// TEST 1: COUCHE TECHNIQUE SEULE
// ========================================
console.log('\n🔧 TEST 1: Application couche technique');
const result1 = await applySelectiveLayer(exempleContenu, {
layerType: 'technical',
llmProvider: 'gpt4',
intensity: 0.9,
csvData: {
personality: { nom: 'Marc', style: 'technique' },
mc0: 'plaque personnalisée'
}
});
console.log(`✅ Résultat: ${result1.stats.enhanced}/${result1.stats.processed} éléments améliorés`);
console.log(` ⏱️ Durée: ${result1.stats.duration}ms`);
// ========================================
// TEST 2: STACK PRÉDÉFINI
// ========================================
console.log('\n📦 TEST 2: Application stack prédéfini');
// Lister stacks disponibles
const stacks = getAvailableStacks();
console.log(' Stacks disponibles:');
stacks.forEach(stack => {
console.log(` - ${stack.name}: ${stack.description}`);
});
const result2 = await applyPredefinedStack(exempleContenu, 'standardEnhancement', {
csvData: {
personality: {
nom: 'Sophie',
style: 'professionnel',
vocabulairePref: 'signalétique,personnalisation,qualité,expertise',
niveauTechnique: 'standard'
},
mc0: 'plaque personnalisée'
}
});
console.log(`✅ Stack standard: ${result2.stats.totalModifications} modifications totales`);
console.log(` 📊 Couches: ${result2.stats.layers.filter(l => l.success).length}/${result2.stats.layers.length} réussies`);
// ========================================
// TEST 3: COUCHES ADAPTATIVES
// ========================================
console.log('\n🧠 TEST 3: Application couches adaptatives');
const result3 = await applyAdaptiveLayers(exempleContenu, {
maxIntensity: 1.2,
analysisThreshold: 0.3,
csvData: {
personality: {
nom: 'Laurent',
style: 'commercial',
vocabulairePref: 'expertise,solution,performance,innovation',
niveauTechnique: 'accessible'
},
mc0: 'signalétique personnalisée'
}
});
if (result3.stats.adaptive) {
console.log(`✅ Adaptatif: ${result3.stats.layersApplied} couches appliquées`);
console.log(` 📊 Modifications: ${result3.stats.totalModifications}`);
}
// ========================================
// COMPARAISON QUALITÉ FINALE
// ========================================
console.log('\n📊 ANALYSE QUALITÉ FINALE:');
const contenuFinal = result2.content; // Prendre résultat stack standard
const fullEnhanced = Object.values(contenuFinal).join(' ');
const qualiteFinale = {
technical: analyzeTechnicalQuality(fullEnhanced, ['dibond', 'aluminium', 'pmma', 'impression']),
transitions: analyzeTransitionFluidity(fullEnhanced),
style: analyzeStyleConsistency(fullEnhanced, result2.csvData?.personality)
};
console.log('\n📈 AMÉLIORATION QUALITÉ:');
console.log(` 🔧 Technique: ${qualiteOriginale.technical.score}${qualiteFinale.technical.score} (+${(qualiteFinale.technical.score - qualiteOriginale.technical.score).toFixed(1)})`);
console.log(` 🔗 Transitions: ${qualiteOriginale.transitions.score}${qualiteFinale.transitions.score} (+${(qualiteFinale.transitions.score - qualiteOriginale.transitions.score).toFixed(1)})`);
console.log(` 🎨 Style: ${qualiteOriginale.style.score}${qualiteFinale.style.score} (+${(qualiteFinale.style.score - qualiteOriginale.style.score).toFixed(1)})`);
// Rapport détaillé
const rapport = generateImprovementReport(exempleContenu, contenuFinal, 'selective');
console.log('\n📋 RAPPORT AMÉLIORATION:');
console.log(` 📈 Amélioration moyenne: ${rapport.summary.averageImprovement.toFixed(1)}%`);
console.log(` ✅ Éléments améliorés: ${rapport.summary.elementsImproved}/${rapport.summary.elementsProcessed}`);
if (rapport.details.recommendations.length > 0) {
console.log(` 💡 Recommandations: ${rapport.details.recommendations.join(', ')}`);
}
// ========================================
// EXEMPLES DE TRANSFORMATION
// ========================================
console.log('\n✨ EXEMPLES DE TRANSFORMATION:');
console.log('\n📝 INTRODUCTION:');
console.log('AVANT:', `"${exempleContenu['|Introduction_1|']}"`);
console.log('APRÈS:', `"${contenuFinal['|Introduction_1|']}"`);
console.log('\n📝 TEXTE PRINCIPAL:');
console.log('AVANT:', `"${exempleContenu['|Texte_1|']}"`);
console.log('APRÈS:', `"${contenuFinal['|Texte_1|']}"`);
console.log('\n✅ === DÉMONSTRATION SELECTIVE MODULAIRE TERMINÉE ===\n');
return {
success: true,
originalQuality: qualiteOriginale,
finalQuality: qualiteFinale,
improvementReport: rapport
};
} catch (error) {
console.error('\n❌ ERREUR DÉMONSTRATION:', error.message);
console.error(error.stack);
return { success: false, error: error.message };
}
}
/**
* EXEMPLE D'INTÉGRATION AVEC PIPELINE EXISTANTE
*/
async function demoIntegrationExistante() {
console.log('\n🔗 === DÉMONSTRATION INTÉGRATION PIPELINE ===\n');
// Simuler contenu venant de ContentGeneration.js (Level 1)
const contenuExistant = {
'|Titre_H1_1|': 'Solutions de plaques personnalisées professionnelles',
'|Meta_Description_1|': 'Découvrez notre gamme complète de plaques personnalisées pour tous vos besoins de signalétique professionnelle.',
'|Introduction_1|': 'Dans le domaine de la signalétique personnalisée, le choix des matériaux et des techniques de fabrication constitue un élément déterminant.',
'|Texte_Avantages_1|': 'Les avantages de nos solutions incluent la durabilité, la résistance aux intempéries et la possibilité de personnalisation complète.'
};
console.log('💼 SCÉNARIO: Application selective post-génération normale');
try {
console.log('\n🎯 Étape 1: Contenu généré par pipeline Level 1');
console.log(' ✅ Contenu de base: qualité préservée');
console.log('\n🎯 Étape 2: Application selective enhancement modulaire');
// Test avec couche technique puis style
let contenuEnhanced = contenuExistant;
// Amélioration technique
const resultTechnique = await applySelectiveLayer(contenuEnhanced, {
layerType: 'technical',
llmProvider: 'gpt4',
intensity: 1.0,
analysisMode: true,
csvData: {
personality: { nom: 'Marc', style: 'technique' },
mc0: 'plaque personnalisée'
}
});
contenuEnhanced = resultTechnique.content;
console.log(` ✅ Couche technique: ${resultTechnique.stats.enhanced} éléments améliorés`);
// Amélioration style
const resultStyle = await applySelectiveLayer(contenuEnhanced, {
layerType: 'style',
llmProvider: 'mistral',
intensity: 0.8,
analysisMode: true,
csvData: {
personality: {
nom: 'Sophie',
style: 'professionnel moderne',
vocabulairePref: 'innovation,expertise,personnalisation,qualité',
niveauTechnique: 'accessible'
}
}
});
contenuEnhanced = resultStyle.content;
console.log(` ✅ Couche style: ${resultStyle.stats.enhanced} éléments stylisés`);
console.log('\n📊 RÉSULTAT FINAL INTÉGRÉ:');
Object.entries(contenuEnhanced).forEach(([tag, content]) => {
console.log(`\n ${tag}:`);
console.log(` ORIGINAL: "${contenuExistant[tag]}"`);
console.log(` ENHANCED: "${content}"`);
});
return {
success: true,
techniqueResult: resultTechnique,
styleResult: resultStyle,
finalContent: contenuEnhanced
};
} catch (error) {
console.error('❌ ERREUR INTÉGRATION:', error.message);
return { success: false, error: error.message };
}
}
/**
* TEST PERFORMANCE ET BENCHMARKS
*/
async function benchmarkPerformance() {
console.log('\n⚡ === BENCHMARK PERFORMANCE ===\n');
// Contenu de test de taille variable
const contenuTest = {};
// Générer contenu test
for (let i = 1; i <= 10; i++) {
contenuTest[`|Element_${i}|`] = `Ceci est un contenu de test numéro ${i} pour valider les performances du système selective enhancement modulaire. ` +
`Il est important de noter que ce contenu contient du vocabulaire générique et des répétitions. Par ailleurs, les transitions sont basiques. ` +
`En effet, la qualité technique est faible et le style est générique. Par ailleurs, cela nécessite des améliorations.`.repeat(Math.floor(i/3) + 1);
}
console.log(`📊 Contenu test: ${Object.keys(contenuTest).length} éléments`);
try {
const benchmarks = [];
// Test 1: Couche technique seule
const start1 = Date.now();
const result1 = await applySelectiveLayer(contenuTest, {
layerType: 'technical',
intensity: 0.8
});
benchmarks.push({
test: 'Couche technique seule',
duration: Date.now() - start1,
enhanced: result1.stats.enhanced,
processed: result1.stats.processed
});
// Test 2: Stack complet
const start2 = Date.now();
const result2 = await applyPredefinedStack(contenuTest, 'fullEnhancement');
benchmarks.push({
test: 'Stack complet (3 couches)',
duration: Date.now() - start2,
totalModifications: result2.stats.totalModifications,
layers: result2.stats.layers.length
});
// Test 3: Adaptatif
const start3 = Date.now();
const result3 = await applyAdaptiveLayers(contenuTest, { maxIntensity: 1.0 });
benchmarks.push({
test: 'Couches adaptatives',
duration: Date.now() - start3,
layersApplied: result3.stats.layersApplied,
totalModifications: result3.stats.totalModifications
});
console.log('\n📈 RÉSULTATS BENCHMARK:');
benchmarks.forEach(bench => {
console.log(`\n ${bench.test}:`);
console.log(` ⏱️ Durée: ${bench.duration}ms`);
if (bench.enhanced) console.log(` ✅ Améliorés: ${bench.enhanced}/${bench.processed}`);
if (bench.totalModifications) console.log(` 🔄 Modifications: ${bench.totalModifications}`);
if (bench.layers) console.log(` 📦 Couches: ${bench.layers}`);
if (bench.layersApplied) console.log(` 🧠 Couches adaptées: ${bench.layersApplied}`);
});
return { success: true, benchmarks };
} catch (error) {
console.error('❌ ERREUR BENCHMARK:', error.message);
return { success: false, error: error.message };
}
}
// Exécuter démonstrations si fichier appelé directement
if (require.main === module) {
(async () => {
await demoModularSelective();
await demoIntegrationExistante();
await benchmarkPerformance();
})().catch(console.error);
}
module.exports = {
demoModularSelective,
demoIntegrationExistante,
benchmarkPerformance
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/trace-wrap.js │
└────────────────────────────────────────────────────────────────────┘
*/
// lib/trace-wrap.js
const { tracer } = require('./trace.js');
const traced = (name, fn, attrs) => (...args) =>
tracer.run(name, () => fn(...args), attrs);
module.exports = {
traced
};