seogeneratorserver/code.js
StillHammer dbf1a3de8c Add technical plan for multi-format export system
Added plan.md with complete architecture for format-agnostic content generation:
- Support for Markdown, HTML, Plain Text, JSON formats
- New FormatExporter module with neutral data structure
- Integration strategy with existing ContentAssembly and ArticleStorage
- Bonus features: SEO metadata generation, readability scoring, WordPress Gutenberg format
- Implementation roadmap with 4 phases (6h total estimated)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 16:14:29 +08:00

19018 lines
655 KiB
JavaScript

/*
code.js — bundle concaténé
Généré: 2025-09-04T15:08:51.662Z
Source: lib
Fichiers: 44
Ordre: topo
*/
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/ErrorReporting.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: lib/error-reporting.js - CONVERTI POUR NODE.JS
// Description: Système de validation et rapport d'erreur
// ========================================
const { google } = require('googleapis');
const nodemailer = require('nodemailer');
const fs = require('fs').promises;
const path = require('path');
const pino = require('pino');
const pretty = require('pino-pretty');
const { PassThrough } = require('stream');
const WebSocket = require('ws');
// Configuration
const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
// WebSocket server for real-time logs
let wsServer;
const wsClients = new Set();
// Enhanced Pino logger configuration with real-time streaming and dated files
const now = new Date();
const timestamp = now.toISOString().slice(0, 10) + '_' +
now.toLocaleTimeString('fr-FR').replace(/:/g, '-');
const logFile = path.join(__dirname, '..', 'logs', `seo-generator-${timestamp}.log`);
const prettyStream = pretty({
colorize: true,
translateTime: 'HH:MM:ss.l',
ignore: 'pid,hostname',
});
const tee = new PassThrough();
tee.pipe(prettyStream).pipe(process.stdout);
// File destination with dated filename - FORCE DEBUG LEVEL
const fileDest = pino.destination({
dest: logFile,
mkdir: true,
sync: false,
minLength: 0 // Force immediate write even for small logs
});
tee.pipe(fileDest);
// Custom levels for Pino to include TRACE, PROMPT, and LLM
const customLevels = {
trace: 5, // Below debug (10)
debug: 10,
info: 20,
prompt: 25, // New level for prompts (between info and warn)
llm: 26, // New level for LLM interactions (between prompt and warn)
warn: 30,
error: 40,
fatal: 50
};
// Pino logger instance with enhanced configuration and custom levels
const logger = pino(
{
level: 'debug', // FORCE DEBUG LEVEL for file logging
base: undefined,
timestamp: pino.stdTimeFunctions.isoTime,
customLevels: customLevels,
useOnlyCustomLevels: true
},
tee
);
// Initialize WebSocket server
function initWebSocketServer() {
if (!wsServer) {
wsServer = new WebSocket.Server({ port: process.env.LOG_WS_PORT || 8081 });
wsServer.on('connection', (ws) => {
wsClients.add(ws);
logger.info('Client connected to log WebSocket');
ws.on('close', () => {
wsClients.delete(ws);
logger.info('Client disconnected from log WebSocket');
});
ws.on('error', (error) => {
logger.error('WebSocket error:', error.message);
wsClients.delete(ws);
});
});
logger.info(`Log WebSocket server started on port ${process.env.LOG_WS_PORT || 8081}`);
}
}
// Broadcast log to WebSocket clients
function broadcastLog(message, level) {
const logData = {
timestamp: new Date().toISOString(),
level: level.toUpperCase(),
message: message
};
wsClients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
try {
ws.send(JSON.stringify(logData));
} catch (error) {
logger.error('Failed to send log to WebSocket client:', error.message);
wsClients.delete(ws);
}
}
});
}
// 🔄 NODE.JS : Google Sheets API setup (remplace SpreadsheetApp)
let sheets;
let auth;
async function initGoogleSheets() {
if (!sheets) {
// Configuration auth Google Sheets API
// Pour la démo, on utilise une clé de service (à configurer)
auth = new google.auth.GoogleAuth({
keyFile: process.env.GOOGLE_CREDENTIALS_PATH, // Chemin vers fichier JSON credentials
scopes: ['https://www.googleapis.com/auth/spreadsheets']
});
sheets = google.sheets({ version: 'v4', auth });
}
return sheets;
}
async function logSh(message, level = 'INFO') {
// Initialize WebSocket server if not already done
if (!wsServer) {
initWebSocketServer();
}
// Convert level to lowercase for Pino
const pinoLevel = level.toLowerCase();
// Enhanced trace metadata for hierarchical logging
const traceData = {};
if (message.includes('▶') || message.includes('✔') || message.includes('✖') || message.includes('•')) {
traceData.trace = true;
traceData.evt = message.includes('▶') ? 'span.start' :
message.includes('✔') ? 'span.end' :
message.includes('✖') ? 'span.error' : 'span.event';
}
// Log with Pino (handles console output with pretty formatting and file logging)
switch (pinoLevel) {
case 'error':
logger.error(traceData, message);
break;
case 'warning':
case 'warn':
logger.warn(traceData, message);
break;
case 'debug':
logger.debug(traceData, message);
break;
case 'trace':
logger.trace(traceData, message);
break;
case 'prompt':
logger.prompt(traceData, message);
break;
case 'llm':
logger.llm(traceData, message);
break;
default:
logger.info(traceData, message);
}
// Broadcast to WebSocket clients for real-time viewing
broadcastLog(message, level);
// Force immediate flush to ensure real-time display and prevent log loss
logger.flush();
// Log to Google Sheets if enabled (async, non-blocking)
if (process.env.ENABLE_SHEETS_LOGGING === 'true') {
setImmediate(() => {
logToGoogleSheets(message, level).catch(err => {
// Silent fail for Google Sheets logging to avoid recursion
});
});
}
}
// Fonction pour déterminer si on doit logger en console
function shouldLogToConsole(messageLevel, configLevel) {
const levels = { DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3 };
return levels[messageLevel] >= levels[configLevel];
}
// Log to file is now handled by Pino transport
// This function is kept for compatibility but does nothing
async function logToFile(message, level) {
// Pino handles file logging via transport configuration
// This function is deprecated and kept for compatibility only
}
// 🔄 NODE.JS : Log vers Google Sheets (version async)
async function logToGoogleSheets(message, level) {
try {
const sheetsApi = await initGoogleSheets();
const values = [[
new Date().toISOString(),
level,
message,
'Node.js workflow'
]];
await sheetsApi.spreadsheets.values.append({
spreadsheetId: SHEET_ID,
range: 'Logs!A:D',
valueInputOption: 'RAW',
insertDataOption: 'INSERT_ROWS',
resource: { values }
});
} catch (error) {
logSh('Échec log Google Sheets: ' + error.message, 'WARNING'); // Using logSh instead of console.warn
}
}
// 🔄 NODE.JS : Version simplifiée cleanLogSheet
async function cleanLogSheet() {
try {
logSh('🧹 Nettoyage logs...', 'INFO'); // Using logSh instead of console.log
// 1. Nettoyer fichiers logs locaux (garder 7 derniers jours)
await cleanLocalLogs();
// 2. Nettoyer Google Sheets si activé
if (process.env.ENABLE_SHEETS_LOGGING === 'true') {
await cleanGoogleSheetsLogs();
}
logSh('✅ Logs nettoyés', 'INFO'); // Using logSh instead of console.log
} catch (error) {
logSh('Erreur nettoyage logs: ' + error.message, 'ERROR'); // Using logSh instead of console.error
}
}
async function cleanLocalLogs() {
try {
// Note: With Pino, log files are managed differently
// This function is kept for compatibility with Google Sheets logs cleanup
// Pino log rotation should be handled by external tools like logrotate
// For now, we keep the basic cleanup for any remaining old log files
const logsDir = path.join(__dirname, '../logs');
try {
const files = await fs.readdir(logsDir);
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - 7); // Garder 7 jours
for (const file of files) {
if (file.endsWith('.log')) {
const filePath = path.join(logsDir, file);
const stats = await fs.stat(filePath);
if (stats.mtime < cutoffDate) {
await fs.unlink(filePath);
logSh(`🗑️ Supprimé log ancien: ${file}`, 'INFO');
}
}
}
} catch (error) {
// Directory might not exist, that's fine
}
} catch (error) {
// Silent fail
}
}
async function cleanGoogleSheetsLogs() {
try {
const sheetsApi = await initGoogleSheets();
// Clear + remettre headers
await sheetsApi.spreadsheets.values.clear({
spreadsheetId: SHEET_ID,
range: 'Logs!A:D'
});
await sheetsApi.spreadsheets.values.update({
spreadsheetId: SHEET_ID,
range: 'Logs!A1:D1',
valueInputOption: 'RAW',
resource: {
values: [['Timestamp', 'Level', 'Message', 'Source']]
}
});
} catch (error) {
logSh('Échec nettoyage Google Sheets: ' + error.message, 'WARNING'); // Using logSh instead of console.warn
}
}
// ============= VALIDATION PRINCIPALE - IDENTIQUE =============
function validateWorkflowIntegrity(elements, generatedContent, finalXML, csvData) {
logSh('🔍 >>> VALIDATION INTÉGRITÉ WORKFLOW <<<', 'INFO'); // Using logSh instead of console.log
const errors = [];
const warnings = [];
const stats = {
elementsExtracted: elements.length,
contentGenerated: Object.keys(generatedContent).length,
tagsReplaced: 0,
tagsRemaining: 0
};
// TEST 1: Détection tags dupliqués
const duplicateCheck = detectDuplicateTags(elements);
if (duplicateCheck.hasDuplicates) {
errors.push({
type: 'DUPLICATE_TAGS',
severity: 'HIGH',
message: `Tags dupliqués détectés: ${duplicateCheck.duplicates.join(', ')}`,
impact: 'Certains contenus ne seront pas remplacés dans le XML final',
suggestion: 'Vérifier le template XML pour corriger la structure'
});
}
// TEST 2: Cohérence éléments extraits vs générés
const missingGeneration = elements.filter(el => !generatedContent[el.originalTag]);
if (missingGeneration.length > 0) {
errors.push({
type: 'MISSING_GENERATION',
severity: 'HIGH',
message: `${missingGeneration.length} éléments extraits mais non générés`,
details: missingGeneration.map(el => el.originalTag),
impact: 'Contenu incomplet dans le XML final'
});
}
// TEST 3: Tags non remplacés dans XML final
const remainingTags = (finalXML.match(/\|[^|]*\|/g) || []);
stats.tagsRemaining = remainingTags.length;
if (remainingTags.length > 0) {
errors.push({
type: 'UNREPLACED_TAGS',
severity: 'HIGH',
message: `${remainingTags.length} tags non remplacés dans le XML final`,
details: remainingTags.slice(0, 5),
impact: 'XML final contient des placeholders non remplacés'
});
}
// TEST 4: Variables CSV manquantes
const missingVars = detectMissingCSVVariables(csvData);
if (missingVars.length > 0) {
warnings.push({
type: 'MISSING_CSV_VARIABLES',
severity: 'MEDIUM',
message: `Variables CSV manquantes: ${missingVars.join(', ')}`,
impact: 'Système de génération de mots-clés automatique activé'
});
}
// TEST 5: Qualité génération IA
const generationQuality = assessGenerationQuality(generatedContent);
if (generationQuality.errorRate > 0.1) {
warnings.push({
type: 'GENERATION_QUALITY',
severity: 'MEDIUM',
message: `${(generationQuality.errorRate * 100).toFixed(1)}% d'erreurs de génération IA`,
impact: 'Qualité du contenu potentiellement dégradée'
});
}
// CALCUL STATS FINALES
stats.tagsReplaced = elements.length - remainingTags.length;
stats.successRate = stats.elementsExtracted > 0 ?
((stats.tagsReplaced / elements.length) * 100).toFixed(1) : '100';
const report = {
timestamp: new Date().toISOString(),
csvData: { mc0: csvData.mc0, t0: csvData.t0 },
stats: stats,
errors: errors,
warnings: warnings,
status: errors.length === 0 ? 'SUCCESS' : 'ERROR'
};
const logLevel = report.status === 'SUCCESS' ? 'INFO' : 'ERROR';
logSh(`✅ Validation terminée: ${report.status} (${errors.length} erreurs, ${warnings.length} warnings)`, 'INFO'); // Using logSh instead of console.log
// ENVOYER RAPPORT SI ERREURS (async en arrière-plan)
if (errors.length > 0 || warnings.length > 2) {
sendErrorReport(report).catch(err => {
logSh('Erreur envoi rapport: ' + err.message, 'ERROR'); // Using logSh instead of console.error
});
}
return report;
}
// ============= HELPERS - IDENTIQUES =============
function detectDuplicateTags(elements) {
const tagCounts = {};
const duplicates = [];
elements.forEach(element => {
const tag = element.originalTag;
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
if (tagCounts[tag] === 2) {
duplicates.push(tag);
logSh(`❌ DUPLICATE détecté: ${tag}`, 'ERROR'); // Using logSh instead of console.error
}
});
return {
hasDuplicates: duplicates.length > 0,
duplicates: duplicates,
counts: tagCounts
};
}
function detectMissingCSVVariables(csvData) {
const missing = [];
if (!csvData.mcPlus1 || csvData.mcPlus1.split(',').length < 4) {
missing.push('MC+1 (insuffisant)');
}
if (!csvData.tPlus1 || csvData.tPlus1.split(',').length < 4) {
missing.push('T+1 (insuffisant)');
}
if (!csvData.lPlus1 || csvData.lPlus1.split(',').length < 4) {
missing.push('L+1 (insuffisant)');
}
return missing;
}
function assessGenerationQuality(generatedContent) {
let errorCount = 0;
let totalCount = Object.keys(generatedContent).length;
Object.values(generatedContent).forEach(content => {
if (content && (
content.includes('[ERREUR') ||
content.includes('ERROR') ||
content.length < 10
)) {
errorCount++;
}
});
return {
errorRate: totalCount > 0 ? errorCount / totalCount : 0,
totalGenerated: totalCount,
errorsFound: errorCount
};
}
// 🔄 NODE.JS : Email avec nodemailer (remplace MailApp)
async function sendErrorReport(report) {
try {
logSh('📧 Envoi rapport d\'erreur par email...', 'INFO'); // Using logSh instead of console.log
// Configuration nodemailer (Gmail par exemple)
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER, // 'your-email@gmail.com'
pass: process.env.EMAIL_APP_PASSWORD // App password Google
}
});
const subject = `Erreur Workflow SEO Node.js - ${report.status} - ${report.csvData.mc0}`;
const htmlBody = createHTMLReport(report);
const mailOptions = {
from: process.env.EMAIL_USER,
to: 'alexistrouve.pro@gmail.com',
subject: subject,
html: htmlBody,
attachments: [{
filename: `error-report-${Date.now()}.json`,
content: JSON.stringify(report, null, 2),
contentType: 'application/json'
}]
};
await transporter.sendMail(mailOptions);
logSh('✅ Rapport d\'erreur envoyé par email', 'INFO'); // Using logSh instead of console.log
} catch (error) {
logSh(`❌ Échec envoi email: ${error.message}`, 'ERROR'); // Using logSh instead of console.error
}
}
// ============= HTML REPORT - IDENTIQUE =============
function createHTMLReport(report) {
const statusColor = report.status === 'SUCCESS' ? '#28a745' : '#dc3545';
let html = `
<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
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ 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');
// ============= CONFIGURATION CENTRALISÉE =============
const LLM_CONFIG = {
openai: {
apiKey: process.env.OPENAI_API_KEY || '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',
endpoint: 'https://api.openai.com/v1/chat/completions',
model: 'gpt-4o-mini',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
timeout: 300000, // 5 minutes
retries: 3
},
claude: {
apiKey: process.env.CLAUDE_API_KEY || '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',
endpoint: 'https://api.anthropic.com/v1/messages',
model: 'claude-sonnet-4-20250514',
headers: {
'x-api-key': '{API_KEY}',
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01'
},
temperature: 0.7,
timeout: 300000, // 5 minutes
retries: 6
},
gemini: {
apiKey: process.env.GEMINI_API_KEY || 'AIzaSyAMzmIGbW5nJlBG5Qyr35sdjb3U2bIBtoE',
endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent',
model: 'gemini-2.5-flash',
headers: {
'Content-Type': 'application/json'
},
temperature: 0.7,
maxTokens: 6000,
timeout: 300000, // 5 minutes
retries: 3
},
deepseek: {
apiKey: process.env.DEEPSEEK_API_KEY || 'sk-6e02bc9513884bb8b92b9920524e17b5',
endpoint: 'https://api.deepseek.com/v1/chat/completions',
model: 'deepseek-chat',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
timeout: 300000, // 5 minutes
retries: 3
},
moonshot: {
apiKey: process.env.MOONSHOT_API_KEY || 'sk-zU9gyNkux2zcsj61cdKfztuP1Jozr6lFJ9viUJRPD8p8owhL',
endpoint: 'https://api.moonshot.ai/v1/chat/completions',
model: 'moonshot-v1-32k',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
timeout: 300000, // 5 minutes
retries: 3
},
mistral: {
apiKey: process.env.MISTRAL_API_KEY || 'wESikMCIuixajSH8WHCiOV2z5sevgmVF',
endpoint: 'https://api.mistral.ai/v1/chat/completions',
model: 'mistral-small-latest',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
max_tokens: 5000,
temperature: 0.7,
timeout: 300000, // 5 minutes
retries: 3
}
};
// ============= HELPER FUNCTIONS =============
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// ============= INTERFACE UNIVERSELLE =============
/**
* Fonction principale pour appeler n'importe quel LLM
* @param {string} llmProvider - claude|openai|gemini|deepseek|moonshot|mistral
* @param {string} prompt - Le prompt à envoyer
* @param {object} options - Options personnalisées (température, tokens, etc.)
* @param {object} personality - Personnalité pour contexte système
* @returns {Promise<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 'deepseek':
case 'moonshot':
case 'mistral':
return {
model: config.model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
],
max_tokens: maxTokens,
temperature: temperature,
stream: false
};
case 'claude':
return {
model: config.model,
max_tokens: maxTokens,
temperature: temperature,
system: systemPrompt,
messages: [
{ role: 'user', content: prompt }
]
};
case 'gemini':
return {
contents: [{
parts: [{
text: `${systemPrompt}\n\n${prompt}`
}]
}],
generationConfig: {
temperature: temperature,
maxOutputTokens: maxTokens
}
};
default:
throw new Error(`Format de requête non supporté pour ${provider}`);
}
}
// ============= APPELS AVEC RETRY =============
async function callWithRetry(provider, requestData, config) {
let lastError;
for (let attempt = 1; attempt <= config.retries; attempt++) {
try {
logSh(`🔄 Tentative ${attempt}/${config.retries} pour ${provider.toUpperCase()}`, 'DEBUG');
// Préparer les headers avec la clé API
const headers = {};
Object.keys(config.headers).forEach(key => {
headers[key] = config.headers[key].replace('{API_KEY}', config.apiKey);
});
// URL avec clé API pour Gemini (cas spécial)
let url = config.endpoint;
if (provider === 'gemini') {
url += `?key=${config.apiKey}`;
}
const options = {
method: 'POST',
headers: headers,
body: JSON.stringify(requestData),
timeout: config.timeout
};
const response = await fetch(url, options);
const responseText = await response.text();
if (response.ok) {
return JSON.parse(responseText);
} else if (response.status === 429) {
// Rate limiting - attendre plus longtemps
const waitTime = Math.pow(2, attempt) * 1000; // Exponential backoff
logSh(`⏳ Rate limit ${provider.toUpperCase()}, attente ${waitTime}ms`, 'WARNING');
await sleep(waitTime);
continue;
} else {
throw new Error(`HTTP ${response.status}: ${responseText}`);
}
} catch (error) {
lastError = error;
if (attempt < config.retries) {
const waitTime = 1000 * attempt;
logSh(`⚠ Erreur tentative ${attempt}: ${error.toString()}, retry dans ${waitTime}ms`, 'WARNING');
await sleep(waitTime);
}
}
}
throw new Error(`Échec après ${config.retries} tentatives: ${lastError.toString()}`);
}
// ============= PARSING DES RÉPONSES =============
function parseResponse(provider, responseData) {
try {
switch (provider) {
case 'openai':
case 'deepseek':
case 'moonshot':
case 'mistral':
return responseData.choices[0].message.content.trim();
case 'claude':
return responseData.content[0].text.trim();
case 'gemini':
const candidate = responseData.candidates[0];
// Vérifications multiples pour Gemini 2.5
if (candidate && candidate.content && candidate.content.parts && candidate.content.parts[0] && candidate.content.parts[0].text) {
return candidate.content.parts[0].text.trim();
} else if (candidate && candidate.text) {
return candidate.text.trim();
} else if (candidate && candidate.content && candidate.content.text) {
return candidate.content.text.trim();
} else {
// Debug : logger la structure complète
logSh('Gemini structure complète: ' + JSON.stringify(responseData), 'DEBUG');
return '[Gemini: pas de texte généré - problème modèle]';
}
default:
throw new Error(`Parser non supporté pour ${provider}`);
}
} catch (error) {
logSh(`❌ Erreur parsing ${provider}: ${error.toString()}`, 'ERROR');
logSh(`Response brute: ${JSON.stringify(responseData)}`, 'DEBUG');
throw new Error(`Impossible de parser la réponse ${provider}: ${error.toString()}`);
}
}
// ============= GESTION DES STATISTIQUES =============
async function recordUsageStats(provider, promptTokens, responseTokens, duration, error = null) {
try {
// TODO: Adapter selon votre système de stockage Node.js
// Peut être une base de données, un fichier, MongoDB, etc.
const statsData = {
timestamp: new Date(),
provider: provider,
model: LLM_CONFIG[provider].model,
promptTokens: promptTokens,
responseTokens: responseTokens,
duration: duration,
error: error || ''
};
// Exemple: log vers console ou fichier
logSh(`📊 Stats: ${JSON.stringify(statsData)}`, 'DEBUG');
// TODO: Implémenter sauvegarde réelle (DB, fichier, etc.)
} catch (statsError) {
// Ne pas faire planter le workflow si les stats échouent
logSh(`⚠ Erreur enregistrement stats: ${statsError.toString()}`, 'WARNING');
}
}
// ============= FONCTIONS UTILITAIRES =============
/**
* Tester la connectivité de tous les LLMs
*/
async function testAllLLMs() {
const testPrompt = "Dis bonjour en 5 mots maximum.";
const results = {};
const allProviders = Object.keys(LLM_CONFIG);
for (const provider of allProviders) {
try {
logSh(`🧪 Test ${provider}...`, 'INFO');
const response = await callLLM(provider, testPrompt);
results[provider] = {
status: 'SUCCESS',
response: response,
model: LLM_CONFIG[provider].model
};
} catch (error) {
results[provider] = {
status: 'ERROR',
error: error.toString(),
model: LLM_CONFIG[provider].model
};
}
// Petit délai entre tests
await sleep(500);
}
logSh(`📊 Tests terminés: ${JSON.stringify(results, null, 2)}`, 'INFO');
return results;
}
/**
* Obtenir les providers disponibles (avec clés API valides)
*/
function getAvailableProviders() {
const available = [];
Object.keys(LLM_CONFIG).forEach(provider => {
const config = LLM_CONFIG[provider];
if (config.apiKey && !config.apiKey.startsWith('VOTRE_CLE_')) {
available.push(provider);
}
});
return available;
}
/**
* Obtenir des statistiques d'usage par provider
*/
async function getUsageStats() {
try {
// TODO: Adapter selon votre système de stockage
// Pour l'instant retourne un message par défaut
return { message: 'Statistiques non implémentées en Node.js' };
} catch (error) {
return { error: error.toString() };
}
}
// ============= MIGRATION DE L'ANCIEN CODE =============
/**
* Fonction de compatibilité pour remplacer votre ancien callOpenAI()
* Maintient la même signature pour ne pas casser votre code existant
*/
async function callOpenAI(prompt, personality) {
return await callLLM('openai', prompt, {}, personality);
}
// ============= EXPORTS POUR TESTS =============
/**
* Fonction de test rapide
*/
async function testLLMManager() {
logSh('🚀 Test du LLM Manager Node.js...', 'INFO');
// Test des providers disponibles
const available = getAvailableProviders();
logSh('Providers disponibles: ' + available.join(', ') + ' (' + available.length + '/6)', 'INFO');
// Test d'appel simple sur chaque provider disponible
for (const provider of available) {
try {
logSh(`🧪 Test ${provider}...`, 'DEBUG');
const startTime = Date.now();
const response = await callLLM(provider, 'Dis juste "Test OK"');
const duration = Date.now() - startTime;
logSh(`✅ Test ${provider} réussi: "${response}" (${duration}ms)`, 'INFO');
} catch (error) {
logSh(`❌ Test ${provider} échoué: ${error.toString()}`, 'ERROR');
}
// Petit délai pour éviter rate limits
await sleep(500);
}
// Test spécifique OpenAI (compatibilité avec ancien code)
try {
logSh('🎯 Test spécifique OpenAI (compatibilité)...', 'DEBUG');
const response = await callLLM('openai', 'Dis juste "Test OK"');
logSh('✅ Test OpenAI compatibilité: ' + response, 'INFO');
} catch (error) {
logSh('❌ Test OpenAI compatibilité échoué: ' + error.toString(), 'ERROR');
}
// Afficher les stats d'usage
try {
logSh('📊 Récupération statistiques d\'usage...', 'DEBUG');
const stats = await getUsageStats();
if (stats.error) {
logSh('⚠ Erreur récupération stats: ' + stats.error, 'WARNING');
} else if (stats.message) {
logSh('📊 Stats: ' + stats.message, 'INFO');
} else {
// Formatter les stats pour les logs
Object.keys(stats).forEach(provider => {
const s = stats[provider];
logSh(`📈 ${provider}: ${s.calls} appels, ${s.successRate}% succès, ${s.avgDuration}ms moyen`, 'INFO');
});
}
} catch (error) {
logSh('❌ Erreur lors de la récupération des stats: ' + error.toString(), 'ERROR');
}
// Résumé final
const workingCount = available.length;
const totalProviders = Object.keys(LLM_CONFIG).length;
if (workingCount === totalProviders) {
logSh(`✅ Test LLM Manager COMPLET: ${workingCount}/${totalProviders} providers opérationnels`, 'INFO');
} else if (workingCount >= 2) {
logSh(`✅ Test LLM Manager PARTIEL: ${workingCount}/${totalProviders} providers opérationnels (suffisant pour DNA Mixing)`, 'INFO');
} else {
logSh(`❌ Test LLM Manager INSUFFISANT: ${workingCount}/${totalProviders} providers opérationnels (minimum 2 requis)`, 'ERROR');
}
logSh('🏁 Test LLM Manager terminé', 'INFO');
}
/**
* Version complète avec test de tous les providers (même non configurés)
*/
async function testLLMManagerComplete() {
logSh('🚀 Test COMPLET du LLM Manager (tous providers)...', 'INFO');
const allProviders = Object.keys(LLM_CONFIG);
logSh(`Providers configurés: ${allProviders.join(', ')}`, 'INFO');
const results = {
configured: 0,
working: 0,
failed: 0
};
for (const provider of allProviders) {
const config = LLM_CONFIG[provider];
// Vérifier si configuré
if (!config.apiKey || config.apiKey.startsWith('VOTRE_CLE_')) {
logSh(`⚙️ ${provider}: NON CONFIGURÉ (clé API manquante)`, 'WARNING');
continue;
}
results.configured++;
try {
logSh(`🧪 Test ${provider} (${config.model})...`, 'DEBUG');
const startTime = Date.now();
const response = await callLLM(provider, 'Réponds "OK" seulement.', { maxTokens: 100 });
const duration = Date.now() - startTime;
results.working++;
logSh(`${provider}: "${response.trim()}" (${duration}ms)`, 'INFO');
} catch (error) {
results.failed++;
logSh(`${provider}: ${error.toString()}`, 'ERROR');
}
// Délai entre tests
await sleep(700);
}
// Résumé final complet
logSh(`📊 RÉSUMÉ FINAL:`, 'INFO');
logSh(` • Providers total: ${allProviders.length}`, 'INFO');
logSh(` • Configurés: ${results.configured}`, 'INFO');
logSh(` • Fonctionnels: ${results.working}`, 'INFO');
logSh(` • En échec: ${results.failed}`, 'INFO');
const status = results.working >= 4 ? 'EXCELLENT' :
results.working >= 2 ? 'BON' : 'INSUFFISANT';
logSh(`🏆 STATUS: ${status} (${results.working} LLMs opérationnels)`,
status === 'INSUFFISANT' ? 'ERROR' : 'INFO');
logSh('🏁 Test LLM Manager COMPLET terminé', 'INFO');
return {
total: allProviders.length,
configured: results.configured,
working: results.working,
failed: results.failed,
status: status
};
}
// ============= EXPORTS MODULE =============
module.exports = {
callLLM,
callOpenAI,
testAllLLMs,
getAvailableProviders,
getUsageStats,
testLLMManager,
testLLMManagerComplete,
LLM_CONFIG
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/ElementExtraction.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: lib/element-extraction.js - CONVERTI POUR NODE.JS
// Description: Extraction et parsing des éléments XML
// ========================================
// 🔄 NODE.JS IMPORTS
const { logSh } = require('./ErrorReporting');
// ============= EXTRACTION PRINCIPALE =============
async function extractElements(xmlTemplate, csvData) {
try {
await logSh('Extraction éléments avec séparation tag/contenu...', 'DEBUG');
const regex = /\|([^|]+)\|/g;
const elements = [];
let match;
while ((match = regex.exec(xmlTemplate)) !== null) {
const fullMatch = match[1]; // Ex: "Titre_H1_1{{T0}}" ou "Titre_H3_3{{MC+1_3}}"
// Séparer nom du tag et variables
const nameMatch = fullMatch.match(/^([^{]+)/);
const variablesMatch = fullMatch.match(/\{\{([^}]+)\}\}/g);
// FIX REGEX INSTRUCTIONS - Enlever d'abord les {{variables}} puis chercher {instructions}
const withoutVariables = fullMatch.replace(/\{\{[^}]+\}\}/g, '');
const instructionsMatch = withoutVariables.match(/\{([^}]+)\}/);
const tagName = nameMatch ? nameMatch[1].trim() : fullMatch.split('{')[0];
// TAG PUR (sans variables)
const pureTag = `|${tagName}|`;
// RÉSOUDRE le contenu des variables
const resolvedContent = resolveVariablesContent(variablesMatch, csvData);
elements.push({
originalTag: pureTag, // ← TAG PUR : |Titre_H3_3|
name: tagName, // ← Titre_H3_3
variables: variablesMatch || [], // ← [{{MC+1_3}}]
resolvedContent: resolvedContent, // ← "Plaque de rue en aluminium"
instructions: instructionsMatch ? instructionsMatch[1] : null,
type: getElementType(tagName),
originalFullMatch: fullMatch // ← Backup si besoin
});
await logSh(`Tag séparé: ${pureTag} → "${resolvedContent}"`, 'DEBUG');
}
await logSh(`${elements.length} éléments extraits avec séparation`, 'INFO');
return elements;
} catch (error) {
await logSh(`Erreur extractElements: ${error}`, 'ERROR');
return [];
}
}
// ============= RÉSOLUTION VARIABLES - IDENTIQUE =============
function resolveVariablesContent(variablesMatch, csvData) {
if (!variablesMatch || variablesMatch.length === 0) {
return ""; // Pas de variables à résoudre
}
let resolvedContent = "";
variablesMatch.forEach(variable => {
const cleanVar = variable.replace(/[{}]/g, ''); // Enlever {{ }}
switch (cleanVar) {
case 'T0':
resolvedContent += csvData.t0;
break;
case 'MC0':
resolvedContent += csvData.mc0;
break;
case 'T-1':
resolvedContent += csvData.tMinus1;
break;
case 'L-1':
resolvedContent += csvData.lMinus1;
break;
default:
// Gérer MC+1_1, MC+1_2, etc.
if (cleanVar.startsWith('MC+1_')) {
const index = parseInt(cleanVar.split('_')[1]) - 1;
const mcPlus1 = csvData.mcPlus1.split(',').map(s => s.trim());
resolvedContent += mcPlus1[index] || `[${cleanVar} non défini]`;
}
else if (cleanVar.startsWith('T+1_')) {
const index = parseInt(cleanVar.split('_')[1]) - 1;
const tPlus1 = csvData.tPlus1.split(',').map(s => s.trim());
resolvedContent += tPlus1[index] || `[${cleanVar} non défini]`;
}
else if (cleanVar.startsWith('L+1_')) {
const index = parseInt(cleanVar.split('_')[1]) - 1;
const lPlus1 = csvData.lPlus1.split(',').map(s => s.trim());
resolvedContent += lPlus1[index] || `[${cleanVar} non défini]`;
}
else {
resolvedContent += `[${cleanVar} non résolu]`;
}
break;
}
});
return resolvedContent;
}
// ============= CLASSIFICATION ÉLÉMENTS - IDENTIQUE =============
function getElementType(name) {
if (name.includes('Titre_H1')) return 'titre_h1';
if (name.includes('Titre_H2')) return 'titre_h2';
if (name.includes('Titre_H3')) return 'titre_h3';
if (name.includes('Intro_')) return 'intro';
if (name.includes('Txt_')) return 'texte';
if (name.includes('Faq_q')) return 'faq_question';
if (name.includes('Faq_a')) return 'faq_reponse';
if (name.includes('Faq_H3')) return 'faq_titre';
return 'autre';
}
// ============= GÉNÉRATION SÉQUENTIELLE - ADAPTÉE =============
async function generateAllContent(elements, csvData, xmlTemplate) {
await logSh(`Début génération pour ${elements.length} éléments`, 'INFO');
const generatedContent = {};
for (let index = 0; index < elements.length; index++) {
const element = elements[index];
try {
await logSh(`Élément ${index + 1}/${elements.length}: ${element.name}`, 'DEBUG');
const prompt = createPromptForElement(element, csvData);
await logSh(`Prompt créé: ${prompt}`, 'DEBUG');
// 🔄 NODE.JS : Import callOpenAI depuis LLM manager
const { callLLM } = require('./LLMManager');
const content = await callLLM('openai', prompt, {}, csvData.personality);
await logSh(`Contenu reçu: ${content}`, 'DEBUG');
generatedContent[element.originalTag] = content;
// 🔄 NODE.JS : Pas de Utilities.sleep(), les appels API gèrent leur rate limiting
} catch (error) {
await logSh(`ERREUR élément ${element.name}: ${error.toString()}`, 'ERROR');
generatedContent[element.originalTag] = `[Erreur génération: ${element.name}]`;
}
}
await logSh(`Génération terminée. ${Object.keys(generatedContent).length} éléments`, 'INFO');
return generatedContent;
}
// ============= PARSING STRUCTURE - IDENTIQUE =============
function parseElementStructure(element) {
// NETTOYER le nom : enlever <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/trace.js │
└────────────────────────────────────────────────────────────────────┘
*/
// lib/trace.js
const { AsyncLocalStorage } = require('node:async_hooks');
const { randomUUID } = require('node:crypto');
const { logSh } = require('./ErrorReporting');
const als = new AsyncLocalStorage();
function now() { return performance.now(); }
function dur(ms) {
if (ms < 1e3) return `${ms.toFixed(1)}ms`;
const s = ms / 1e3;
return s < 60 ? `${s.toFixed(2)}s` : `${(s/60).toFixed(2)}m`;
}
class Span {
constructor({ name, parent = null, attrs = {} }) {
this.id = randomUUID();
this.name = name;
this.parent = parent;
this.children = [];
this.attrs = attrs;
this.start = now();
this.end = null;
this.status = 'ok';
this.error = null;
}
pathNames() {
const names = [];
let cur = this;
while (cur) { names.unshift(cur.name); cur = cur.parent; }
return names.join(' > ');
}
finish() { this.end = now(); }
duration() { return (this.end ?? now()) - this.start; }
}
class Tracer {
constructor() {
this.rootSpans = [];
}
current() { return als.getStore(); }
async startSpan(name, attrs = {}) {
const parent = this.current();
const span = new Span({ name, parent, attrs });
if (parent) parent.children.push(span);
else this.rootSpans.push(span);
// Formater les paramètres pour affichage
const paramsStr = this.formatParams(attrs);
await logSh(`${name}${paramsStr}`, 'TRACE');
return span;
}
async run(name, fn, attrs = {}) {
const parent = this.current();
const span = await this.startSpan(name, attrs);
return await als.run(span, async () => {
try {
const res = await fn();
span.finish();
const paramsStr = this.formatParams(span.attrs);
await logSh(`${name}${paramsStr} (${dur(span.duration())})`, 'TRACE');
return res;
} catch (err) {
span.status = 'error';
span.error = { message: err?.message, stack: err?.stack };
span.finish();
const paramsStr = this.formatParams(span.attrs);
await logSh(`${name}${paramsStr} FAILED (${dur(span.duration())})`, 'ERROR');
await logSh(`Stack trace: ${span.error.message}`, 'ERROR');
if (span.error.stack) {
const stackLines = span.error.stack.split('\n').slice(1, 6); // Première 5 lignes du stack
for (const line of stackLines) {
await logSh(` ${line.trim()}`, 'ERROR');
}
}
throw err;
}
});
}
async event(msg, extra = {}) {
const span = this.current();
const data = { trace: true, evt: 'span.event', ...extra };
if (span) {
data.span = span.id;
data.path = span.pathNames();
data.since_ms = +( (now() - span.start).toFixed(1) );
}
await logSh(`${msg}`, 'TRACE');
}
async annotate(fields = {}) {
const span = this.current();
if (span) Object.assign(span.attrs, fields);
await logSh('… annotate', 'TRACE');
}
formatParams(attrs = {}) {
const params = Object.entries(attrs)
.filter(([key, value]) => value !== undefined && value !== null)
.map(([key, value]) => {
// Tronquer les valeurs trop longues
const strValue = String(value);
const truncated = strValue.length > 50 ? strValue.substring(0, 47) + '...' : strValue;
return `${key}=${truncated}`;
});
return params.length > 0 ? `(${params.join(', ')})` : '';
}
printSummary() {
const lines = [];
const draw = (node, depth = 0) => {
const pad = ' '.repeat(depth);
const icon = node.status === 'error' ? '✖' : '✔';
lines.push(`${pad}${icon} ${node.name} (${dur(node.duration())})`);
if (Object.keys(node.attrs ?? {}).length) {
lines.push(`${pad} attrs: ${JSON.stringify(node.attrs)}`);
}
for (const ch of node.children) draw(ch, depth + 1);
if (node.status === 'error' && node.error?.message) {
lines.push(`${pad} error: ${node.error.message}`);
if (node.error.stack) {
const stackLines = String(node.error.stack || '').split('\n').slice(1, 4).map(s => s.trim());
if (stackLines.length) {
lines.push(`${pad} stack:`);
stackLines.forEach(line => {
if (line) lines.push(`${pad} ${line}`);
});
}
}
}
};
for (const r of this.rootSpans) draw(r, 0);
const summary = lines.join('\n');
logSh(`\n—— TRACE SUMMARY ——\n${summary}\n—— END TRACE ——`, 'INFO');
return summary;
}
}
const tracer = new Tracer();
module.exports = {
Span,
Tracer,
tracer
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/generation/InitialGeneration.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ÉTAPE 1: GÉNÉRATION INITIALE
// Responsabilité: Créer le contenu de base avec Claude uniquement
// LLM: Claude Sonnet (température 0.7)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - GÉNÉRATION INITIALE
* Input: { content: {}, csvData: {}, context: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function generateInitialContent(input) {
return await tracer.run('InitialGeneration.generateInitialContent()', async () => {
const { hierarchy, csvData, context = {} } = input;
await tracer.annotate({
step: '1/4',
llmProvider: 'claude',
elementsCount: Object.keys(hierarchy).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🚀 ÉTAPE 1/4: Génération initiale (Claude)`, 'INFO');
logSh(` 📊 ${Object.keys(hierarchy).length} éléments à générer`, 'INFO');
try {
// Collecter tous les éléments dans l'ordre XML
const allElements = collectElementsInXMLOrder(hierarchy);
// Séparer FAQ pairs et autres éléments
const { faqPairs, otherElements } = separateElementTypes(allElements);
// Générer en chunks pour éviter timeouts
const results = {};
// 1. Générer éléments normaux (titres, textes, intro)
if (otherElements.length > 0) {
const normalResults = await generateNormalElements(otherElements, csvData);
Object.assign(results, normalResults);
}
// 2. Générer paires FAQ si présentes
if (faqPairs.length > 0) {
const faqResults = await generateFAQPairs(faqPairs, csvData);
Object.assign(results, faqResults);
}
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(results).length,
generated: Object.keys(results).length,
faqPairs: faqPairs.length,
duration
};
logSh(`✅ ÉTAPE 1/4 TERMINÉE: ${stats.generated} éléments générés (${duration}ms)`, 'INFO');
await tracer.event(`Génération initiale terminée`, stats);
return {
content: results,
stats,
debug: {
llmProvider: 'claude',
step: 1,
elementsGenerated: Object.keys(results)
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 1/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`InitialGeneration failed: ${error.message}`);
}
}, input);
}
/**
* Générer éléments normaux (titres, textes, intro) en chunks
*/
async function generateNormalElements(elements, csvData) {
logSh(`📝 Génération éléments normaux: ${elements.length} éléments`, 'DEBUG');
const results = {};
const chunks = chunkArray(elements, 4); // Chunks de 4 pour éviter timeouts
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
try {
const prompt = createBatchPrompt(chunk, csvData);
const response = await callLLM('claude', prompt, {
temperature: 0.7,
maxTokens: 2000 * chunk.length
}, csvData.personality);
const chunkResults = parseBatchResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} éléments générés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
throw error;
}
}
return results;
}
/**
* Générer paires FAQ cohérentes
*/
async function generateFAQPairs(faqPairs, csvData) {
logSh(`❓ Génération paires FAQ: ${faqPairs.length} paires`, 'DEBUG');
const prompt = createFAQPairsPrompt(faqPairs, csvData);
const response = await callLLM('claude', prompt, {
temperature: 0.8,
maxTokens: 3000
}, csvData.personality);
return parseFAQResponse(response, faqPairs);
}
/**
* Créer prompt batch pour éléments normaux
*/
function createBatchPrompt(elements, csvData) {
const personality = csvData.personality;
let prompt = `=== GÉNÉRATION CONTENU INITIAL ===
Entreprise: Autocollant.fr - signalétique personnalisée
Sujet: ${csvData.mc0}
Rédacteur: ${personality.nom} (${personality.style})
ÉLÉMENTS À GÉNÉRER:
`;
elements.forEach((elementInfo, index) => {
const cleanTag = elementInfo.tag.replace(/\|/g, '');
prompt += `${index + 1}. [${cleanTag}] - ${getElementDescription(elementInfo)}\n`;
});
prompt += `
STYLE ${personality.nom.toUpperCase()}:
- Vocabulaire: ${personality.vocabulairePref}
- Phrases: ${personality.longueurPhrases}
- Niveau: ${personality.niveauTechnique}
CONSIGNES:
- Contenu SEO optimisé pour ${csvData.mc0}
- Style ${personality.style} naturel
- Pas de références techniques dans contenu
- RÉPONSE DIRECTE par le contenu
FORMAT:
[${elements[0].tag.replace(/\|/g, '')}]
Contenu généré...
[${elements[1] ? elements[1].tag.replace(/\|/g, '') : 'element2'}]
Contenu généré...`;
return prompt;
}
/**
* Parser réponse batch
*/
function parseBatchResponse(response, elements) {
const results = {};
const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = cleanGeneratedContent(match[2].trim());
parsedItems[tag] = content;
}
// Mapper aux vrais tags
elements.forEach(element => {
const cleanTag = element.tag.replace(/\|/g, '');
if (parsedItems[cleanTag] && parsedItems[cleanTag].length > 10) {
results[element.tag] = parsedItems[cleanTag];
} else {
results[element.tag] = `Contenu professionnel pour ${element.element.name || cleanTag}`;
logSh(`⚠️ Fallback pour [${cleanTag}]`, 'WARNING');
}
});
return results;
}
/**
* Créer prompt pour paires FAQ
*/
function createFAQPairsPrompt(faqPairs, csvData) {
const personality = csvData.personality;
let prompt = `=== GÉNÉRATION PAIRES FAQ ===
Sujet: ${csvData.mc0}
Rédacteur: ${personality.nom} (${personality.style})
PAIRES À GÉNÉRER:
`;
faqPairs.forEach((pair, index) => {
const qTag = pair.question.tag.replace(/\|/g, '');
const aTag = pair.answer.tag.replace(/\|/g, '');
prompt += `${index + 1}. [${qTag}] + [${aTag}]\n`;
});
prompt += `
CONSIGNES:
- Questions naturelles de clients
- Réponses expertes ${personality.style}
- Couvrir: prix, livraison, personnalisation
FORMAT:
[${faqPairs[0].question.tag.replace(/\|/g, '')}]
Question client naturelle ?
[${faqPairs[0].answer.tag.replace(/\|/g, '')}]
Réponse utile et rassurante.`;
return prompt;
}
/**
* Parser réponse FAQ
*/
function parseFAQResponse(response, faqPairs) {
const results = {};
const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = cleanGeneratedContent(match[2].trim());
parsedItems[tag] = content;
}
// Mapper aux paires FAQ
faqPairs.forEach(pair => {
const qCleanTag = pair.question.tag.replace(/\|/g, '');
const aCleanTag = pair.answer.tag.replace(/\|/g, '');
if (parsedItems[qCleanTag]) results[pair.question.tag] = parsedItems[qCleanTag];
if (parsedItems[aCleanTag]) results[pair.answer.tag] = parsedItems[aCleanTag];
});
return results;
}
// ============= HELPER FUNCTIONS =============
function collectElementsInXMLOrder(hierarchy) {
const allElements = [];
Object.keys(hierarchy).forEach(path => {
const section = hierarchy[path];
if (section.title) {
allElements.push({
tag: section.title.originalElement.originalTag,
element: section.title.originalElement,
type: section.title.originalElement.type
});
}
if (section.text) {
allElements.push({
tag: section.text.originalElement.originalTag,
element: section.text.originalElement,
type: section.text.originalElement.type
});
}
section.questions.forEach(q => {
allElements.push({
tag: q.originalElement.originalTag,
element: q.originalElement,
type: q.originalElement.type
});
});
});
return allElements;
}
function separateElementTypes(allElements) {
const faqPairs = [];
const otherElements = [];
const faqQuestions = {};
const faqAnswers = {};
// Collecter FAQ questions et answers
allElements.forEach(element => {
if (element.type === 'faq_question') {
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqQuestions[faqNumber] = element;
} else if (element.type === 'faq_reponse') {
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqAnswers[faqNumber] = element;
} else {
otherElements.push(element);
}
});
// Créer paires FAQ
Object.keys(faqQuestions).forEach(number => {
const question = faqQuestions[number];
const answer = faqAnswers[number];
if (question && answer) {
faqPairs.push({ number, question, answer });
} else if (question) {
otherElements.push(question);
} else if (answer) {
otherElements.push(answer);
}
});
return { faqPairs, otherElements };
}
function getElementDescription(elementInfo) {
switch (elementInfo.type) {
case 'titre_h1': return 'Titre principal accrocheur';
case 'titre_h2': return 'Titre de section';
case 'titre_h3': return 'Sous-titre';
case 'intro': return 'Introduction engageante';
case 'texte': return 'Paragraphe informatif';
default: return 'Contenu pertinent';
}
}
function cleanGeneratedContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?Titre_[HU]\d+_\d+[.,\s]*/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
generateInitialContent, // ← MAIN ENTRY POINT
generateNormalElements,
generateFAQPairs,
createBatchPrompt,
parseBatchResponse,
collectElementsInXMLOrder,
separateElementTypes
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/generation/TechnicalEnhancement.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ÉTAPE 2: ENHANCEMENT TECHNIQUE
// Responsabilité: Améliorer la précision technique avec GPT-4
// LLM: GPT-4o-mini (température 0.4)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - ENHANCEMENT TECHNIQUE
* Input: { content: {}, csvData: {}, context: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function enhanceTechnicalTerms(input) {
return await tracer.run('TechnicalEnhancement.enhanceTechnicalTerms()', async () => {
const { content, csvData, context = {} } = input;
await tracer.annotate({
step: '2/4',
llmProvider: 'gpt4',
elementsCount: Object.keys(content).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🔧 ÉTAPE 2/4: Enhancement technique (GPT-4)`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'INFO');
try {
// 1. Analyser tous les éléments pour détecter termes techniques
const technicalAnalysis = await analyzeTechnicalTerms(content, csvData);
// 2. Filter les éléments qui ont besoin d'enhancement
const elementsNeedingEnhancement = technicalAnalysis.filter(item => item.needsEnhancement);
logSh(` 📋 Analyse: ${elementsNeedingEnhancement.length}/${Object.keys(content).length} éléments nécessitent enhancement`, 'INFO');
if (elementsNeedingEnhancement.length === 0) {
logSh(`✅ ÉTAPE 2/4: Aucun enhancement nécessaire`, 'INFO');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'gpt4', step: 2, enhancementsApplied: [] }
};
}
// 3. Améliorer les éléments sélectionnés
const enhancedResults = await enhanceSelectedElements(elementsNeedingEnhancement, csvData);
// 4. Merger avec contenu original
const finalContent = { ...content };
let actuallyEnhanced = 0;
Object.keys(enhancedResults).forEach(tag => {
if (enhancedResults[tag] !== content[tag]) {
finalContent[tag] = enhancedResults[tag];
actuallyEnhanced++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyEnhanced,
candidate: elementsNeedingEnhancement.length,
duration
};
logSh(`✅ ÉTAPE 2/4 TERMINÉE: ${stats.enhanced} éléments améliorés (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement technique terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'gpt4',
step: 2,
enhancementsApplied: Object.keys(enhancedResults),
technicalTermsFound: elementsNeedingEnhancement.map(e => e.technicalTerms)
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 2/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`TechnicalEnhancement failed: ${error.message}`);
}
}, input);
}
/**
* Analyser tous les éléments pour détecter termes techniques
*/
async function analyzeTechnicalTerms(content, csvData) {
logSh(`🔍 Analyse termes techniques batch`, 'DEBUG');
const contentEntries = Object.keys(content);
const analysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTENUS À ANALYSER:
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
CONTENU: "${content[tag]}"`).join('\n\n')}
CONSIGNES:
- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
- Évite mots génériques (qualité, service, pratique, personnalisé)
- Focus: matériaux, procédés, normes, dimensions, technologies
- Si aucun terme technique → "AUCUN"
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne
FORMAT RÉPONSE:
[1] dibond, impression UV OU AUCUN
[2] AUCUN
[3] aluminium, fraisage CNC OU AUCUN
etc...`;
try {
const analysisResponse = await callLLM('gpt4', analysisPrompt, {
temperature: 0.3,
maxTokens: 2000
}, csvData.personality);
return parseAnalysisResponse(analysisResponse, content, contentEntries);
} catch (error) {
logSh(`❌ Analyse termes techniques échouée: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Améliorer les éléments sélectionnés
*/
async function enhanceSelectedElements(elementsNeedingEnhancement, csvData) {
logSh(`🛠️ Enhancement ${elementsNeedingEnhancement.length} éléments`, 'DEBUG');
const enhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces contenus.
CONTEXTE: ${csvData.mc0} - Secteur signalétique/impression
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style})
CONTENUS À AMÉLIORER:
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag}
CONTENU: "${item.content}"
TERMES TECHNIQUES: ${item.technicalTerms.join(', ')}`).join('\n\n')}
CONSIGNES:
- GARDE même longueur, structure et ton ${csvData.personality?.style}
- Intègre naturellement les termes techniques listés
- NE CHANGE PAS le fond du message
- Vocabulaire expert mais accessible
- Termes secteur: dibond, aluminium, impression UV, fraisage, PMMA
FORMAT RÉPONSE:
[1] Contenu avec amélioration technique
[2] Contenu avec amélioration technique
etc...`;
try {
const enhancedResponse = await callLLM('gpt4', enhancementPrompt, {
temperature: 0.4,
maxTokens: 5000
}, csvData.personality);
return parseEnhancementResponse(enhancedResponse, elementsNeedingEnhancement);
} catch (error) {
logSh(`❌ Enhancement éléments échoué: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Parser réponse analyse
*/
function parseAnalysisResponse(response, content, contentEntries) {
const results = [];
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const index = parseInt(match[1]) - 1;
const termsText = match[2].trim();
parsedItems[index] = termsText;
}
contentEntries.forEach((tag, index) => {
const termsText = parsedItems[index] || 'AUCUN';
const hasTerms = !termsText.toUpperCase().includes('AUCUN');
const technicalTerms = hasTerms ?
termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) :
[];
results.push({
tag,
content: content[tag],
technicalTerms,
needsEnhancement: hasTerms && technicalTerms.length > 0
});
logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'aucun terme technique'}`, 'DEBUG');
});
return results;
}
/**
* Parser réponse enhancement
*/
function parseEnhancementResponse(response, elementsNeedingEnhancement) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < elementsNeedingEnhancement.length) {
let enhancedContent = match[2].trim();
const element = elementsNeedingEnhancement[index];
// Nettoyer le contenu généré
enhancedContent = cleanEnhancedContent(enhancedContent);
if (enhancedContent && enhancedContent.length > 10) {
results[element.tag] = enhancedContent;
logSh(`✅ Enhanced [${element.tag}]: "${enhancedContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: contenu invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < elementsNeedingEnhancement.length) {
const element = elementsNeedingEnhancement[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu amélioré
*/
function cleanEnhancedContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?pour\s+/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
module.exports = {
enhanceTechnicalTerms, // ← MAIN ENTRY POINT
analyzeTechnicalTerms,
enhanceSelectedElements,
parseAnalysisResponse,
parseEnhancementResponse
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/generation/TransitionEnhancement.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ÉTAPE 3: ENHANCEMENT TRANSITIONS
// Responsabilité: Améliorer la fluidité avec Gemini
// LLM: Gemini (température 0.6)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - ENHANCEMENT TRANSITIONS
* Input: { content: {}, csvData: {}, context: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function enhanceTransitions(input) {
return await tracer.run('TransitionEnhancement.enhanceTransitions()', async () => {
const { content, csvData, context = {} } = input;
await tracer.annotate({
step: '3/4',
llmProvider: 'gemini',
elementsCount: Object.keys(content).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🔗 ÉTAPE 3/4: Enhancement transitions (Gemini)`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'INFO');
try {
// 1. Analyser quels éléments ont besoin d'amélioration transitions
const elementsNeedingTransitions = analyzeTransitionNeeds(content);
logSh(` 📋 Analyse: ${elementsNeedingTransitions.length}/${Object.keys(content).length} éléments nécessitent fluidité`, 'INFO');
if (elementsNeedingTransitions.length === 0) {
logSh(`✅ ÉTAPE 3/4: Transitions déjà optimales`, 'INFO');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'gemini', step: 3, enhancementsApplied: [] }
};
}
// 2. Améliorer en chunks pour Gemini
const improvedResults = await improveTransitionsInChunks(elementsNeedingTransitions, csvData);
// 3. Merger avec contenu original
const finalContent = { ...content };
let actuallyImproved = 0;
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
finalContent[tag] = improvedResults[tag];
actuallyImproved++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyImproved,
candidate: elementsNeedingTransitions.length,
duration
};
logSh(`✅ ÉTAPE 3/4 TERMINÉE: ${stats.enhanced} éléments fluidifiés (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement transitions terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'gemini',
step: 3,
enhancementsApplied: Object.keys(improvedResults),
transitionIssues: elementsNeedingTransitions.map(e => e.issues)
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 3/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original si Gemini indisponible
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration },
debug: { llmProvider: 'gemini', step: 3, error: error.message, fallback: true }
};
}
}, input);
}
/**
* Analyser besoin d'amélioration transitions
*/
function analyzeTransitionNeeds(content) {
const elementsNeedingTransitions = [];
Object.keys(content).forEach(tag => {
const text = content[tag];
// Filtrer les éléments longs (>150 chars) qui peuvent bénéficier d'améliorations
if (text.length > 150) {
const needsTransitions = evaluateTransitionQuality(text);
if (needsTransitions.needsImprovement) {
elementsNeedingTransitions.push({
tag,
content: text,
issues: needsTransitions.issues,
score: needsTransitions.score
});
logSh(` 🔍 [${tag}]: Score=${needsTransitions.score.toFixed(2)}, Issues: ${needsTransitions.issues.join(', ')}`, 'DEBUG');
}
} else {
logSh(` ⏭️ [${tag}]: Trop court (${text.length}c), ignoré`, 'DEBUG');
}
});
// Trier par score (plus problématique en premier)
elementsNeedingTransitions.sort((a, b) => a.score - b.score);
return elementsNeedingTransitions;
}
/**
* Évaluer qualité transitions d'un texte
*/
function evaluateTransitionQuality(text) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) {
return { needsImprovement: false, score: 1.0, issues: [] };
}
const issues = [];
let score = 1.0; // Score parfait = 1.0, problématique = 0.0
// Analyse 1: Connecteurs répétitifs
const repetitiveConnectors = analyzeRepetitiveConnectors(text);
if (repetitiveConnectors > 0.3) {
issues.push('connecteurs_répétitifs');
score -= 0.3;
}
// Analyse 2: Transitions abruptes
const abruptTransitions = analyzeAbruptTransitions(sentences);
if (abruptTransitions > 0.4) {
issues.push('transitions_abruptes');
score -= 0.4;
}
// Analyse 3: Manque de variété dans longueurs
const sentenceVariety = analyzeSentenceVariety(sentences);
if (sentenceVariety < 0.3) {
issues.push('phrases_uniformes');
score -= 0.2;
}
// Analyse 4: Trop formel ou trop familier
const formalityIssues = analyzeFormalityBalance(text);
if (formalityIssues > 0.5) {
issues.push('formalité_déséquilibrée');
score -= 0.1;
}
return {
needsImprovement: score < 0.6,
score: Math.max(0, score),
issues
};
}
/**
* Améliorer transitions en chunks
*/
async function improveTransitionsInChunks(elementsNeedingTransitions, csvData) {
logSh(`🔄 Amélioration transitions: ${elementsNeedingTransitions.length} éléments`, 'DEBUG');
const results = {};
const chunks = chunkArray(elementsNeedingTransitions, 6); // Chunks plus petits pour Gemini
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const improvementPrompt = createTransitionImprovementPrompt(chunk, csvData);
const improvedResponse = await callLLM('gemini', improvementPrompt, {
temperature: 0.6,
maxTokens: 2500
}, csvData.personality);
const chunkResults = parseTransitionResponse(improvedResponse, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} améliorés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: garder contenu original pour ce chunk
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* Créer prompt amélioration transitions
*/
function createTransitionImprovementPrompt(chunk, csvData) {
const personality = csvData.personality;
let prompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.
CONTEXTE: Article SEO ${csvData.mc0}
PERSONNALITÉ: ${personality?.nom} (${personality?.style} web professionnel)
CONNECTEURS PRÉFÉRÉS: ${personality?.connecteursPref}
CONTENUS À FLUIDIFIER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
PROBLÈMES: ${item.issues.join(', ')}
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS:
- Connecteurs plus naturels et variés: ${personality?.connecteursPref}
- Transitions fluides entre idées
- ÉVITE répétitions excessives ("du coup", "franchement", "par ailleurs")
- Style ${personality?.style} mais professionnel web
CONSIGNES STRICTES:
- NE CHANGE PAS le fond du message
- GARDE même structure et longueur
- Améliore SEULEMENT la fluidité
- RESPECTE le style ${personality?.nom}
FORMAT RÉPONSE:
[1] Contenu avec transitions améliorées
[2] Contenu avec transitions améliorées
etc...`;
return prompt;
}
/**
* Parser réponse amélioration transitions
*/
function parseTransitionResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let improvedContent = match[2].trim();
const element = chunk[index];
// Nettoyer le contenu amélioré
improvedContent = cleanImprovedContent(improvedContent);
if (improvedContent && improvedContent.length > 10) {
results[element.tag] = improvedContent;
logSh(`✅ Improved [${element.tag}]: "${improvedContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
// ============= HELPER FUNCTIONS =============
function analyzeRepetitiveConnectors(content) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc'];
let totalConnectors = 0;
let repetitions = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
totalConnectors += matches.length;
if (matches.length > 1) repetitions += matches.length - 1;
});
return totalConnectors > 0 ? repetitions / totalConnectors : 0;
}
function analyzeAbruptTransitions(sentences) {
if (sentences.length < 2) return 0;
let abruptCount = 0;
for (let i = 1; i < sentences.length; i++) {
const current = sentences[i].trim();
const hasConnector = hasTransitionWord(current);
if (!hasConnector && current.length > 30) {
abruptCount++;
}
}
return abruptCount / (sentences.length - 1);
}
function analyzeSentenceVariety(sentences) {
if (sentences.length < 2) return 1;
const lengths = sentences.map(s => s.trim().length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
return Math.min(1, stdDev / avgLength);
}
function analyzeFormalityBalance(content) {
const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois'];
const casualIndicators = ['du coup', 'bon', 'franchement', 'nickel'];
let formalCount = 0;
let casualCount = 0;
formalIndicators.forEach(indicator => {
if (content.toLowerCase().includes(indicator)) formalCount++;
});
casualIndicators.forEach(indicator => {
if (content.toLowerCase().includes(indicator)) casualCount++;
});
const total = formalCount + casualCount;
if (total === 0) return 0;
// Déséquilibre si trop d'un côté
const balance = Math.abs(formalCount - casualCount) / total;
return balance;
}
function hasTransitionWord(sentence) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite', 'puis', 'également', 'aussi'];
return connectors.some(connector => sentence.toLowerCase().includes(connector));
}
function cleanImprovedContent(content) {
if (!content) return content;
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?/, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
enhanceTransitions, // ← MAIN ENTRY POINT
analyzeTransitionNeeds,
evaluateTransitionQuality,
improveTransitionsInChunks,
createTransitionImprovementPrompt,
parseTransitionResponse
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/generation/StyleEnhancement.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ÉTAPE 4: ENHANCEMENT STYLE PERSONNALITÉ
// Responsabilité: Appliquer le style personnalité avec Mistral
// LLM: Mistral (température 0.8)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - ENHANCEMENT STYLE
* Input: { content: {}, csvData: {}, context: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function applyPersonalityStyle(input) {
return await tracer.run('StyleEnhancement.applyPersonalityStyle()', async () => {
const { content, csvData, context = {} } = input;
await tracer.annotate({
step: '4/4',
llmProvider: 'mistral',
elementsCount: Object.keys(content).length,
personality: csvData.personality?.nom,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎭 ÉTAPE 4/4: Enhancement style ${csvData.personality?.nom} (Mistral)`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à styliser`, 'INFO');
try {
const personality = csvData.personality;
if (!personality) {
logSh(`⚠️ ÉTAPE 4/4: Aucune personnalité définie, style standard`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'mistral', step: 4, personalityApplied: 'none' }
};
}
// 1. Préparer éléments pour stylisation
const styleElements = prepareElementsForStyling(content);
// 2. Appliquer style en chunks
const styledResults = await applyStyleInChunks(styleElements, csvData);
// 3. Merger résultats
const finalContent = { ...content };
let actuallyStyled = 0;
Object.keys(styledResults).forEach(tag => {
if (styledResults[tag] !== content[tag]) {
finalContent[tag] = styledResults[tag];
actuallyStyled++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyStyled,
personality: personality.nom,
duration
};
logSh(`✅ ÉTAPE 4/4 TERMINÉE: ${stats.enhanced} éléments stylisés ${personality.nom} (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement style terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'mistral',
step: 4,
personalityApplied: personality.nom,
styleCharacteristics: {
vocabulaire: personality.vocabulairePref,
connecteurs: personality.connecteursPref,
style: personality.style
}
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 4/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original si Mistral indisponible
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration },
debug: { llmProvider: 'mistral', step: 4, error: error.message, fallback: true }
};
}
}, input);
}
/**
* Préparer éléments pour stylisation
*/
function prepareElementsForStyling(content) {
const styleElements = [];
Object.keys(content).forEach(tag => {
const text = content[tag];
// Tous les éléments peuvent bénéficier d'adaptation personnalité
// Même les courts (titres) peuvent être adaptés au style
styleElements.push({
tag,
content: text,
priority: calculateStylePriority(text, tag)
});
});
// Trier par priorité (titres d'abord, puis textes longs)
styleElements.sort((a, b) => b.priority - a.priority);
return styleElements;
}
/**
* Calculer priorité de stylisation
*/
function calculateStylePriority(text, tag) {
let priority = 1.0;
// Titres = haute priorité (plus visible)
if (tag.includes('Titre') || tag.includes('H1') || tag.includes('H2')) {
priority += 0.5;
}
// Textes longs = priorité selon longueur
if (text.length > 200) {
priority += 0.3;
} else if (text.length > 100) {
priority += 0.2;
}
// Introduction = haute priorité
if (tag.includes('intro') || tag.includes('Introduction')) {
priority += 0.4;
}
return priority;
}
/**
* Appliquer style en chunks
*/
async function applyStyleInChunks(styleElements, csvData) {
logSh(`🎨 Stylisation: ${styleElements.length} éléments selon ${csvData.personality.nom}`, 'DEBUG');
const results = {};
const chunks = chunkArray(styleElements, 8); // Chunks de 8 pour Mistral
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const stylePrompt = createStylePrompt(chunk, csvData);
const styledResponse = await callLLM('mistral', stylePrompt, {
temperature: 0.8,
maxTokens: 3000
}, csvData.personality);
const chunkResults = parseStyleResponse(styledResponse, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} stylisés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: garder contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* Créer prompt de stylisation
*/
function createStylePrompt(chunk, csvData) {
const personality = csvData.personality;
let prompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}.
CONTEXTE: Article SEO e-commerce ${csvData.mc0}
PERSONNALITÉ: ${personality.nom}
DESCRIPTION: ${personality.description}
STYLE: ${personality.style} adapté web professionnel
VOCABULAIRE: ${personality.vocabulairePref}
CONNECTEURS: ${personality.connecteursPref}
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
LONGUEUR PHRASES: ${personality.longueurPhrases}
CONTENUS À STYLISER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} (Priorité: ${item.priority.toFixed(1)})
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS STYLISATION ${personality.nom.toUpperCase()}:
- Adapte le TON selon ${personality.style}
- Vocabulaire: ${personality.vocabulairePref}
- Connecteurs variés: ${personality.connecteursPref}
- Phrases: ${personality.longueurPhrases}
- Niveau: ${personality.niveauTechnique}
CONSIGNES STRICTES:
- GARDE le même contenu informatif et technique
- Adapte SEULEMENT ton, expressions, vocabulaire selon ${personality.nom}
- RESPECTE longueur approximative (±20%)
- ÉVITE répétitions excessives
- Style ${personality.nom} reconnaissable mais NATUREL web
- PAS de messages d'excuse
FORMAT RÉPONSE:
[1] Contenu stylisé selon ${personality.nom}
[2] Contenu stylisé selon ${personality.nom}
etc...`;
return prompt;
}
/**
* Parser réponse stylisation
*/
function parseStyleResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let styledContent = match[2].trim();
const element = chunk[index];
// Nettoyer le contenu stylisé
styledContent = cleanStyledContent(styledContent);
if (styledContent && styledContent.length > 10) {
results[element.tag] = styledContent;
logSh(`✅ Styled [${element.tag}]: "${styledContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: stylisation invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu stylisé
*/
function cleanStyledContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?voici\s+/gi, '');
content = content.replace(/^pour\s+ce\s+contenu[,\s]*/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
// Réduire répétitions excessives mais garder le style personnalité
content = content.replace(/(du coup[,\s]+){4,}/gi, 'du coup ');
content = content.replace(/(bon[,\s]+){4,}/gi, 'bon ');
content = content.replace(/(franchement[,\s]+){3,}/gi, 'franchement ');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
/**
* Obtenir instructions de style dynamiques
*/
function getPersonalityStyleInstructions(personality) {
if (!personality) return "Style professionnel standard";
return `STYLE ${personality.nom.toUpperCase()} (${personality.style}):
- Description: ${personality.description}
- Vocabulaire: ${personality.vocabulairePref || 'professionnel'}
- Connecteurs: ${personality.connecteursPref || 'par ailleurs, en effet'}
- Mots-clés: ${personality.motsClesSecteurs || 'technique, qualité'}
- Phrases: ${personality.longueurPhrases || 'Moyennes'}
- Niveau: ${personality.niveauTechnique || 'Accessible'}
- CTA: ${personality.ctaStyle || 'Professionnel'}`;
}
// ============= HELPER FUNCTIONS =============
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
applyPersonalityStyle, // ← MAIN ENTRY POINT
prepareElementsForStyling,
calculateStylePriority,
applyStyleInChunks,
createStylePrompt,
parseStyleResponse,
getPersonalityStyleInstructions
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/post-processing/SentenceVariation.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// PATTERN BREAKING - TECHNIQUE 1: SENTENCE VARIATION
// Responsabilité: Varier les longueurs de phrases pour casser l'uniformité
// Anti-détection: Éviter patterns syntaxiques réguliers des LLMs
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - VARIATION LONGUEUR PHRASES
* @param {Object} input - { content: {}, config: {}, context: {} }
* @returns {Object} - { content: {}, stats: {}, debug: {} }
*/
async function applySentenceVariation(input) {
return await tracer.run('SentenceVariation.applySentenceVariation()', async () => {
const { content, config = {}, context = {} } = input;
const {
intensity = 0.3, // Probabilité de modification (30%)
splitThreshold = 100, // Chars pour split
mergeThreshold = 30, // Chars pour merge
preserveQuestions = true, // Préserver questions FAQ
preserveTitles = true // Préserver titres
} = config;
await tracer.annotate({
technique: 'sentence_variation',
intensity,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`📐 TECHNIQUE 1/3: Variation longueur phrases (intensité: ${intensity})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'DEBUG');
try {
const results = {};
let totalProcessed = 0;
let totalModified = 0;
let modificationsDetails = [];
// Traiter chaque élément de contenu
for (const [tag, text] of Object.entries(content)) {
totalProcessed++;
// Skip certains éléments selon config
if (shouldSkipElement(tag, text, { preserveQuestions, preserveTitles })) {
results[tag] = text;
logSh(` ⏭️ [${tag}]: Préservé (${getSkipReason(tag, text)})`, 'DEBUG');
continue;
}
// Appliquer variation si éligible
const variationResult = varyTextStructure(text, {
intensity,
splitThreshold,
mergeThreshold,
tag
});
results[tag] = variationResult.text;
if (variationResult.modified) {
totalModified++;
modificationsDetails.push({
tag,
modifications: variationResult.modifications,
originalLength: text.length,
newLength: variationResult.text.length
});
logSh(` ✏️ [${tag}]: ${variationResult.modifications.length} modifications`, 'DEBUG');
} else {
logSh(` ➡️ [${tag}]: Aucune modification`, 'DEBUG');
}
}
const duration = Date.now() - startTime;
const stats = {
processed: totalProcessed,
modified: totalModified,
modificationRate: Math.round((totalModified / totalProcessed) * 100),
duration,
technique: 'sentence_variation'
};
logSh(`✅ VARIATION PHRASES: ${stats.modified}/${stats.processed} éléments modifiés (${stats.modificationRate}%) en ${duration}ms`, 'INFO');
await tracer.event('Sentence variation terminée', stats);
return {
content: results,
stats,
debug: {
technique: 'sentence_variation',
config: { intensity, splitThreshold, mergeThreshold },
modifications: modificationsDetails
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ VARIATION PHRASES échouée après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`SentenceVariation failed: ${error.message}`);
}
}, input);
}
/**
* Appliquer variation structure à un texte
*/
function varyTextStructure(text, config) {
const { intensity, splitThreshold, mergeThreshold, tag } = config;
if (text.length < 50) {
return { text, modified: false, modifications: [] };
}
// Séparer en phrases
const sentences = splitIntoSentences(text);
if (sentences.length < 2) {
return { text, modified: false, modifications: [] };
}
let modifiedSentences = [...sentences];
const modifications = [];
// TECHNIQUE 1: SPLIT des phrases longues
for (let i = 0; i < modifiedSentences.length; i++) {
const sentence = modifiedSentences[i];
if (sentence.length > splitThreshold && Math.random() < intensity) {
const splitResult = splitLongSentence(sentence);
if (splitResult.success) {
modifiedSentences.splice(i, 1, splitResult.part1, splitResult.part2);
modifications.push({
type: 'split',
original: sentence.substring(0, 50) + '...',
result: `${splitResult.part1.substring(0, 25)}... | ${splitResult.part2.substring(0, 25)}...`
});
i++; // Skip la phrase suivante (qui est notre part2)
}
}
}
// TECHNIQUE 2: MERGE des phrases courtes
for (let i = 0; i < modifiedSentences.length - 1; i++) {
const current = modifiedSentences[i];
const next = modifiedSentences[i + 1];
if (current.length < mergeThreshold && next.length < mergeThreshold && Math.random() < intensity) {
const merged = mergeSentences(current, next);
if (merged.success) {
modifiedSentences.splice(i, 2, merged.result);
modifications.push({
type: 'merge',
original: `${current.substring(0, 20)}... + ${next.substring(0, 20)}...`,
result: merged.result.substring(0, 50) + '...'
});
}
}
}
const finalText = modifiedSentences.join(' ').trim();
return {
text: finalText,
modified: modifications.length > 0,
modifications
};
}
/**
* Diviser texte en phrases
*/
function splitIntoSentences(text) {
// Regex plus sophistiquée pour gérer les abréviations
const sentences = text.split(/(?<![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/LLMFingerprintRemoval.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// PATTERN BREAKING - TECHNIQUE 2: LLM FINGERPRINT REMOVAL
// Responsabilité: Remplacer mots/expressions typiques des LLMs
// Anti-détection: Éviter vocabulaire détectable par les analyseurs IA
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* DICTIONNAIRE ANTI-DÉTECTION
* Mots/expressions LLM → Alternatives humaines naturelles
*/
const LLM_FINGERPRINTS = {
// Mots techniques/corporate typiques IA
'optimal': ['idéal', 'parfait', 'adapté', 'approprié', 'convenable'],
'optimale': ['idéale', 'parfaite', 'adaptée', 'appropriée', 'convenable'],
'comprehensive': ['complet', 'détaillé', 'exhaustif', 'approfondi', 'global'],
'seamless': ['fluide', 'naturel', 'sans accroc', 'harmonieux', 'lisse'],
'robust': ['solide', 'fiable', 'résistant', 'costaud', 'stable'],
'robuste': ['solide', 'fiable', 'résistant', 'costaud', 'stable'],
// Expressions trop formelles/IA
'il convient de noter': ['on remarque', 'il faut savoir', 'à noter', 'important'],
'il convient de': ['il faut', 'on doit', 'mieux vaut', 'il est bon de'],
'par conséquent': ['du coup', 'donc', 'résultat', 'ainsi'],
'néanmoins': ['cependant', 'mais', 'pourtant', 'malgré tout'],
'toutefois': ['cependant', 'mais', 'pourtant', 'quand même'],
'de surcroît': ['de plus', 'en plus', 'aussi', 'également'],
// Superlatifs excessifs typiques IA
'extrêmement': ['très', 'super', 'vraiment', 'particulièrement'],
'particulièrement': ['très', 'vraiment', 'spécialement', 'surtout'],
'remarquablement': ['très', 'vraiment', 'sacrément', 'fichement'],
'exceptionnellement': ['très', 'vraiment', 'super', 'incroyablement'],
// Mots de liaison trop mécaniques
'en définitive': ['au final', 'finalement', 'bref', 'en gros'],
'il s\'avère que': ['on voit que', 'il se trouve que', 'en fait'],
'force est de constater': ['on constate', 'on voit bien', 'c\'est clair'],
// Expressions commerciales robotiques
'solution innovante': ['nouveauté', 'innovation', 'solution moderne', 'nouvelle approche'],
'approche holistique': ['approche globale', 'vision d\'ensemble', 'approche complète'],
'expérience utilisateur': ['confort d\'utilisation', 'facilité d\'usage', 'ergonomie'],
'retour sur investissement': ['rentabilité', 'bénéfices', 'profits'],
// Adjectifs surutilisés par IA
'révolutionnaire': ['nouveau', 'moderne', 'innovant', 'original'],
'game-changer': ['nouveauté', 'innovation', 'changement', 'révolution'],
'cutting-edge': ['moderne', 'récent', 'nouveau', 'avancé'],
'state-of-the-art': ['moderne', 'récent', 'performant', 'haut de gamme']
};
/**
* EXPRESSIONS CONTEXTUELLES SECTEUR SIGNALÉTIQUE
* Adaptées au domaine métier pour plus de naturel
*/
const CONTEXTUAL_REPLACEMENTS = {
'solution': {
'signalétique': ['plaque', 'panneau', 'support', 'réalisation'],
'impression': ['tirage', 'print', 'production', 'fabrication'],
'default': ['option', 'possibilité', 'choix', 'alternative']
},
'produit': {
'signalétique': ['plaque', 'panneau', 'enseigne', 'support'],
'default': ['article', 'réalisation', 'création']
},
'service': {
'signalétique': ['prestation', 'réalisation', 'travail', 'création'],
'default': ['prestation', 'travail', 'aide']
}
};
/**
* MAIN ENTRY POINT - SUPPRESSION EMPREINTES LLM
* @param {Object} input - { content: {}, config: {}, context: {} }
* @returns {Object} - { content: {}, stats: {}, debug: {} }
*/
async function removeLLMFingerprints(input) {
return await tracer.run('LLMFingerprintRemoval.removeLLMFingerprints()', async () => {
const { content, config = {}, context = {} } = input;
const {
intensity = 1.0, // Probabilité de remplacement (100%)
preserveKeywords = true, // Préserver mots-clés SEO
contextualMode = true, // Mode contextuel métier
csvData = null // Pour contexte métier
} = config;
await tracer.annotate({
technique: 'fingerprint_removal',
intensity,
elementsCount: Object.keys(content).length,
contextualMode
});
const startTime = Date.now();
logSh(`🔍 TECHNIQUE 2/3: Suppression empreintes LLM (intensité: ${intensity})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à nettoyer`, 'DEBUG');
try {
const results = {};
let totalProcessed = 0;
let totalReplacements = 0;
let replacementDetails = [];
// Préparer contexte métier
const businessContext = extractBusinessContext(csvData);
// Traiter chaque élément de contenu
for (const [tag, text] of Object.entries(content)) {
totalProcessed++;
if (text.length < 20) {
results[tag] = text;
continue;
}
// Appliquer suppression des empreintes
const cleaningResult = cleanTextFingerprints(text, {
intensity,
preserveKeywords,
contextualMode,
businessContext,
tag
});
results[tag] = cleaningResult.text;
if (cleaningResult.replacements.length > 0) {
totalReplacements += cleaningResult.replacements.length;
replacementDetails.push({
tag,
replacements: cleaningResult.replacements,
fingerprintsFound: cleaningResult.fingerprintsDetected
});
logSh(` 🧹 [${tag}]: ${cleaningResult.replacements.length} remplacements`, 'DEBUG');
} else {
logSh(` ✅ [${tag}]: Aucune empreinte détectée`, 'DEBUG');
}
}
const duration = Date.now() - startTime;
const stats = {
processed: totalProcessed,
totalReplacements,
avgReplacementsPerElement: Math.round(totalReplacements / totalProcessed * 100) / 100,
elementsWithFingerprints: replacementDetails.length,
duration,
technique: 'fingerprint_removal'
};
logSh(`✅ NETTOYAGE EMPREINTES: ${stats.totalReplacements} remplacements sur ${stats.elementsWithFingerprints}/${stats.processed} éléments en ${duration}ms`, 'INFO');
await tracer.event('Fingerprint removal terminée', stats);
return {
content: results,
stats,
debug: {
technique: 'fingerprint_removal',
config: { intensity, preserveKeywords, contextualMode },
replacements: replacementDetails,
businessContext
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ NETTOYAGE EMPREINTES échoué après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`LLMFingerprintRemoval failed: ${error.message}`);
}
}, input);
}
/**
* Nettoyer les empreintes LLM d'un texte
*/
function cleanTextFingerprints(text, config) {
const { intensity, preserveKeywords, contextualMode, businessContext, tag } = config;
let cleanedText = text;
const replacements = [];
const fingerprintsDetected = [];
// PHASE 1: Remplacements directs du dictionnaire
for (const [fingerprint, alternatives] of Object.entries(LLM_FINGERPRINTS)) {
const regex = new RegExp(`\\b${escapeRegex(fingerprint)}\\b`, 'gi');
const matches = text.match(regex);
if (matches) {
fingerprintsDetected.push(fingerprint);
// Appliquer remplacement selon intensité
if (Math.random() <= intensity) {
const alternative = selectBestAlternative(alternatives, businessContext, contextualMode);
cleanedText = cleanedText.replace(regex, (match) => {
// Préserver la casse originale
return preserveCase(match, alternative);
});
replacements.push({
type: 'direct',
original: fingerprint,
replacement: alternative,
occurrences: matches.length
});
}
}
}
// PHASE 2: Remplacements contextuels
if (contextualMode && businessContext) {
const contextualReplacements = applyContextualReplacements(cleanedText, businessContext);
cleanedText = contextualReplacements.text;
replacements.push(...contextualReplacements.replacements);
}
// PHASE 3: Détection patterns récurrents
const patternReplacements = replaceRecurringPatterns(cleanedText, intensity);
cleanedText = patternReplacements.text;
replacements.push(...patternReplacements.replacements);
return {
text: cleanedText,
replacements,
fingerprintsDetected
};
}
/**
* Sélectionner la meilleure alternative selon le contexte
*/
function selectBestAlternative(alternatives, businessContext, contextualMode) {
if (!contextualMode || !businessContext) {
// Mode aléatoire simple
return alternatives[Math.floor(Math.random() * alternatives.length)];
}
// Mode contextuel : privilégier alternatives adaptées au métier
const contextualAlternatives = alternatives.filter(alt =>
isContextuallyAppropriate(alt, businessContext)
);
const finalAlternatives = contextualAlternatives.length > 0 ? contextualAlternatives : alternatives;
return finalAlternatives[Math.floor(Math.random() * finalAlternatives.length)];
}
/**
* Vérifier si une alternative est contextuelle appropriée
*/
function isContextuallyAppropriate(alternative, businessContext) {
const { sector, vocabulary } = businessContext;
// Signalétique : privilégier vocabulaire technique/artisanal
if (sector === 'signalétique') {
const technicalWords = ['solide', 'fiable', 'costaud', 'résistant', 'adapté'];
return technicalWords.includes(alternative);
}
return true; // Par défaut accepter
}
/**
* Appliquer remplacements contextuels
*/
function applyContextualReplacements(text, businessContext) {
let processedText = text;
const replacements = [];
for (const [word, contexts] of Object.entries(CONTEXTUAL_REPLACEMENTS)) {
const regex = new RegExp(`\\b${word}\\b`, 'gi');
const matches = processedText.match(regex);
if (matches) {
const contextAlternatives = contexts[businessContext.sector] || contexts.default;
const replacement = contextAlternatives[Math.floor(Math.random() * contextAlternatives.length)];
processedText = processedText.replace(regex, (match) => {
return preserveCase(match, replacement);
});
replacements.push({
type: 'contextual',
original: word,
replacement,
occurrences: matches.length,
context: businessContext.sector
});
}
}
return { text: processedText, replacements };
}
/**
* Remplacer patterns récurrents
*/
function replaceRecurringPatterns(text, intensity) {
let processedText = text;
const replacements = [];
// Pattern 1: "très + adjectif" → variantes
const veryPattern = /\btrès\s+(\w+)/gi;
const veryMatches = [...text.matchAll(veryPattern)];
if (veryMatches.length > 2 && Math.random() < intensity) {
// Remplacer certains "très" par des alternatives
const alternatives = ['super', 'vraiment', 'particulièrement', 'assez'];
veryMatches.slice(1).forEach((match, index) => {
if (Math.random() < 0.5) {
const alternative = alternatives[Math.floor(Math.random() * alternatives.length)];
const fullMatch = match[0];
const adjective = match[1];
const replacement = `${alternative} ${adjective}`;
processedText = processedText.replace(fullMatch, replacement);
replacements.push({
type: 'pattern',
pattern: '"très + adjectif"',
original: fullMatch,
replacement
});
}
});
}
return { text: processedText, replacements };
}
/**
* Extraire contexte métier des données CSV
*/
function extractBusinessContext(csvData) {
if (!csvData) {
return { sector: 'general', vocabulary: [] };
}
const mc0 = csvData.mc0?.toLowerCase() || '';
// Détection secteur
let sector = 'general';
if (mc0.includes('plaque') || mc0.includes('panneau') || mc0.includes('enseigne')) {
sector = 'signalétique';
} else if (mc0.includes('impression') || mc0.includes('print')) {
sector = 'impression';
}
// Extraction vocabulaire clé
const vocabulary = [csvData.mc0, csvData.t0, csvData.tMinus1].filter(Boolean);
return { sector, vocabulary };
}
/**
* Préserver la casse originale
*/
function preserveCase(original, replacement) {
if (original === original.toUpperCase()) {
return replacement.toUpperCase();
} else if (original[0] === original[0].toUpperCase()) {
return replacement.charAt(0).toUpperCase() + replacement.slice(1).toLowerCase();
} else {
return replacement.toLowerCase();
}
}
/**
* Échapper caractères regex
*/
function escapeRegex(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Analyser les empreintes LLM dans un texte
*/
function analyzeLLMFingerprints(text) {
const detectedFingerprints = [];
let totalMatches = 0;
for (const fingerprint of Object.keys(LLM_FINGERPRINTS)) {
const regex = new RegExp(`\\b${escapeRegex(fingerprint)}\\b`, 'gi');
const matches = text.match(regex);
if (matches) {
detectedFingerprints.push({
fingerprint,
occurrences: matches.length,
category: categorizefingerprint(fingerprint)
});
totalMatches += matches.length;
}
}
return {
hasFingerprints: detectedFingerprints.length > 0,
fingerprints: detectedFingerprints,
totalMatches,
riskLevel: calculateRiskLevel(detectedFingerprints, text.length)
};
}
/**
* Catégoriser une empreinte LLM
*/
function categorizefingerprint(fingerprint) {
const categories = {
'technical': ['optimal', 'comprehensive', 'robust', 'seamless'],
'formal': ['il convient de', 'néanmoins', 'par conséquent'],
'superlative': ['extrêmement', 'particulièrement', 'remarquablement'],
'commercial': ['solution innovante', 'game-changer', 'révolutionnaire']
};
for (const [category, words] of Object.entries(categories)) {
if (words.some(word => fingerprint.includes(word))) {
return category;
}
}
return 'other';
}
/**
* Calculer niveau de risque de détection
*/
function calculateRiskLevel(fingerprints, textLength) {
if (fingerprints.length === 0) return 'low';
const fingerprintDensity = fingerprints.reduce((sum, fp) => sum + fp.occurrences, 0) / (textLength / 100);
if (fingerprintDensity > 3) return 'high';
if (fingerprintDensity > 1.5) return 'medium';
return 'low';
}
module.exports = {
removeLLMFingerprints, // ← MAIN ENTRY POINT
cleanTextFingerprints,
analyzeLLMFingerprints,
LLM_FINGERPRINTS,
CONTEXTUAL_REPLACEMENTS,
extractBusinessContext
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/post-processing/TransitionHumanization.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// PATTERN BREAKING - TECHNIQUE 3: TRANSITION HUMANIZATION
// Responsabilité: Remplacer connecteurs mécaniques par transitions naturelles
// Anti-détection: Éviter patterns de liaison typiques des LLMs
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* DICTIONNAIRE CONNECTEURS HUMANISÉS
* Connecteurs LLM → Alternatives naturelles par contexte
*/
const TRANSITION_REPLACEMENTS = {
// Connecteurs trop formels → versions naturelles
'par ailleurs': {
alternatives: ['d\'ailleurs', 'au fait', 'soit dit en passant', 'à propos', 'sinon'],
weight: 0.8,
contexts: ['casual', 'conversational']
},
'en effet': {
alternatives: ['effectivement', 'c\'est vrai', 'tout à fait', 'absolument', 'exactement'],
weight: 0.9,
contexts: ['confirmative', 'agreement']
},
'de plus': {
alternatives: ['aussi', 'également', 'qui plus est', 'en plus', 'et puis'],
weight: 0.7,
contexts: ['additive', 'continuation']
},
'cependant': {
alternatives: ['mais', 'pourtant', 'néanmoins', 'malgré tout', 'quand même'],
weight: 0.6,
contexts: ['contrast', 'opposition']
},
'ainsi': {
alternatives: ['donc', 'du coup', 'comme ça', 'par conséquent', 'résultat'],
weight: 0.8,
contexts: ['consequence', 'result']
},
'donc': {
alternatives: ['du coup', 'alors', 'par conséquent', 'ainsi', 'résultat'],
weight: 0.5,
contexts: ['consequence', 'logical']
},
// Connecteurs de séquence
'ensuite': {
alternatives: ['puis', 'après', 'et puis', 'alors', 'du coup'],
weight: 0.6,
contexts: ['sequence', 'temporal']
},
'puis': {
alternatives: ['ensuite', 'après', 'et puis', 'alors'],
weight: 0.4,
contexts: ['sequence', 'temporal']
},
// Connecteurs d'emphase
'également': {
alternatives: ['aussi', 'de même', 'pareillement', 'en plus'],
weight: 0.6,
contexts: ['similarity', 'addition']
},
'aussi': {
alternatives: ['également', 'de même', 'en plus', 'pareillement'],
weight: 0.3,
contexts: ['similarity', 'addition']
},
// Connecteurs de conclusion
'enfin': {
alternatives: ['finalement', 'au final', 'pour finir', 'en dernier'],
weight: 0.5,
contexts: ['conclusion', 'final']
},
'finalement': {
alternatives: ['au final', 'en fin de compte', 'pour finir', 'enfin'],
weight: 0.4,
contexts: ['conclusion', 'final']
}
};
/**
* PATTERNS DE TRANSITION NATURELLE
* Selon le style de personnalité
*/
const PERSONALITY_TRANSITIONS = {
'décontracté': {
preferred: ['du coup', 'alors', 'bon', 'après', 'sinon'],
avoided: ['par conséquent', 'néanmoins', 'toutefois']
},
'technique': {
preferred: ['donc', 'ainsi', 'par conséquent', 'résultat'],
avoided: ['du coup', 'bon', 'franchement']
},
'commercial': {
preferred: ['aussi', 'de plus', 'également', 'qui plus est'],
avoided: ['du coup', 'bon', 'franchement']
},
'familier': {
preferred: ['du coup', 'bon', 'alors', 'après', 'franchement'],
avoided: ['par conséquent', 'néanmoins', 'de surcroît']
}
};
/**
* MAIN ENTRY POINT - HUMANISATION TRANSITIONS
* @param {Object} input - { content: {}, config: {}, context: {} }
* @returns {Object} - { content: {}, stats: {}, debug: {} }
*/
async function humanizeTransitions(input) {
return await tracer.run('TransitionHumanization.humanizeTransitions()', async () => {
const { content, config = {}, context = {} } = input;
const {
intensity = 0.6, // Probabilité de remplacement (60%)
personalityStyle = null, // Style de personnalité pour guidage
avoidRepetition = true, // Éviter répétitions excessives
preserveFormal = false, // Préserver style formel
csvData = null // Données pour personnalité
} = config;
await tracer.annotate({
technique: 'transition_humanization',
intensity,
personalityStyle: personalityStyle || csvData?.personality?.style,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🔗 TECHNIQUE 3/3: Humanisation transitions (intensité: ${intensity})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à humaniser`, 'DEBUG');
try {
const results = {};
let totalProcessed = 0;
let totalReplacements = 0;
let humanizationDetails = [];
// Extraire style de personnalité
const effectivePersonalityStyle = personalityStyle || csvData?.personality?.style || 'neutral';
// Analyser patterns globaux pour éviter répétitions
const globalPatterns = analyzeGlobalTransitionPatterns(content);
// Traiter chaque élément de contenu
for (const [tag, text] of Object.entries(content)) {
totalProcessed++;
if (text.length < 30) {
results[tag] = text;
continue;
}
// Appliquer humanisation des transitions
const humanizationResult = humanizeTextTransitions(text, {
intensity,
personalityStyle: effectivePersonalityStyle,
avoidRepetition,
preserveFormal,
globalPatterns,
tag
});
results[tag] = humanizationResult.text;
if (humanizationResult.replacements.length > 0) {
totalReplacements += humanizationResult.replacements.length;
humanizationDetails.push({
tag,
replacements: humanizationResult.replacements,
transitionsDetected: humanizationResult.transitionsFound
});
logSh(` 🔄 [${tag}]: ${humanizationResult.replacements.length} transitions humanisées`, 'DEBUG');
} else {
logSh(` ➡️ [${tag}]: Transitions déjà naturelles`, 'DEBUG');
}
}
const duration = Date.now() - startTime;
const stats = {
processed: totalProcessed,
totalReplacements,
avgReplacementsPerElement: Math.round(totalReplacements / totalProcessed * 100) / 100,
elementsWithTransitions: humanizationDetails.length,
personalityStyle: effectivePersonalityStyle,
duration,
technique: 'transition_humanization'
};
logSh(`✅ HUMANISATION TRANSITIONS: ${stats.totalReplacements} remplacements sur ${stats.elementsWithTransitions}/${stats.processed} éléments en ${duration}ms`, 'INFO');
await tracer.event('Transition humanization terminée', stats);
return {
content: results,
stats,
debug: {
technique: 'transition_humanization',
config: { intensity, personalityStyle: effectivePersonalityStyle, avoidRepetition },
humanizations: humanizationDetails,
globalPatterns
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ HUMANISATION TRANSITIONS échouée après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`TransitionHumanization failed: ${error.message}`);
}
}, input);
}
/**
* Humaniser les transitions d'un texte
*/
function humanizeTextTransitions(text, config) {
const { intensity, personalityStyle, avoidRepetition, preserveFormal, globalPatterns, tag } = config;
let humanizedText = text;
const replacements = [];
const transitionsFound = [];
// Statistiques usage pour éviter répétitions
const usageStats = {};
// Traiter chaque connecteur du dictionnaire
for (const [transition, transitionData] of Object.entries(TRANSITION_REPLACEMENTS)) {
const { alternatives, weight, contexts } = transitionData;
// Rechercher occurrences (insensible à la casse, mais préserver limites mots)
const regex = new RegExp(`\\b${escapeRegex(transition)}\\b`, 'gi');
const matches = [...text.matchAll(regex)];
if (matches.length > 0) {
transitionsFound.push(transition);
// Décider si on remplace selon intensité et poids
const shouldReplace = Math.random() < (intensity * weight);
if (shouldReplace && !preserveFormal) {
// Sélectionner meilleure alternative
const selectedAlternative = selectBestTransitionAlternative(
alternatives,
personalityStyle,
usageStats,
avoidRepetition
);
// Appliquer remplacement en préservant la casse
humanizedText = humanizedText.replace(regex, (match) => {
return preserveCase(match, selectedAlternative);
});
// Enregistrer usage
usageStats[selectedAlternative] = (usageStats[selectedAlternative] || 0) + matches.length;
replacements.push({
original: transition,
replacement: selectedAlternative,
occurrences: matches.length,
contexts,
personalityMatch: isPersonalityAppropriate(selectedAlternative, personalityStyle)
});
}
}
}
// Post-processing : éviter accumulations
if (avoidRepetition) {
const repetitionCleaned = reduceTransitionRepetition(humanizedText, usageStats);
humanizedText = repetitionCleaned.text;
replacements.push(...repetitionCleaned.additionalChanges);
}
return {
text: humanizedText,
replacements,
transitionsFound
};
}
/**
* Sélectionner meilleure alternative de transition
*/
function selectBestTransitionAlternative(alternatives, personalityStyle, usageStats, avoidRepetition) {
// Filtrer selon personnalité
const personalityFiltered = alternatives.filter(alt =>
isPersonalityAppropriate(alt, personalityStyle)
);
const candidateList = personalityFiltered.length > 0 ? personalityFiltered : alternatives;
if (!avoidRepetition) {
return candidateList[Math.floor(Math.random() * candidateList.length)];
}
// Éviter les alternatives déjà trop utilisées
const lessUsedAlternatives = candidateList.filter(alt =>
(usageStats[alt] || 0) < 2
);
const finalList = lessUsedAlternatives.length > 0 ? lessUsedAlternatives : candidateList;
return finalList[Math.floor(Math.random() * finalList.length)];
}
/**
* Vérifier si alternative appropriée pour personnalité
*/
function isPersonalityAppropriate(alternative, personalityStyle) {
if (!personalityStyle || personalityStyle === 'neutral') return true;
const styleMapping = {
'décontracté': PERSONALITY_TRANSITIONS.décontracté,
'technique': PERSONALITY_TRANSITIONS.technique,
'commercial': PERSONALITY_TRANSITIONS.commercial,
'familier': PERSONALITY_TRANSITIONS.familier
};
const styleConfig = styleMapping[personalityStyle.toLowerCase()];
if (!styleConfig) return true;
// Éviter les connecteurs inappropriés
if (styleConfig.avoided.includes(alternative)) return false;
// Privilégier les connecteurs préférés
if (styleConfig.preferred.includes(alternative)) return true;
return true;
}
/**
* Réduire répétitions excessives de transitions
*/
function reduceTransitionRepetition(text, usageStats) {
let processedText = text;
const additionalChanges = [];
// Identifier connecteurs surutilisés (>3 fois)
const overusedTransitions = Object.entries(usageStats)
.filter(([transition, count]) => count > 3)
.map(([transition]) => transition);
for (const overusedTransition of overusedTransitions) {
// Remplacer quelques occurrences par des alternatives
const regex = new RegExp(`\\b${escapeRegex(overusedTransition)}\\b`, 'g');
let replacements = 0;
processedText = processedText.replace(regex, (match, offset) => {
// Remplacer 1 occurrence sur 3 environ
if (Math.random() < 0.33 && replacements < 2) {
replacements++;
const alternatives = findAlternativesFor(overusedTransition);
const alternative = alternatives[Math.floor(Math.random() * alternatives.length)];
additionalChanges.push({
type: 'repetition_reduction',
original: overusedTransition,
replacement: alternative,
reason: 'overuse'
});
return preserveCase(match, alternative);
}
return match;
});
}
return { text: processedText, additionalChanges };
}
/**
* Trouver alternatives pour un connecteur donné
*/
function findAlternativesFor(transition) {
// Chercher dans le dictionnaire
for (const [key, data] of Object.entries(TRANSITION_REPLACEMENTS)) {
if (data.alternatives.includes(transition)) {
return data.alternatives.filter(alt => alt !== transition);
}
}
// Alternatives génériques
const genericAlternatives = {
'du coup': ['alors', 'donc', 'ainsi'],
'alors': ['du coup', 'donc', 'ensuite'],
'donc': ['du coup', 'alors', 'ainsi'],
'aussi': ['également', 'de plus', 'en plus'],
'mais': ['cependant', 'pourtant', 'néanmoins']
};
return genericAlternatives[transition] || ['donc', 'alors'];
}
/**
* Analyser patterns globaux de transitions
*/
function analyzeGlobalTransitionPatterns(content) {
const allText = Object.values(content).join(' ');
const transitionCounts = {};
const repetitionPatterns = [];
// Compter occurrences globales
for (const transition of Object.keys(TRANSITION_REPLACEMENTS)) {
const regex = new RegExp(`\\b${escapeRegex(transition)}\\b`, 'gi');
const matches = allText.match(regex);
if (matches) {
transitionCounts[transition] = matches.length;
}
}
// Identifier patterns de répétition problématiques
const sortedTransitions = Object.entries(transitionCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 5); // Top 5 plus utilisées
sortedTransitions.forEach(([transition, count]) => {
if (count > 5) {
repetitionPatterns.push({
transition,
count,
severity: count > 10 ? 'high' : count > 7 ? 'medium' : 'low'
});
}
});
return {
transitionCounts,
repetitionPatterns,
diversityScore: Object.keys(transitionCounts).length / Math.max(1, Object.values(transitionCounts).reduce((a,b) => a+b, 0))
};
}
/**
* Préserver la casse originale
*/
function preserveCase(original, replacement) {
if (original === original.toUpperCase()) {
return replacement.toUpperCase();
} else if (original[0] === original[0].toUpperCase()) {
return replacement.charAt(0).toUpperCase() + replacement.slice(1).toLowerCase();
} else {
return replacement.toLowerCase();
}
}
/**
* Échapper caractères regex
*/
function escapeRegex(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Analyser qualité des transitions d'un texte
*/
function analyzeTransitionQuality(text) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 5);
if (sentences.length < 2) {
return { score: 100, issues: [], naturalness: 'high' };
}
let mechanicalTransitions = 0;
let totalTransitions = 0;
const issues = [];
// Analyser chaque transition
sentences.forEach((sentence, index) => {
if (index === 0) return;
const trimmed = sentence.trim();
const startsWithTransition = Object.keys(TRANSITION_REPLACEMENTS).some(transition =>
trimmed.toLowerCase().startsWith(transition.toLowerCase())
);
if (startsWithTransition) {
totalTransitions++;
// Vérifier si transition mécanique
const transition = Object.keys(TRANSITION_REPLACEMENTS).find(t =>
trimmed.toLowerCase().startsWith(t.toLowerCase())
);
if (transition && TRANSITION_REPLACEMENTS[transition].weight > 0.7) {
mechanicalTransitions++;
issues.push({
type: 'mechanical_transition',
transition,
suggestion: TRANSITION_REPLACEMENTS[transition].alternatives[0]
});
}
}
});
const mechanicalRatio = totalTransitions > 0 ? mechanicalTransitions / totalTransitions : 0;
const score = Math.max(0, 100 - (mechanicalRatio * 100));
let naturalness = 'high';
if (mechanicalRatio > 0.5) naturalness = 'low';
else if (mechanicalRatio > 0.25) naturalness = 'medium';
return { score: Math.round(score), issues, naturalness, mechanicalRatio };
}
module.exports = {
humanizeTransitions, // ← MAIN ENTRY POINT
humanizeTextTransitions,
analyzeTransitionQuality,
analyzeGlobalTransitionPatterns,
TRANSITION_REPLACEMENTS,
PERSONALITY_TRANSITIONS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/post-processing/PatternBreaking.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ORCHESTRATEUR PATTERN BREAKING - NIVEAU 2
// Responsabilité: Coordonner les 3 techniques anti-détection
// Objectif: -20% détection IA vs Niveau 1
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
// Import des 3 techniques Pattern Breaking
const { applySentenceVariation } = require('./SentenceVariation');
const { removeLLMFingerprints } = require('./LLMFingerprintRemoval');
const { humanizeTransitions } = require('./TransitionHumanization');
/**
* MAIN ENTRY POINT - PATTERN BREAKING COMPLET
* @param {Object} input - { content: {}, csvData: {}, options: {} }
* @returns {Object} - { content: {}, stats: {}, debug: {} }
*/
async function applyPatternBreaking(input) {
return await tracer.run('PatternBreaking.applyPatternBreaking()', async () => {
const { content, csvData, options = {} } = input;
const config = {
// Configuration globale
intensity: 0.6, // Intensité générale (60%)
// Contrôle par technique
sentenceVariation: true, // Activer variation phrases
fingerprintRemoval: true, // Activer suppression empreintes
transitionHumanization: true, // Activer humanisation transitions
// Configuration spécifique par technique
sentenceVariationConfig: {
intensity: 0.3,
splitThreshold: 100,
mergeThreshold: 30,
preserveQuestions: true,
preserveTitles: true
},
fingerprintRemovalConfig: {
intensity: 1.0,
preserveKeywords: true,
contextualMode: true,
csvData
},
transitionHumanizationConfig: {
intensity: 0.6,
personalityStyle: csvData?.personality?.style,
avoidRepetition: true,
preserveFormal: false,
csvData
},
// Options avancées
qualityPreservation: true, // Préserver qualité contenu
seoIntegrity: true, // Maintenir intégrité SEO
readabilityCheck: true, // Vérifier lisibilité
...options // Override avec options fournies
};
await tracer.annotate({
level: 2,
technique: 'pattern_breaking',
elementsCount: Object.keys(content).length,
personality: csvData?.personality?.nom,
config: {
sentenceVariation: config.sentenceVariation,
fingerprintRemoval: config.fingerprintRemoval,
transitionHumanization: config.transitionHumanization,
intensity: config.intensity
}
});
const startTime = Date.now();
logSh(`🎯 NIVEAU 2: PATTERN BREAKING (3 techniques)`, 'INFO');
logSh(` 🎭 Personnalité: ${csvData?.personality?.nom} (${csvData?.personality?.style})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à traiter`, 'INFO');
logSh(` ⚙️ Techniques actives: ${[config.sentenceVariation && 'Variation', config.fingerprintRemoval && 'Empreintes', config.transitionHumanization && 'Transitions'].filter(Boolean).join(' + ')}`, 'INFO');
try {
let currentContent = { ...content };
const pipelineStats = {
techniques: [],
totalDuration: 0,
qualityMetrics: {}
};
// Analyse initiale de qualité
if (config.qualityPreservation) {
pipelineStats.qualityMetrics.initial = analyzeContentQuality(currentContent);
}
// TECHNIQUE 1: VARIATION LONGUEUR PHRASES
if (config.sentenceVariation) {
const step1Result = await applySentenceVariation({
content: currentContent,
config: config.sentenceVariationConfig,
context: { step: 1, totalSteps: 3 }
});
currentContent = step1Result.content;
pipelineStats.techniques.push({
name: 'SentenceVariation',
...step1Result.stats,
qualityImpact: calculateQualityImpact(content, step1Result.content)
});
logSh(` ✅ 1/3: Variation phrases - ${step1Result.stats.modified}/${step1Result.stats.processed} éléments`, 'INFO');
}
// TECHNIQUE 2: SUPPRESSION EMPREINTES LLM
if (config.fingerprintRemoval) {
const step2Result = await removeLLMFingerprints({
content: currentContent,
config: config.fingerprintRemovalConfig,
context: { step: 2, totalSteps: 3 }
});
currentContent = step2Result.content;
pipelineStats.techniques.push({
name: 'FingerprintRemoval',
...step2Result.stats,
qualityImpact: calculateQualityImpact(content, step2Result.content)
});
logSh(` ✅ 2/3: Suppression empreintes - ${step2Result.stats.totalReplacements} remplacements`, 'INFO');
}
// TECHNIQUE 3: HUMANISATION TRANSITIONS
if (config.transitionHumanization) {
const step3Result = await humanizeTransitions({
content: currentContent,
config: config.transitionHumanizationConfig,
context: { step: 3, totalSteps: 3 }
});
currentContent = step3Result.content;
pipelineStats.techniques.push({
name: 'TransitionHumanization',
...step3Result.stats,
qualityImpact: calculateQualityImpact(content, step3Result.content)
});
logSh(` ✅ 3/3: Humanisation transitions - ${step3Result.stats.totalReplacements} améliorations`, 'INFO');
}
// POST-PROCESSING: Vérifications qualité
if (config.qualityPreservation || config.readabilityCheck) {
const qualityCheck = performQualityChecks(content, currentContent, config);
pipelineStats.qualityMetrics.final = qualityCheck;
// Rollback si qualité trop dégradée
if (qualityCheck.shouldRollback) {
logSh(`⚠️ ROLLBACK: Qualité dégradée, retour contenu original`, 'WARNING');
currentContent = content;
pipelineStats.rollback = true;
}
}
// RÉSULTATS FINAUX
const totalDuration = Date.now() - startTime;
pipelineStats.totalDuration = totalDuration;
const totalModifications = pipelineStats.techniques.reduce((sum, tech) => {
return sum + (tech.modified || tech.totalReplacements || 0);
}, 0);
const stats = {
level: 2,
technique: 'pattern_breaking',
processed: Object.keys(content).length,
totalModifications,
techniquesUsed: pipelineStats.techniques.length,
duration: totalDuration,
techniques: pipelineStats.techniques,
qualityPreserved: !pipelineStats.rollback,
rollback: pipelineStats.rollback || false
};
logSh(`🎯 NIVEAU 2 TERMINÉ: ${totalModifications} modifications sur ${stats.processed} éléments (${totalDuration}ms)`, 'INFO');
// Log détaillé par technique
pipelineStats.techniques.forEach(tech => {
const modificationsCount = tech.modified || tech.totalReplacements || 0;
logSh(`${tech.name}: ${modificationsCount} modifications (${tech.duration}ms)`, 'DEBUG');
});
await tracer.event('Pattern breaking terminé', stats);
return {
content: currentContent,
stats,
debug: {
level: 2,
technique: 'pattern_breaking',
config,
pipeline: pipelineStats,
qualityMetrics: pipelineStats.qualityMetrics
}
};
} catch (error) {
const totalDuration = Date.now() - startTime;
logSh(`❌ NIVEAU 2 ÉCHOUÉ après ${totalDuration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
await tracer.event('Pattern breaking échoué', {
error: error.message,
duration: totalDuration,
fallback: true
});
return {
content,
stats: {
level: 2,
technique: 'pattern_breaking',
processed: Object.keys(content).length,
totalModifications: 0,
duration: totalDuration,
error: error.message,
fallback: true
},
debug: { error: error.message, fallback: true }
};
}
}, input);
}
/**
* MODE DIAGNOSTIC - Test individuel des techniques
*/
async function diagnosticPatternBreaking(content, csvData) {
logSh(`🔬 DIAGNOSTIC NIVEAU 2: Test individuel des techniques`, 'INFO');
const diagnostics = {
techniques: [],
errors: [],
performance: {},
recommendations: []
};
const techniques = [
{ name: 'SentenceVariation', func: applySentenceVariation },
{ name: 'FingerprintRemoval', func: removeLLMFingerprints },
{ name: 'TransitionHumanization', func: humanizeTransitions }
];
for (const technique of techniques) {
try {
const startTime = Date.now();
const result = await technique.func({
content,
config: { csvData },
context: { diagnostic: true }
});
diagnostics.techniques.push({
name: technique.name,
success: true,
duration: Date.now() - startTime,
stats: result.stats,
effectivenessScore: calculateEffectivenessScore(result.stats)
});
} catch (error) {
diagnostics.errors.push({
technique: technique.name,
error: error.message
});
diagnostics.techniques.push({
name: technique.name,
success: false,
error: error.message
});
}
}
// Générer recommandations
diagnostics.recommendations = generateRecommendations(diagnostics.techniques);
const successfulTechniques = diagnostics.techniques.filter(t => t.success);
diagnostics.performance.totalDuration = diagnostics.techniques.reduce((sum, t) => sum + (t.duration || 0), 0);
diagnostics.performance.successRate = Math.round((successfulTechniques.length / techniques.length) * 100);
logSh(`🔬 DIAGNOSTIC TERMINÉ: ${successfulTechniques.length}/${techniques.length} techniques opérationnelles`, 'INFO');
return diagnostics;
}
/**
* Analyser qualité du contenu
*/
function analyzeContentQuality(content) {
const allText = Object.values(content).join(' ');
const wordCount = allText.split(/\s+/).length;
const avgWordsPerElement = wordCount / Object.keys(content).length;
// Métrique de lisibilité approximative (Flesch simplifié)
const sentences = allText.split(/[.!?]+/).filter(s => s.trim().length > 5);
const avgWordsPerSentence = wordCount / Math.max(1, sentences.length);
const readabilityScore = Math.max(0, 100 - (avgWordsPerSentence * 1.5));
return {
wordCount,
elementCount: Object.keys(content).length,
avgWordsPerElement: Math.round(avgWordsPerElement),
avgWordsPerSentence: Math.round(avgWordsPerSentence),
readabilityScore: Math.round(readabilityScore),
sentenceCount: sentences.length
};
}
/**
* Calculer impact qualité entre avant/après
*/
function calculateQualityImpact(originalContent, modifiedContent) {
const originalQuality = analyzeContentQuality(originalContent);
const modifiedQuality = analyzeContentQuality(modifiedContent);
const wordCountChange = ((modifiedQuality.wordCount - originalQuality.wordCount) / originalQuality.wordCount) * 100;
const readabilityChange = modifiedQuality.readabilityScore - originalQuality.readabilityScore;
return {
wordCountChange: Math.round(wordCountChange * 100) / 100,
readabilityChange: Math.round(readabilityChange),
severe: Math.abs(wordCountChange) > 10 || Math.abs(readabilityChange) > 15
};
}
/**
* Effectuer vérifications qualité
*/
function performQualityChecks(originalContent, modifiedContent, config) {
const originalQuality = analyzeContentQuality(originalContent);
const modifiedQuality = analyzeContentQuality(modifiedContent);
const qualityThresholds = {
maxWordCountChange: 15, // % max changement nombre mots
minReadabilityScore: 50, // Score lisibilité minimum
maxReadabilityDrop: 20 // Baisse max lisibilité
};
const issues = [];
// Vérification nombre de mots
const wordCountChange = Math.abs(modifiedQuality.wordCount - originalQuality.wordCount) / originalQuality.wordCount * 100;
if (wordCountChange > qualityThresholds.maxWordCountChange) {
issues.push({
type: 'word_count_change',
severity: 'high',
change: wordCountChange,
threshold: qualityThresholds.maxWordCountChange
});
}
// Vérification lisibilité
if (modifiedQuality.readabilityScore < qualityThresholds.minReadabilityScore) {
issues.push({
type: 'low_readability',
severity: 'medium',
score: modifiedQuality.readabilityScore,
threshold: qualityThresholds.minReadabilityScore
});
}
const readabilityDrop = originalQuality.readabilityScore - modifiedQuality.readabilityScore;
if (readabilityDrop > qualityThresholds.maxReadabilityDrop) {
issues.push({
type: 'readability_drop',
severity: 'high',
drop: readabilityDrop,
threshold: qualityThresholds.maxReadabilityDrop
});
}
// Décision rollback
const highSeverityIssues = issues.filter(issue => issue.severity === 'high');
const shouldRollback = highSeverityIssues.length > 0 && config.qualityPreservation;
return {
originalQuality,
modifiedQuality,
issues,
shouldRollback,
qualityScore: calculateOverallQualityScore(issues, modifiedQuality)
};
}
/**
* Calculer score de qualité global
*/
function calculateOverallQualityScore(issues, quality) {
let baseScore = 100;
issues.forEach(issue => {
const penalty = issue.severity === 'high' ? 30 : issue.severity === 'medium' ? 15 : 5;
baseScore -= penalty;
});
// Bonus pour bonne lisibilité
if (quality.readabilityScore > 70) baseScore += 10;
return Math.max(0, Math.min(100, baseScore));
}
/**
* Calculer score d'efficacité d'une technique
*/
function calculateEffectivenessScore(stats) {
if (!stats) return 0;
const modificationsCount = stats.modified || stats.totalReplacements || 0;
const processedCount = stats.processed || 1;
const modificationRate = (modificationsCount / processedCount) * 100;
// Score basé sur taux de modification et durée
const baseScore = Math.min(100, modificationRate * 2); // Max 50% modification = score 100
const durationPenalty = Math.max(0, (stats.duration - 1000) / 100); // Pénalité si > 1s
return Math.max(0, Math.round(baseScore - durationPenalty));
}
/**
* Générer recommandations basées sur diagnostic
*/
function generateRecommendations(techniqueResults) {
const recommendations = [];
techniqueResults.forEach(tech => {
if (!tech.success) {
recommendations.push({
type: 'error',
technique: tech.name,
message: `${tech.name} a échoué: ${tech.error}`,
action: 'Vérifier configuration et dépendances'
});
return;
}
const effectiveness = tech.effectivenessScore || 0;
if (effectiveness < 30) {
recommendations.push({
type: 'low_effectiveness',
technique: tech.name,
message: `${tech.name} peu efficace (score: ${effectiveness})`,
action: 'Augmenter intensité ou réviser configuration'
});
} else if (effectiveness > 80) {
recommendations.push({
type: 'high_effectiveness',
technique: tech.name,
message: `${tech.name} très efficace (score: ${effectiveness})`,
action: 'Configuration optimale'
});
}
if (tech.duration > 3000) {
recommendations.push({
type: 'performance',
technique: tech.name,
message: `${tech.name} lent (${tech.duration}ms)`,
action: 'Considérer réduction intensité ou optimisation'
});
}
});
return recommendations;
}
module.exports = {
applyPatternBreaking, // ← MAIN ENTRY POINT
diagnosticPatternBreaking, // ← Mode diagnostic
analyzeContentQuality,
performQualityChecks,
calculateQualityImpact,
calculateEffectivenessScore
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/ContentGeneration.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ORCHESTRATEUR GÉNÉRATION - ARCHITECTURE REFACTORISÉE
// Responsabilité: Coordonner les 4 étapes de génération
// ========================================
const { logSh } = require('./ErrorReporting');
const { tracer } = require('./trace');
// Import des 4 étapes séparées
const { generateInitialContent } = require('./generation/InitialGeneration');
const { enhanceTechnicalTerms } = require('./generation/TechnicalEnhancement');
const { enhanceTransitions } = require('./generation/TransitionEnhancement');
const { applyPersonalityStyle } = require('./generation/StyleEnhancement');
// Import Pattern Breaking (Niveau 2)
const { applyPatternBreaking } = require('./post-processing/PatternBreaking');
/**
* MAIN ENTRY POINT - GÉNÉRATION AVEC SELECTIVE ENHANCEMENT
* @param {Object} hierarchy - Hiérarchie des éléments extraits
* @param {Object} csvData - Données CSV avec personnalité
* @param {Object} options - Options de génération
* @returns {Object} - Contenu généré final
*/
async function generateWithContext(hierarchy, csvData, options = {}) {
return await tracer.run('ContentGeneration.generateWithContext()', async () => {
const startTime = Date.now();
const pipelineName = options.patternBreaking ? 'selective_enhancement_with_pattern_breaking' : 'selective_enhancement';
const totalSteps = options.patternBreaking ? 5 : 4;
await tracer.annotate({
pipeline: pipelineName,
elementsCount: Object.keys(hierarchy).length,
personality: csvData.personality?.nom,
mc0: csvData.mc0,
options,
totalSteps
});
logSh(`🚀 DÉBUT PIPELINE ${options.patternBreaking ? 'NIVEAU 2' : 'NIVEAU 1'}`, 'INFO');
logSh(` 🎭 Personnalité: ${csvData.personality?.nom} (${csvData.personality?.style})`, 'INFO');
logSh(` 📊 ${Object.keys(hierarchy).length} éléments à traiter`, 'INFO');
logSh(` 🔧 Options: ${JSON.stringify(options)}`, 'DEBUG');
try {
let pipelineResults = {
content: {},
stats: { stages: [], totalDuration: 0 },
debug: { pipeline: 'selective_enhancement', stages: [] }
};
// ÉTAPE 1: GÉNÉRATION INITIALE (Claude)
const step1Result = await generateInitialContent({
hierarchy,
csvData,
context: { step: 1, totalSteps, options }
});
pipelineResults.content = step1Result.content;
pipelineResults.stats.stages.push({ stage: 1, name: 'InitialGeneration', ...step1Result.stats });
pipelineResults.debug.stages.push(step1Result.debug);
// ÉTAPE 2: ENHANCEMENT TECHNIQUE (GPT-4) - Optionnel
if (!options.skipTechnical) {
const step2Result = await enhanceTechnicalTerms({
content: pipelineResults.content,
csvData,
context: { step: 2, totalSteps, options }
});
pipelineResults.content = step2Result.content;
pipelineResults.stats.stages.push({ stage: 2, name: 'TechnicalEnhancement', ...step2Result.stats });
pipelineResults.debug.stages.push(step2Result.debug);
} else {
logSh(`⏭️ ÉTAPE 2/4 IGNORÉE: Enhancement technique désactivé`, 'INFO');
}
// ÉTAPE 3: ENHANCEMENT TRANSITIONS (Gemini) - Optionnel
if (!options.skipTransitions) {
const step3Result = await enhanceTransitions({
content: pipelineResults.content,
csvData,
context: { step: 3, totalSteps, options }
});
pipelineResults.content = step3Result.content;
pipelineResults.stats.stages.push({ stage: 3, name: 'TransitionEnhancement', ...step3Result.stats });
pipelineResults.debug.stages.push(step3Result.debug);
} else {
logSh(`⏭️ ÉTAPE 3/4 IGNORÉE: Enhancement transitions désactivé`, 'INFO');
}
// ÉTAPE 4: ENHANCEMENT STYLE (Mistral) - Optionnel
if (!options.skipStyle) {
const step4Result = await applyPersonalityStyle({
content: pipelineResults.content,
csvData,
context: { step: 4, totalSteps, options }
});
pipelineResults.content = step4Result.content;
pipelineResults.stats.stages.push({ stage: 4, name: 'StyleEnhancement', ...step4Result.stats });
pipelineResults.debug.stages.push(step4Result.debug);
} else {
logSh(`⏭️ ÉTAPE 4/${totalSteps} IGNORÉE: Enhancement style désactivé`, 'INFO');
}
// ÉTAPE 5: PATTERN BREAKING (NIVEAU 2) - Optionnel
if (options.patternBreaking) {
const step5Result = await applyPatternBreaking({
content: pipelineResults.content,
csvData,
options: options.patternBreakingConfig || {}
});
pipelineResults.content = step5Result.content;
pipelineResults.stats.stages.push({ stage: 5, name: 'PatternBreaking', ...step5Result.stats });
pipelineResults.debug.stages.push(step5Result.debug);
} else if (totalSteps === 5) {
logSh(`⏭️ ÉTAPE 5/5 IGNORÉE: Pattern Breaking désactivé`, 'INFO');
}
// RÉSULTATS FINAUX
const totalDuration = Date.now() - startTime;
pipelineResults.stats.totalDuration = totalDuration;
const totalProcessed = pipelineResults.stats.stages.reduce((sum, stage) => sum + (stage.processed || 0), 0);
const totalEnhanced = pipelineResults.stats.stages.reduce((sum, stage) => sum + (stage.enhanced || 0), 0);
logSh(`✅ PIPELINE TERMINÉ: ${Object.keys(pipelineResults.content).length} éléments générés`, 'INFO');
logSh(` ⏱️ Durée totale: ${totalDuration}ms`, 'INFO');
logSh(` 📈 Enhancements: ${totalEnhanced} sur ${totalProcessed} éléments traités`, 'INFO');
// Log détaillé par étape
pipelineResults.stats.stages.forEach(stage => {
const enhancementRate = stage.processed > 0 ? Math.round((stage.enhanced / stage.processed) * 100) : 0;
logSh(` ${stage.stage}. ${stage.name}: ${stage.enhanced}/${stage.processed} (${enhancementRate}%) en ${stage.duration}ms`, 'DEBUG');
});
await tracer.event(`Pipeline ${pipelineName} terminé`, {
totalElements: Object.keys(pipelineResults.content).length,
totalEnhanced,
totalDuration,
stagesExecuted: pipelineResults.stats.stages.length
});
// Retourner uniquement le contenu pour compatibilité
return pipelineResults.content;
} catch (error) {
const totalDuration = Date.now() - startTime;
logSh(`❌ PIPELINE ÉCHOUÉ après ${totalDuration}ms: ${error.message}`, 'ERROR');
logSh(`❌ Stack trace: ${error.stack}`, 'DEBUG');
await tracer.event(`Pipeline ${pipelineName} échoué`, {
error: error.message,
duration: totalDuration
});
throw new Error(`ContentGeneration pipeline failed: ${error.message}`);
}
}, { hierarchy, csvData, options });
}
/**
* GÉNÉRATION SIMPLE (ÉTAPE 1 UNIQUEMENT)
* Pour tests ou fallback rapide
*/
async function generateSimple(hierarchy, csvData) {
logSh(`🔥 GÉNÉRATION SIMPLE: Claude uniquement`, 'INFO');
const result = await generateInitialContent({
hierarchy,
csvData,
context: { step: 1, totalSteps: 1, simple: true }
});
return result.content;
}
/**
* GÉNÉRATION AVANCÉE AVEC CONTRÔLE GRANULAIRE
* Permet de choisir exactement quelles étapes exécuter
*/
async function generateAdvanced(hierarchy, csvData, stageConfig = {}) {
const {
initial = true,
technical = true,
transitions = true,
style = true,
patternBreaking = false, // ✨ NOUVEAU: Niveau 2
patternBreakingConfig = {} // ✨ NOUVEAU: Config Pattern Breaking
} = stageConfig;
const options = {
skipTechnical: !technical,
skipTransitions: !transitions,
skipStyle: !style,
patternBreaking, // ✨ NOUVEAU
patternBreakingConfig // ✨ NOUVEAU
};
const activeStages = [
initial && 'Initial',
technical && 'Technical',
transitions && 'Transitions',
style && 'Style',
patternBreaking && 'PatternBreaking' // ✨ NOUVEAU
].filter(Boolean);
logSh(`🎛️ GÉNÉRATION AVANCÉE: ${activeStages.join(' + ')}`, 'INFO');
return await generateWithContext(hierarchy, csvData, options);
}
/**
* GÉNÉRATION NIVEAU 2 (AVEC PATTERN BREAKING)
* Shortcut pour activer Pattern Breaking facilement
*/
async function generateWithPatternBreaking(hierarchy, csvData, patternConfig = {}) {
logSh(`🎯 GÉNÉRATION NIVEAU 2: Pattern Breaking activé`, 'INFO');
const options = {
patternBreaking: true,
patternBreakingConfig: {
intensity: 0.6,
sentenceVariation: true,
fingerprintRemoval: true,
transitionHumanization: true,
...patternConfig
}
};
return await generateWithContext(hierarchy, csvData, options);
}
/**
* DIAGNOSTIC PIPELINE
* Exécute chaque étape avec mesures détaillées
*/
async function diagnosticPipeline(hierarchy, csvData) {
logSh(`🔬 MODE DIAGNOSTIC: Analyse détaillée pipeline`, 'INFO');
const diagnostics = {
stages: [],
errors: [],
performance: {},
content: {}
};
let currentContent = {};
try {
// Test étape 1
const step1Start = Date.now();
const step1Result = await generateInitialContent({ hierarchy, csvData });
diagnostics.stages.push({
stage: 1,
name: 'InitialGeneration',
success: true,
duration: Date.now() - step1Start,
elementsGenerated: Object.keys(step1Result.content).length,
stats: step1Result.stats
});
currentContent = step1Result.content;
} catch (error) {
diagnostics.errors.push({ stage: 1, error: error.message });
diagnostics.stages.push({ stage: 1, name: 'InitialGeneration', success: false });
return diagnostics;
}
// Test étapes 2-4 individuellement
const stages = [
{ stage: 2, name: 'TechnicalEnhancement', func: enhanceTechnicalTerms },
{ stage: 3, name: 'TransitionEnhancement', func: enhanceTransitions },
{ stage: 4, name: 'StyleEnhancement', func: applyPersonalityStyle }
];
for (const stageInfo of stages) {
try {
const stageStart = Date.now();
const stageResult = await stageInfo.func({ content: currentContent, csvData });
diagnostics.stages.push({
...stageInfo,
success: true,
duration: Date.now() - stageStart,
stats: stageResult.stats
});
currentContent = stageResult.content;
} catch (error) {
diagnostics.errors.push({ stage: stageInfo.stage, error: error.message });
diagnostics.stages.push({ ...stageInfo, success: false });
}
}
diagnostics.content = currentContent;
diagnostics.performance.totalDuration = diagnostics.stages.reduce((sum, stage) => sum + (stage.duration || 0), 0);
logSh(`🔬 DIAGNOSTIC TERMINÉ: ${diagnostics.stages.filter(s => s.success).length}/4 étapes réussies`, 'INFO');
return diagnostics;
}
module.exports = {
generateWithContext, // ← MAIN ENTRY POINT (compatible ancien code)
generateSimple, // ← Génération rapide
generateAdvanced, // ← Contrôle granulaire
generateWithPatternBreaking, // ← NOUVEAU: Niveau 2 shortcut
diagnosticPipeline // ← Tests et debug
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/ContentAssembly.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: ContentAssembly.js
// Description: Assemblage et nettoyage du contenu XML
// ========================================
const { logSh } = require('./ErrorReporting'); // Using unified logSh from ErrorReporting
/**
* Nettoie les balises <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();
// 1. ANALYSER l'ordre original des éléments
const originalOrder = elements ? elements.map(el => el.originalTag) : Object.keys(generatedTexts);
logSh(`📋 Analyse de ${originalOrder.length} éléments dans l'ordre original...`, 'DEBUG');
// 2. DÉTECTER les associations naturelles
for (let i = 0; i < originalOrder.length; i++) {
const currentTag = originalOrder[i];
const currentContent = generatedTexts[currentTag];
if (!currentContent || usedTags.has(currentTag)) continue;
const currentType = identifyElementType(currentTag);
if (currentType === 'titre_h1' || currentType === 'titre_h2' || currentType === 'titre_h3') {
// CHERCHER le contenu associé qui suit
const associatedContent = findAssociatedContent(originalOrder, i, generatedTexts, usedTags);
sections.push({
type: 'header_with_content',
title: currentContent,
content: associatedContent.content,
titleTag: currentTag,
contentTag: associatedContent.tag
});
usedTags.add(currentTag);
if (associatedContent.tag) {
usedTags.add(associatedContent.tag);
}
logSh(` ✓ Section: ${currentType} + contenu associé`, 'DEBUG');
}
else if (currentType === 'faq_question') {
// CHERCHER la réponse correspondante
const matchingAnswer = findMatchingFAQAnswer(currentTag, generatedTexts);
if (matchingAnswer) {
sections.push({
type: 'faq_pair',
question: currentContent,
answer: matchingAnswer.content,
questionTag: currentTag,
answerTag: matchingAnswer.tag
});
usedTags.add(currentTag);
usedTags.add(matchingAnswer.tag);
logSh(` ✓ Paire FAQ: ${currentTag} + ${matchingAnswer.tag}`, 'DEBUG');
}
}
else if (currentType !== 'faq_reponse') {
// CONTENU STANDALONE (pas une réponse FAQ déjà traitée)
sections.push({
type: 'standalone_content',
content: currentContent,
contentTag: currentTag
});
usedTags.add(currentTag);
logSh(` ✓ Contenu standalone: ${currentType}`, 'DEBUG');
}
}
logSh(`🏗️ ${sections.length} sections organiques construites`, 'INFO');
return sections;
}
/**
* Trouver le contenu associé à un titre (paragraphe qui suit)
*/
function findAssociatedContent(originalOrder, titleIndex, generatedTexts, usedTags) {
// Chercher dans les éléments suivants
for (let j = titleIndex + 1; j < originalOrder.length; j++) {
const nextTag = originalOrder[j];
const nextContent = generatedTexts[nextTag];
if (!nextContent || usedTags.has(nextTag)) continue;
const nextType = identifyElementType(nextTag);
// Si on trouve un autre titre, on s'arrête
if (nextType === 'titre_h1' || nextType === 'titre_h2' || nextType === 'titre_h3') {
break;
}
// Si on trouve du contenu (texte, intro), c'est probablement associé
if (nextType === 'texte' || nextType === 'intro') {
return {
content: nextContent,
tag: nextTag
};
}
}
return { content: null, tag: null };
}
/**
* Extraire le numéro d'une FAQ : |Faq_q_1| ou |Faq_a_2| → "1" ou "2"
*/
function extractFAQNumber(tag) {
const match = tag.match(/(\d+)/);
return match ? match[1] : null;
}
/**
* Trouver la réponse FAQ correspondant à une question
*/
function findMatchingFAQAnswer(questionTag, generatedTexts) {
// Extraire le numéro : |Faq_q_1| → 1
const questionNumber = extractFAQNumber(questionTag);
if (!questionNumber) return null;
// Chercher la réponse correspondante
for (const tag in generatedTexts) {
const tagType = identifyElementType(tag);
if (tagType === 'faq_reponse') {
const answerNumber = extractFAQNumber(tag);
if (answerNumber === questionNumber) {
return {
content: generatedTexts[tag],
tag: tag
};
}
}
}
return null;
}
/**
* Nouvelle fonction de sauvegarde avec compilation organique
*/
async function saveGeneratedArticleOrganic(articleData, csvData, config = {}) {
try {
logSh('💾 Sauvegarde article avec compilation organique...', 'INFO');
const sheets = await getSheetsClient();
// Vérifier si la sheet existe, sinon la créer
let articlesSheet = await getOrCreateSheet(sheets, 'Generated_Articles');
// ===== COMPILATION ORGANIQUE =====
const compiledText = await compileGeneratedTextsOrganic(
articleData.generatedTexts,
articleData.originalElements // Passer les éléments originaux si disponibles
);
logSh(`📝 Texte compilé organiquement: ${compiledText.length} caractères`, 'INFO');
// Métadonnées avec format français
const now = new Date();
const frenchTimestamp = formatDateToFrench(now);
// UTILISER le slug du CSV (colonne A du Google Sheet source)
// Le slug doit venir de csvData.slug (récupéré via getBrainConfig)
const slug = csvData.slug || generateSlugFromContent(csvData.mc0, csvData.t0);
const metadata = {
timestamp: frenchTimestamp,
slug: slug,
mc0: csvData.mc0,
t0: csvData.t0,
personality: csvData.personality?.nom || 'Unknown',
antiDetectionLevel: config.antiDetectionLevel || 'MVP',
elementsCount: Object.keys(articleData.generatedTexts || {}).length,
textLength: compiledText.length,
wordCount: countWords(compiledText),
llmUsed: config.llmUsed || 'openai',
validationStatus: articleData.validationReport?.status || 'unknown'
};
// Préparer la ligne de données
const row = [
metadata.timestamp,
metadata.slug,
metadata.mc0,
metadata.t0,
metadata.personality,
metadata.antiDetectionLevel,
compiledText, // ← TEXTE ORGANIQUE
metadata.textLength,
metadata.wordCount,
metadata.elementsCount,
metadata.llmUsed,
metadata.validationStatus,
'', '', '', '',
JSON.stringify({
csvData: csvData,
config: config,
stats: metadata
})
];
// DEBUG: Vérifier le slug généré
logSh(`💾 Sauvegarde avec slug: "${metadata.slug}" (colonne B)`, 'DEBUG');
// Ajouter la ligne aux données
await sheets.spreadsheets.values.append({
spreadsheetId: SHEET_CONFIG.sheetId,
range: 'Generated_Articles!A:Q',
valueInputOption: 'USER_ENTERED',
resource: {
values: [row]
}
});
// Récupérer le numéro de ligne pour l'ID article
const response = await sheets.spreadsheets.values.get({
spreadsheetId: SHEET_CONFIG.sheetId,
range: 'Generated_Articles!A:A'
});
const articleId = response.data.values ? response.data.values.length - 1 : 1;
logSh(`✅ Article organique sauvé: ID ${articleId}, ${metadata.wordCount} mots`, 'INFO');
return {
articleId: articleId,
textLength: metadata.textLength,
wordCount: metadata.wordCount,
sheetRow: response.data.values ? response.data.values.length : 2
};
} catch (error) {
logSh(`❌ Erreur sauvegarde organique: ${error.toString()}`, 'ERROR');
throw error;
}
}
/**
* Générer un slug à partir du contenu MC0 et T0
*/
function generateSlugFromContent(mc0, t0) {
if (!mc0 && !t0) return 'article-generated';
const source = mc0 || t0;
return source
.toString()
.toLowerCase()
.replace(/[àáâäã]/g, 'a')
.replace(/[èéêë]/g, 'e')
.replace(/[ìíîï]/g, 'i')
.replace(/[òóôöõ]/g, 'o')
.replace(/[ùúûü]/g, 'u')
.replace(/[ç]/g, 'c')
.replace(/[ñ]/g, 'n')
.replace(/[^a-z0-9\s-]/g, '') // Enlever caractères spéciaux
.replace(/\s+/g, '-') // Espaces -> tirets
.replace(/-+/g, '-') // Éviter doubles tirets
.replace(/^-+|-+$/g, '') // Enlever tirets début/fin
.substring(0, 50); // Limiter longueur
}
/**
* Identifier le type d'élément par son tag
*/
function identifyElementType(tag) {
const cleanTag = tag.toLowerCase().replace(/[|{}]/g, '');
if (cleanTag.includes('titre_h1') || cleanTag.includes('h1')) return 'titre_h1';
if (cleanTag.includes('titre_h2') || cleanTag.includes('h2')) return 'titre_h2';
if (cleanTag.includes('titre_h3') || cleanTag.includes('h3')) return 'titre_h3';
if (cleanTag.includes('intro')) return 'intro';
if (cleanTag.includes('faq_q') || cleanTag.includes('faq_question')) return 'faq_question';
if (cleanTag.includes('faq_a') || cleanTag.includes('faq_reponse')) return 'faq_reponse';
return 'texte'; // Par défaut
}
/**
* Nettoyer un contenu individuel
*/
function cleanIndividualContent(content) {
if (!content) return '';
let cleaned = content.toString();
// 1. Supprimer les balises HTML
cleaned = cleaned.replace(/<[^>]*>/g, '');
// 2. Décoder les entités HTML
cleaned = cleaned.replace(/&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) {
logSh('🗄️ Création sheet Generated_Articles...', 'INFO');
try {
// Créer la nouvelle sheet
await sheets.spreadsheets.batchUpdate({
spreadsheetId: SHEET_CONFIG.sheetId,
resource: {
requests: [{
addSheet: {
properties: {
title: 'Generated_Articles'
}
}
}]
}
});
// Headers
const headers = [
'Timestamp',
'Slug',
'MC0',
'T0',
'Personality',
'AntiDetection_Level',
'Compiled_Text', // ← COLONNE PRINCIPALE
'Text_Length',
'Word_Count',
'Elements_Count',
'LLM_Used',
'Validation_Status',
'GPTZero_Score', // Scores détecteurs (à remplir)
'Originality_Score',
'CopyLeaks_Score',
'Human_Quality_Score',
'Full_Metadata_JSON' // Backup complet
];
// Ajouter les headers
await sheets.spreadsheets.values.update({
spreadsheetId: SHEET_CONFIG.sheetId,
range: 'Generated_Articles!A1:Q1',
valueInputOption: 'USER_ENTERED',
resource: {
values: [headers]
}
});
// Formatter les headers
await sheets.spreadsheets.batchUpdate({
spreadsheetId: SHEET_CONFIG.sheetId,
resource: {
requests: [{
repeatCell: {
range: {
sheetId: await getSheetIdByName(sheets, 'Generated_Articles'),
startRowIndex: 0,
endRowIndex: 1,
startColumnIndex: 0,
endColumnIndex: headers.length
},
cell: {
userEnteredFormat: {
textFormat: {
bold: true
},
backgroundColor: {
red: 0.878,
green: 0.878,
blue: 0.878
},
horizontalAlignment: 'CENTER'
}
},
fields: 'userEnteredFormat(textFormat,backgroundColor,horizontalAlignment)'
}
}]
}
});
logSh('✅ Sheet Generated_Articles créée avec succès', 'INFO');
return true;
} catch (error) {
logSh(`❌ Erreur création sheet: ${error.toString()}`, 'ERROR');
throw error;
}
}
/**
* Formater date au format français DD/MM/YYYY HH:mm:ss
*/
function formatDateToFrench(date) {
// Utiliser toLocaleString avec le format français
return date.toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZone: 'Europe/Paris'
}).replace(',', '');
}
/**
* Compter les mots dans un texte
*/
function countWords(text) {
if (!text || text.trim() === '') return 0;
return text.trim().split(/\s+/).length;
}
/**
* Récupérer un article sauvé par ID
*/
async function getStoredArticle(articleId) {
try {
const sheets = await getSheetsClient();
const rowNumber = articleId + 2; // +2 car header + 0-indexing
const response = await sheets.spreadsheets.values.get({
spreadsheetId: SHEET_CONFIG.sheetId,
range: `Generated_Articles!A${rowNumber}:Q${rowNumber}`
});
if (!response.data.values || response.data.values.length === 0) {
throw new Error(`Article ${articleId} non trouvé`);
}
const data = response.data.values[0];
return {
articleId: articleId,
timestamp: data[0],
slug: data[1],
mc0: data[2],
t0: data[3],
personality: data[4],
antiDetectionLevel: data[5],
compiledText: data[6], // ← TEXTE PUR
textLength: data[7],
wordCount: data[8],
elementsCount: data[9],
llmUsed: data[10],
validationStatus: data[11],
gptZeroScore: data[12],
originalityScore: data[13],
copyLeaksScore: data[14],
humanScore: data[15],
fullMetadata: data[16] ? JSON.parse(data[16]) : null
};
} catch (error) {
logSh(`❌ Erreur récupération article ${articleId}: ${error.toString()}`, 'ERROR');
throw error;
}
}
/**
* Lister les derniers articles générés
*/
async function getRecentArticles(limit = 10) {
try {
const sheets = await getSheetsClient();
const response = await sheets.spreadsheets.values.get({
spreadsheetId: SHEET_CONFIG.sheetId,
range: 'Generated_Articles!A:L'
});
if (!response.data.values || response.data.values.length <= 1) {
return []; // Pas de données ou seulement headers
}
const data = response.data.values.slice(1); // Exclure headers
const startIndex = Math.max(0, data.length - limit);
const recentData = data.slice(startIndex);
return recentData.map((row, index) => ({
articleId: startIndex + index,
timestamp: row[0],
slug: row[1],
mc0: row[2],
personality: row[4],
antiDetectionLevel: row[5],
wordCount: row[8],
validationStatus: row[11]
})).reverse(); // Plus récents en premier
} catch (error) {
logSh(`❌ Erreur liste articles récents: ${error.toString()}`, 'ERROR');
return [];
}
}
/**
* Mettre à jour les scores de détection d'un article
*/
async function updateDetectionScores(articleId, scores) {
try {
const sheets = await getSheetsClient();
const rowNumber = articleId + 2;
const updates = [];
// Colonnes des scores : M, N, O (GPTZero, Originality, CopyLeaks)
if (scores.gptzero !== undefined) {
updates.push({
range: `Generated_Articles!M${rowNumber}`,
values: [[scores.gptzero]]
});
}
if (scores.originality !== undefined) {
updates.push({
range: `Generated_Articles!N${rowNumber}`,
values: [[scores.originality]]
});
}
if (scores.copyleaks !== undefined) {
updates.push({
range: `Generated_Articles!O${rowNumber}`,
values: [[scores.copyleaks]]
});
}
if (updates.length > 0) {
await sheets.spreadsheets.values.batchUpdate({
spreadsheetId: SHEET_CONFIG.sheetId,
resource: {
valueInputOption: 'USER_ENTERED',
data: updates
}
});
}
logSh(`✅ Scores détection mis à jour pour article ${articleId}`, 'INFO');
} catch (error) {
logSh(`❌ Erreur maj scores article ${articleId}: ${error.toString()}`, 'ERROR');
throw error;
}
}
// ============= HELPERS GOOGLE SHEETS =============
/**
* Obtenir le client Google Sheets authentifié
*/
async function getSheetsClient() {
const auth = new google.auth.GoogleAuth({
credentials: {
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n')
},
scopes: ['https://www.googleapis.com/auth/spreadsheets']
});
const authClient = await auth.getClient();
const sheets = google.sheets({ version: 'v4', auth: authClient });
return sheets;
}
/**
* Obtenir ou créer une sheet
*/
async function getOrCreateSheet(sheets, sheetName) {
try {
// Vérifier si la sheet existe
const response = await sheets.spreadsheets.get({
spreadsheetId: SHEET_CONFIG.sheetId
});
const existingSheet = response.data.sheets.find(
sheet => sheet.properties.title === sheetName
);
if (existingSheet) {
return existingSheet;
} else {
// Créer la sheet si elle n'existe pas
if (sheetName === 'Generated_Articles') {
await createArticlesStorageSheet(sheets);
return await getOrCreateSheet(sheets, sheetName); // Récursif pour récupérer la sheet créée
}
throw new Error(`Sheet ${sheetName} non supportée pour création automatique`);
}
} catch (error) {
logSh(`❌ Erreur accès/création sheet ${sheetName}: ${error.toString()}`, 'ERROR');
throw error;
}
}
/**
* Obtenir l'ID d'une sheet par son nom
*/
async function getSheetIdByName(sheets, sheetName) {
const response = await sheets.spreadsheets.get({
spreadsheetId: SHEET_CONFIG.sheetId
});
const sheet = response.data.sheets.find(
s => s.properties.title === sheetName
);
return sheet ? sheet.properties.sheetId : null;
}
// ============= EXPORTS =============
module.exports = {
compileGeneratedTextsOrganic,
buildOrganicSections,
findAssociatedContent,
extractFAQNumber,
findMatchingFAQAnswer,
saveGeneratedArticleOrganic,
identifyElementType,
cleanIndividualContent,
createArticlesStorageSheet,
formatDateToFrench,
countWords,
getStoredArticle,
getRecentArticles,
updateDetectionScores,
getSheetsClient,
getOrCreateSheet,
getSheetIdByName,
generateSlugFromContent
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/DigitalOceanWorkflow.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: DigitalOceanWorkflow.js - REFACTORISÉ POUR NODE.JS
// RESPONSABILITÉ: Orchestration + Interface Digital Ocean UNIQUEMENT
// ========================================
const crypto = require('crypto');
const axios = require('axios');
const { GoogleSpreadsheet } = require('google-spreadsheet');
const { JWT } = require('google-auth-library');
// Import des autres modules du projet (à adapter selon votre structure)
const { logSh } = require('./ErrorReporting');
const { handleFullWorkflow } = require('./Main');
const { getPersonalities, selectPersonalityWithAI } = require('./BrainConfig');
// ============= CONFIGURATION DIGITAL OCEAN =============
const DO_CONFIG = {
endpoint: 'https://autocollant.fra1.digitaloceanspaces.com',
bucketName: 'autocollant',
accessKeyId: 'DO801XTYPE968NZGAQM3',
secretAccessKey: '5aCCBiS9K+J8gsAe3M3/0GlliHCNjtLntwla1itCN1s',
region: 'fra1'
};
// Configuration Google Sheets
const SHEET_CONFIG = {
sheetId: '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c',
serviceAccountEmail: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
privateKey: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
// Alternative: utiliser fichier JSON directement
keyFile: './seo-generator-470715-85d4a971c1af.json'
};
async function deployArticle({ path, html, dryRun = false, ...rest }) {
if (!path || typeof html !== 'string') {
const err = new Error('deployArticle: invalid payload (requires {path, html})');
err.code = 'E_PAYLOAD';
throw err;
}
if (dryRun) {
return {
ok: true,
dryRun: true,
length: html.length,
path,
meta: rest || {}
};
}
// --- Impl réelle à toi ici (upload DO Spaces / API / SSH etc.) ---
// return await realDeploy({ path, html, ...rest });
// Placeholder pour ne pas casser l'appel si pas encore implémenté
return { ok: true, dryRun: false, path, length: html.length };
}
module.exports.deployArticle = module.exports.deployArticle || deployArticle;
// ============= TRIGGER PRINCIPAL REMPLACÉ PAR WEBHOOK/API =============
/**
* Point d'entrée pour déclencher le workflow
* Remplace le trigger onEdit d'Apps Script
* @param {number} rowNumber - Numéro de ligne à traiter
* @returns {Promise<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/Main.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: lib/main.js - CONVERTI POUR NODE.JS
// RESPONSABILITÉ: COEUR DU WORKFLOW DE GÉNÉRATION
// ========================================
// 🔧 CONFIGURATION ENVIRONNEMENT
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
// 🔄 IMPORTS NODE.JS (remplace les dépendances Apps Script)
const { getBrainConfig } = require('./BrainConfig');
const { extractElements, buildSmartHierarchy } = require('./ElementExtraction');
const { generateMissingKeywords } = require('./MissingKeywords');
const { generateWithContext } = require('./ContentGeneration');
const { injectGeneratedContent, cleanStrongTags } = require('./ContentAssembly');
const { validateWorkflowIntegrity, logSh } = require('./ErrorReporting');
const { saveGeneratedArticleOrganic } = require('./ArticleStorage');
const { tracer } = require('./trace.js');
const { fetchXMLFromDigitalOcean } = require('./DigitalOceanWorkflow');
const { spawn } = require('child_process');
const path = require('path');
// Variable pour éviter de relancer Edge plusieurs fois
let logViewerLaunched = false;
/**
* Lancer le log viewer dans Edge
*/
function launchLogViewer() {
if (logViewerLaunched || process.env.NODE_ENV === 'test') return;
try {
const logViewerPath = path.join(__dirname, '..', 'tools', 'logs-viewer.html');
const fileUrl = `file:///${logViewerPath.replace(/\\/g, '/')}`;
// Détecter l'environnement et adapter la commande
const isWSL = process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP;
const isWindows = process.platform === 'win32';
if (isWindows && !isWSL) {
// Windows natif
const edgeProcess = spawn('cmd', ['/c', 'start', 'msedge', fileUrl], {
detached: true,
stdio: 'ignore'
});
edgeProcess.unref();
} else if (isWSL) {
// WSL - utiliser cmd.exe via /mnt/c/Windows/System32/
const edgeProcess = spawn('/mnt/c/Windows/System32/cmd.exe', ['/c', 'start', 'msedge', fileUrl], {
detached: true,
stdio: 'ignore'
});
edgeProcess.unref();
} else {
// Linux/Mac - essayer xdg-open ou open
const command = process.platform === 'darwin' ? 'open' : 'xdg-open';
const browserProcess = spawn(command, [fileUrl], {
detached: true,
stdio: 'ignore'
});
browserProcess.unref();
}
logViewerLaunched = true;
logSh('🌐 Log viewer lancé', 'INFO');
} catch (error) {
logSh(`⚠️ Impossible d'ouvrir le log viewer: ${error.message}`, 'WARNING');
}
}
/**
* COEUR DU WORKFLOW - Compatible Make.com ET Digital Ocean ET Node.js
* @param {object} data - Données du workflow
* @param {string} data.xmlTemplate - XML template (base64 encodé)
* @param {object} data.csvData - Données CSV ou rowNumber
* @param {string} data.source - 'make_com' | 'digital_ocean_autonomous' | 'node_server'
*/
async function handleFullWorkflow(data) {
// Lancer le log viewer au début du workflow
launchLogViewer();
return await tracer.run('Main.handleFullWorkflow()', async () => {
await tracer.annotate({ source: data.source || 'node_server', mc0: data.csvData?.mc0 || data.rowNumber });
// 1. PRÉPARER LES DONNÉES CSV
const csvData = await tracer.run('Main.prepareCSVData()', async () => {
const result = await prepareCSVData(data);
await tracer.event(`CSV préparé: ${result.mc0}`, { csvKeys: Object.keys(result) });
return result;
}, { rowNumber: data.rowNumber, source: data.source });
// 2. DÉCODER LE XML TEMPLATE
const xmlString = await tracer.run('Main.decodeXMLTemplate()', async () => {
const result = decodeXMLTemplate(data.xmlTemplate);
await tracer.event(`XML décodé: ${result.length} caractères`);
return result;
}, { templateLength: data.xmlTemplate?.length });
// 3. PREPROCESSING XML
const processedXML = await tracer.run('Main.preprocessXML()', async () => {
const result = preprocessXML(xmlString);
await tracer.event('XML préprocessé');
global.currentXmlTemplate = result;
return result;
}, { originalLength: xmlString?.length });
// 4. EXTRAIRE ÉLÉMENTS
const elements = await tracer.run('ElementExtraction.extractElements()', async () => {
const result = await extractElements(processedXML, csvData);
await tracer.event(`${result.length} éléments extraits`);
return result;
}, { xmlLength: processedXML?.length, mc0: csvData.mc0 });
// 5. GÉNÉRER MOTS-CLÉS MANQUANTS
const finalElements = await tracer.run('MissingKeywords.generateMissingKeywords()', async () => {
const updatedElements = await generateMissingKeywords(elements, csvData);
const result = Object.keys(updatedElements).length > 0 ? updatedElements : elements;
await tracer.event('Mots-clés manquants traités');
return result;
}, { elementsCount: elements.length, mc0: csvData.mc0 });
// 6. CONSTRUIRE HIÉRARCHIE INTELLIGENTE
const hierarchy = await tracer.run('ElementExtraction.buildSmartHierarchy()', async () => {
const result = await buildSmartHierarchy(finalElements);
await tracer.event(`Hiérarchie construite: ${Object.keys(result).length} sections`);
return result;
}, { finalElementsCount: finalElements.length });
// 7. 🎯 GÉNÉRATION AVEC SELECTIVE ENHANCEMENT (Phase 2)
const generatedContent = await tracer.run('ContentGeneration.generateWithContext()', async () => {
const result = await generateWithContext(hierarchy, csvData);
await tracer.event(`Contenu généré: ${Object.keys(result).length} éléments`);
return result;
}, { elementsCount: Object.keys(hierarchy).length, personality: csvData.personality?.nom });
// 8. ASSEMBLER XML FINAL
const finalXML = await tracer.run('ContentAssembly.injectGeneratedContent()', async () => {
const result = injectGeneratedContent(processedXML, generatedContent, finalElements);
await tracer.event('XML final assemblé');
return result;
}, { contentPieces: Object.keys(generatedContent).length, elementsCount: finalElements.length });
// 9. VALIDATION INTÉGRITÉ
const validationReport = await tracer.run('ErrorReporting.validateWorkflowIntegrity()', async () => {
const result = validateWorkflowIntegrity(finalElements, generatedContent, finalXML, csvData);
await tracer.event(`Validation: ${result.status}`);
return result;
}, { finalXMLLength: finalXML?.length, contentKeys: Object.keys(generatedContent).length });
// 10. SAUVEGARDE ARTICLE
const articleStorage = await tracer.run('Main.saveArticle()', async () => {
const result = await saveArticle(finalXML, generatedContent, finalElements, csvData, data.source);
if (result) {
await tracer.event(`Article sauvé: ID ${result.articleId}`);
}
return result;
}, { source: data.source, mc0: csvData.mc0, elementsCount: finalElements.length });
// 11. RÉPONSE FINALE
const response = await tracer.run('Main.buildWorkflowResponse()', async () => {
const result = await buildWorkflowResponse(finalXML, generatedContent, finalElements, csvData, validationReport, articleStorage, data.source);
await tracer.event(`Response keys: ${Object.keys(result).join(', ')}`);
return result;
}, { validationStatus: validationReport?.status, articleId: articleStorage?.articleId });
return response;
}, { source: data.source || 'node_server', rowNumber: data.rowNumber, hasXMLTemplate: !!data.xmlTemplate });
}
// ============= PRÉPARATION DONNÉES =============
/**
* Préparer les données CSV selon la source - ASYNC pour Node.js
* RÉCUPÈRE: Google Sheets (données CSV) + Digital Ocean (XML template)
*/
async function prepareCSVData(data) {
if (data.csvData && data.csvData.mc0) {
// Données déjà préparées (Digital Ocean ou direct)
return data.csvData;
} else if (data.rowNumber) {
// 1. RÉCUPÉRER DONNÉES CSV depuis Google Sheet (OBLIGATOIRE)
await logSh(`🧠 Récupération données CSV ligne ${data.rowNumber}...`, 'INFO');
const config = await getBrainConfig(data.rowNumber);
if (!config.success) {
await logSh('❌ ÉCHEC: Impossible de récupérer les données Google Sheets', 'ERROR');
throw new Error('FATAL: Google Sheets inaccessible - arrêt du workflow');
}
// 2. VÉRIFIER XML FILENAME depuis Google Sheet (colonne I)
const xmlFileName = config.data.xmlFileName;
if (!xmlFileName || xmlFileName.trim() === '') {
await logSh('❌ ÉCHEC: Nom fichier XML manquant (colonne I Google Sheets)', 'ERROR');
throw new Error('FATAL: XML filename manquant - arrêt du workflow');
}
await logSh(`📋 CSV récupéré: ${config.data.mc0}`, 'INFO');
await logSh(`📄 XML filename: ${xmlFileName}`, 'INFO');
// 3. RÉCUPÉRER XML CONTENT depuis Digital Ocean avec AUTH (OBLIGATOIRE)
await logSh(`🌊 Récupération XML template depuis Digital Ocean (avec signature AWS)...`, 'INFO');
let xmlContent;
try {
xmlContent = await fetchXMLFromDigitalOcean(xmlFileName);
await logSh(`✅ XML récupéré: ${xmlContent.length} caractères`, 'INFO');
} catch (digitalOceanError) {
await logSh(`❌ ÉCHEC: Digital Ocean inaccessible - ${digitalOceanError.message}`, 'ERROR');
throw new Error(`FATAL: Digital Ocean échec - arrêt du workflow: ${digitalOceanError.message}`);
}
// 4. ENCODER XML pour le workflow (comme Make.com)
// Si on a récupéré un fichier XML, l'utiliser. Sinon utiliser le template par défaut déjà dans config.data.xmlTemplate
if (xmlContent) {
data.xmlTemplate = Buffer.from(xmlContent).toString('base64');
await logSh('🔄 XML depuis Digital Ocean encodé base64 pour le workflow', 'DEBUG');
} else if (config.data.xmlTemplate) {
data.xmlTemplate = Buffer.from(config.data.xmlTemplate).toString('base64');
await logSh('🔄 XML template par défaut encodé base64 pour le workflow', 'DEBUG');
}
return config.data;
} else {
throw new Error('FATAL: Données CSV invalides - rowNumber requis');
}
}
/**
* Décoder le XML template - NODE.JS VERSION
*/
function decodeXMLTemplate(xmlTemplate) {
if (!xmlTemplate) {
throw new Error('Template XML manquant');
}
// Si le template commence déjà par <?xml, c'est du texte plain
if (xmlTemplate.startsWith('<?xml') || xmlTemplate.startsWith('<')) {
return xmlTemplate;
}
try {
// 🔄 NODE.JS : Tenter base64 uniquement si ce n'est pas déjà du XML
const decoded = Buffer.from(xmlTemplate, 'base64').toString('utf8');
return decoded;
} catch (error) {
// Si échec, considérer comme texte plain
logSh('🔍 XML pas encodé base64, utilisation directe', 'DEBUG'); // Using logSh instead of console.log
return xmlTemplate;
}
}
/**
* Preprocessing XML (nettoyage) - IDENTIQUE
*/
function preprocessXML(xmlString) {
let processed = xmlString;
// Nettoyer balises <strong>
processed = cleanStrongTags(processed);
// Autres nettoyages futurs...
return processed;
}
// ============= SAUVEGARDE =============
/**
* Sauvegarder l'article avec métadonnées source - ASYNC pour Node.js
*/
async function saveArticle(finalXML, generatedContent, finalElements, csvData, source) {
await logSh('💾 Sauvegarde article...', 'INFO');
const articleData = {
xmlContent: finalXML,
generatedTexts: generatedContent,
elementsGenerated: finalElements.length,
originalElements: finalElements
};
const storageConfig = {
antiDetectionLevel: 'Selective_Enhancement',
llmUsed: 'claude+openai+gemini+mistral',
workflowVersion: '2.0-NodeJS', // 🔄 Mise à jour version
source: source || 'node_server', // 🔄 Source par défaut
enhancementTechniques: [
'technical_terms_gpt4',
'transitions_gemini',
'personality_style_mistral'
]
};
try {
const articleStorage = await saveGeneratedArticleOrganic(articleData, csvData, storageConfig);
await logSh(`✅ Article sauvé: ID ${articleStorage.articleId}`, 'INFO');
return articleStorage;
} catch (storageError) {
await logSh(`⚠️ Erreur sauvegarde: ${storageError.toString()}`, 'WARNING');
return null; // Non-bloquant
}
}
// ============= RÉPONSE =============
/**
* Construire la réponse finale du workflow - ASYNC pour logSh
*/
async function buildWorkflowResponse(finalXML, generatedContent, finalElements, csvData, validationReport, articleStorage, source) {
const response = {
success: true,
source: source,
xmlContent: finalXML,
generatedTexts: generatedContent,
elementsGenerated: finalElements.length,
personality: csvData.personality?.nom || 'Unknown',
csvData: {
mc0: csvData.mc0,
t0: csvData.t0,
personality: csvData.personality?.nom
},
timestamp: new Date().toISOString(),
validationReport: validationReport,
articleStorage: articleStorage,
// NOUVELLES MÉTADONNÉES PHASE 2
antiDetectionLevel: 'Selective_Enhancement',
llmsUsed: ['claude', 'openai', 'gemini', 'mistral'],
enhancementApplied: true,
workflowVersion: '2.0-NodeJS', // 🔄 Version mise à jour
// STATS PERFORMANCE
stats: {
xmlLength: finalXML.length,
contentPieces: Object.keys(generatedContent).length,
wordCount: calculateTotalWordCount(generatedContent),
validationStatus: validationReport.status
}
};
await logSh(`🔍 Response.stats: ${JSON.stringify(response.stats)}`, 'DEBUG');
return response;
}
// ============= HELPERS =============
/**
* Calculer nombre total de mots - IDENTIQUE
*/
function calculateTotalWordCount(generatedContent) {
let totalWords = 0;
Object.values(generatedContent).forEach(content => {
if (content && typeof content === 'string') {
totalWords += content.trim().split(/\s+/).length;
}
});
return totalWords;
}
// ============= POINTS D'ENTRÉE SUPPLÉMENTAIRES =============
/**
* Test du workflow principal - ASYNC pour Node.js
*/
async function testMainWorkflow() {
try {
const testData = {
csvData: {
mc0: 'plaque test nodejs',
t0: 'Test workflow principal Node.js',
personality: { nom: 'Marc', style: 'professionnel' },
tMinus1: 'parent test',
mcPlus1: 'mot1,mot2,mot3,mot4',
tPlus1: 'Titre1,Titre2,Titre3,Titre4'
},
xmlTemplate: Buffer.from('<?xml version="1.0"?><test>|Test_Element{{T0}}|</test>').toString('base64'),
source: 'test_main_nodejs'
};
const result = await handleFullWorkflow(testData);
return result;
} catch (error) {
throw error;
} finally {
tracer.printSummary();
}
}
// 🔄 NODE.JS EXPORTS
module.exports = {
handleFullWorkflow,
testMainWorkflow,
prepareCSVData,
decodeXMLTemplate,
preprocessXML,
saveArticle,
buildWorkflowResponse,
calculateTotalWordCount,
launchLogViewer
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/test-manual.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: test-manual.js - ENTRY POINT MANUEL
// Description: Test workflow ligne 2 Google Sheets
// Usage: node test-manual.js
// ========================================
require('./polyfills/fetch.cjs');
require('dotenv').config();
const { handleFullWorkflow } = require('./Main');
const { logSh } = require('./ErrorReporting');
/**
* TEST MANUEL LIGNE 2
*/
async function testWorkflowLigne2() {
logSh('🚀 === DÉMARRAGE TEST MANUEL LIGNE 2 ===', 'INFO'); // Using logSh instead of console.log
const startTime = Date.now();
try {
// DONNÉES DE TEST POUR LIGNE 2
const testData = {
rowNumber: 2, // Ligne 2 Google Sheets
source: 'test_manual_nodejs'
};
logSh('📊 Configuration test:', 'INFO'); // Using logSh instead of console.log
logSh(` • Ligne: ${testData.rowNumber}`, 'INFO'); // Using logSh instead of console.log
logSh(` • Source: ${testData.source}`, 'INFO'); // Using logSh instead of console.log
logSh(` • Timestamp: ${new Date().toISOString()}`, 'INFO'); // Using logSh instead of console.log
// LANCER LE WORKFLOW
logSh('\n🎯 Lancement workflow principal...', 'INFO'); // Using logSh instead of console.log
const result = await handleFullWorkflow(testData);
// AFFICHER RÉSULTATS
const duration = Date.now() - startTime;
logSh('\n🏆 === WORKFLOW TERMINÉ AVEC SUCCÈS ===', 'INFO'); // Using logSh instead of console.log
logSh(`⏱️ Durée: ${Math.round(duration/1000)}s`, 'INFO'); // Using logSh instead of console.log
logSh(`📊 Status: ${result.success ? '✅ SUCCESS' : '❌ ERROR'}`, 'INFO'); // Using logSh instead of console.log
if (result.success) {
logSh(`📝 Éléments générés: ${result.elementsGenerated}`, 'INFO'); // Using logSh instead of console.log
logSh(`👤 Personnalité: ${result.personality}`, 'INFO'); // Using logSh instead of console.log
logSh(`🎯 MC0: ${result.csvData?.mc0 || 'N/A'}`, 'INFO'); // Using logSh instead of console.log
logSh(`📄 XML length: ${result.stats?.xmlLength || 'N/A'} chars`, 'INFO'); // Using logSh instead of console.log
logSh(`🔤 Mots total: ${result.stats?.wordCount || 'N/A'}`, 'INFO'); // Using logSh instead of console.log
logSh(`🧠 LLMs utilisés: ${result.llmsUsed?.join(', ') || 'N/A'}`, 'INFO'); // Using logSh instead of console.log
if (result.articleStorage) {
logSh(`💾 Article sauvé: ID ${result.articleStorage.articleId}`, 'INFO'); // Using logSh instead of console.log
}
}
logSh('\n📋 Résultat complet:', 'DEBUG'); // Using logSh instead of console.log
logSh(JSON.stringify(result, null, 2), 'DEBUG'); // Using logSh instead of console.log
return result;
} catch (error) {
const duration = Date.now() - startTime;
logSh('\n❌ === ERREUR WORKFLOW ===', 'ERROR'); // Using logSh instead of console.error
logSh(`❌ Message: ${error.message}`, 'ERROR'); // Using logSh instead of console.error
logSh(`❌ Durée avant échec: ${Math.round(duration/1000)}s`, 'ERROR'); // Using logSh instead of console.error
if (process.env.NODE_ENV === 'development') {
logSh(`❌ Stack: ${error.stack}`, 'ERROR'); // Using logSh instead of console.error
}
// Afficher conseils de debug
logSh('\n🔧 CONSEILS DE DEBUG:', 'INFO'); // Using logSh instead of console.log
logSh('1. Vérifiez vos variables d\'environnement (.env)', 'INFO'); // Using logSh instead of console.log
logSh('2. Vérifiez la connexion Google Sheets', 'INFO'); // Using logSh instead of console.log
logSh('3. Vérifiez les API keys LLM', 'INFO'); // Using logSh instead of console.log
logSh('4. Regardez les logs détaillés dans ./logs/', 'INFO'); // Using logSh instead of console.log
process.exit(1);
}
}
/**
* VÉRIFICATIONS PRÉALABLES
*/
function checkEnvironment() {
logSh('🔍 Vérification environnement...', 'INFO'); // Using logSh instead of console.log
const required = [
'GOOGLE_SHEETS_ID',
'OPENAI_API_KEY'
];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
logSh('❌ Variables d\'environnement manquantes:', 'ERROR'); // Using logSh instead of console.error
missing.forEach(key => logSh(`${key}`, 'ERROR')); // Using logSh instead of console.error
logSh('\n💡 Créez un fichier .env avec ces variables', 'ERROR'); // Using logSh instead of console.error
process.exit(1);
}
logSh('✅ Variables d\'environnement OK', 'INFO'); // Using logSh instead of console.log
// Info sur les variables configurées
logSh('📋 Configuration détectée:', 'INFO'); // Using logSh instead of console.log
logSh(` • Google Sheets ID: ${process.env.GOOGLE_SHEETS_ID}`, 'INFO'); // Using logSh instead of console.log
logSh(` • OpenAI: ${process.env.OPENAI_API_KEY ? '✅ Configuré' : '❌ Manquant'}`, 'INFO'); // Using logSh instead of console.log
logSh(` • Claude: ${process.env.CLAUDE_API_KEY ? '✅ Configuré' : '⚠️ Optionnel'}`, 'INFO'); // Using logSh instead of console.log
logSh(` • Gemini: ${process.env.GEMINI_API_KEY ? '✅ Configuré' : '⚠️ Optionnel'}`, 'INFO'); // Using logSh instead of console.log
}
/**
* POINT D'ENTRÉE PRINCIPAL
*/
async function main() {
try {
// Vérifications préalables
checkEnvironment();
// Test workflow
await testWorkflowLigne2();
logSh('\n🎉 Test manuel terminé avec succès !', 'INFO'); // Using logSh instead of console.log
process.exit(0);
} catch (error) {
logSh('\n💥 Erreur fatale: ' + error.message, 'ERROR'); // Using logSh instead of console.error
process.exit(1);
}
}
// Lancer si exécuté directement
if (require.main === module) {
main();
}
module.exports = { testWorkflowLigne2 };
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/DetectorStrategies.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// DETECTOR STRATEGIES - NIVEAU 3
// Responsabilité: Stratégies spécialisées par détecteur IA
// Anti-détection: Techniques ciblées contre chaque analyseur
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* STRATÉGIES DÉTECTEUR PAR DÉTECTEUR
* Chaque classe implémente une approche spécialisée
*/
class BaseDetectorStrategy {
constructor(name) {
this.name = name;
this.effectiveness = 0.8;
this.targetMetrics = [];
}
/**
* Générer instructions spécifiques pour ce détecteur
*/
generateInstructions(elementType, personality, csvData) {
throw new Error('generateInstructions must be implemented by subclass');
}
/**
* Obtenir instructions anti-détection (NOUVEAU pour modularité)
*/
getInstructions(intensity = 1.0) {
throw new Error('getInstructions must be implemented by subclass');
}
/**
* Obtenir conseils d'amélioration (NOUVEAU pour modularité)
*/
getEnhancementTips(intensity = 1.0) {
throw new Error('getEnhancementTips must be implemented by subclass');
}
/**
* Analyser efficacité contre ce détecteur
*/
analyzeEffectiveness(content) {
return {
detector: this.name,
effectiveness: this.effectiveness,
metrics: this.analyzeContent(content)
};
}
/**
* Analyser contenu selon métriques de ce détecteur
*/
analyzeContent(content) {
return {
wordCount: content.split(/\s+/).length,
sentenceCount: content.split(/[.!?]+/).length
};
}
}
/**
* STRATÉGIE ANTI-GPTZERO
* Focus: Imprévisibilité et variation syntaxique
*/
class GPTZeroStrategy extends BaseDetectorStrategy {
constructor() {
super('GPTZero');
this.effectiveness = 0.9;
this.targetMetrics = ['perplexity', 'burstiness', 'unpredictability'];
this.techniques = {
syntaxVariation: {
name: 'Variation syntaxique drastique',
rules: [
'Alterne phrases simples (5-8 mots) et complexes (25-35 mots)',
'Utilise structures atypiques : inversion, anacoluthe, ellipse',
'Intercale incises, parenthèses, tirets pour briser linéarité',
'Varie types : déclarative → interrogative → exclamative'
]
},
lexicalUnpredictability: {
name: 'Imprévisibilité lexicale',
rules: [
'Synonymes rares ou détournés : "performant" → "qui tient la route"',
'Néologismes justifiés : "sur-mesurable" pour "très mesurable"',
'Registres mélangés : soutenu puis familier dans même paragraphe',
'Évite collocations attendues : "haute qualité" → "qualité qui dépote"'
]
},
narrativeDisruption: {
name: 'Disruption narrative',
rules: [
'Apartés personnels : "(j\'ai testé, c\'est bluffant)"',
'Questions rhétoriques inattendues au milieu des faits',
'Changements de perspective soudains : technique → humain → commercial',
'Digressions courtes puis retour au sujet'
]
}
};
}
generateInstructions(elementType, personality, csvData) {
const instructions = [];
instructions.push(`ANTI-GPTZERO - MAXIMUM IMPRÉVISIBILITÉ:`);
// Techniques syntaxiques
instructions.push(`\nSYNTAXE VARIABLE:`);
this.techniques.syntaxVariation.rules.forEach(rule => {
instructions.push(`${rule}`);
});
// Imprévisibilité lexicale
instructions.push(`\nLEXIQUE IMPRÉVISIBLE:`);
this.techniques.lexicalUnpredictability.rules.forEach(rule => {
instructions.push(`${rule}`);
});
// Adaptation selon type élément
if (elementType === 'titre_h1' || elementType === 'titre_h2') {
instructions.push(`\nTITRES ANTI-GPTZERO:`);
instructions.push(`• Évite formules attendues : "Guide complet" → "Le vrai topo sur"`);
instructions.push(`• Structure atypique : question, exclamation, fragment`);
} else if (elementType === 'texte') {
instructions.push(`\nTEXTES ANTI-GPTZERO:`);
this.techniques.narrativeDisruption.rules.forEach(rule => {
instructions.push(`${rule}`);
});
}
// Adaptation personnalité
if (personality) {
instructions.push(`\nAVEC STYLE ${personality.nom.toUpperCase()}:`);
instructions.push(`• Pousse ${personality.style} vers extrêmes : plus marqué, moins lisse`);
instructions.push(`• Accentue tics de langage : répétitions, expressions favorites`);
}
return instructions.join('\n');
}
/**
* Instructions modulaires pour AdversarialCore
*/
getInstructions(intensity = 1.0) {
const baseRules = [
'Surprends avec tournures inattendues et constructions atypiques',
'Évite phrases trop prévisibles ou patterns répétitifs',
'Utilise synonymes moins évidents et vocabulaire moins commun',
'Varie drastiquement la complexité syntaxique entre phrases'
];
const intensiveRules = [
'Intercale observations personnelles ou détours narratifs',
'Alterne registres de langue (soutenu → familier → technique)',
'Brise la logique linéaire avec des parenthèses, incises, apartés',
'Créé ruptures narratives et changements de perspective soudains'
];
return intensity >= 1.0 ? [...baseRules, ...intensiveRules] : baseRules;
}
/**
* Conseils d'amélioration pour enhancement method
*/
getEnhancementTips(intensity = 1.0) {
return [
'Remplace connecteurs prévisibles par variations inattendues',
'Ajoute incises courtes pour briser la linéarité',
'Varie longueurs phrases dans même paragraphe',
'Utilise synonymes moins courants mais naturels',
...(intensity > 0.8 ? [
'Insère questions rhétoriques ponctuelles',
'Ajoute nuances et hésitations authentiques'
] : [])
];
}
analyzeContent(content) {
const baseMetrics = super.analyzeContent(content);
// Analyse perplexité approximative
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 5);
const sentenceLengths = sentences.map(s => s.split(/\s+/).length);
// Variance longueur (proxy pour burstiness)
const avgLength = sentenceLengths.reduce((a, b) => a + b, 0) / sentenceLengths.length;
const variance = sentenceLengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / sentenceLengths.length;
const burstiness = Math.sqrt(variance) / avgLength;
// Diversité lexicale (proxy pour imprévisibilité)
const words = content.toLowerCase().split(/\s+/).filter(w => w.length > 2);
const uniqueWords = [...new Set(words)];
const lexicalDiversity = uniqueWords.length / words.length;
return {
...baseMetrics,
burstiness: Math.round(burstiness * 100) / 100,
lexicalDiversity: Math.round(lexicalDiversity * 100) / 100,
avgSentenceLength: Math.round(avgLength),
gptZeroRiskLevel: this.calculateGPTZeroRisk(burstiness, lexicalDiversity)
};
}
calculateGPTZeroRisk(burstiness, lexicalDiversity) {
// Heuristique : GPTZero détecte uniformité faible + diversité faible
const uniformityScore = Math.min(burstiness, 1) * 100;
const diversityScore = lexicalDiversity * 100;
const combinedScore = (uniformityScore + diversityScore) / 2;
if (combinedScore > 70) return 'low';
if (combinedScore > 40) return 'medium';
return 'high';
}
}
/**
* STRATÉGIE ANTI-ORIGINALITY
* Focus: Diversité sémantique et originalité
*/
class OriginalityStrategy extends BaseDetectorStrategy {
constructor() {
super('Originality');
this.effectiveness = 0.85;
this.targetMetrics = ['semantic_diversity', 'originality_score', 'vocabulary_range'];
this.techniques = {
semanticCreativity: {
name: 'Créativité sémantique',
rules: [
'Métaphores inattendues : "cette plaque, c\'est le passeport de votre façade"',
'Comparaisons originales : évite clichés, invente analogies',
'Reformulations créatives : "résistant aux intempéries" → "qui brave les saisons"',
'Néologismes justifiés et expressifs'
]
},
perspectiveShifting: {
name: 'Changements de perspective',
rules: [
'Angles multiples sur même info : technique → esthétique → pratique',
'Points de vue variés : fabricant, utilisateur, installateur, voisin',
'Temporalités mélangées : présent, futur proche, retour d\'expérience',
'Niveaux d\'abstraction : détail précis puis vue d\'ensemble'
]
},
linguisticInventiveness: {
name: 'Inventivité linguistique',
rules: [
'Jeux de mots subtils et expressions détournées',
'Régionalismes et références culturelles précises',
'Vocabulaire technique humanisé avec créativité',
'Rythmes et sonorités travaillés : allitérations, assonances'
]
}
};
}
generateInstructions(elementType, personality, csvData) {
const instructions = [];
instructions.push(`ANTI-ORIGINALITY - MAXIMUM CRÉATIVITÉ SÉMANTIQUE:`);
// Créativité sémantique
instructions.push(`\nCRÉATIVITÉ SÉMANTIQUE:`);
this.techniques.semanticCreativity.rules.forEach(rule => {
instructions.push(`${rule}`);
});
// Changements de perspective
instructions.push(`\nPERSPECTIVES MULTIPLES:`);
this.techniques.perspectiveShifting.rules.forEach(rule => {
instructions.push(`${rule}`);
});
// Spécialisation par élément
if (elementType === 'intro') {
instructions.push(`\nINTROS ANTI-ORIGINALITY:`);
instructions.push(`• Commence par angle totalement inattendu pour le sujet`);
instructions.push(`• Évite intro-types, réinvente présentation du sujet`);
instructions.push(`• Crée surprise puis retour naturel au cœur du sujet`);
} else if (elementType.includes('faq')) {
instructions.push(`\nFAQ ANTI-ORIGINALITY:`);
instructions.push(`• Questions vraiment originales, pas standard secteur`);
instructions.push(`• Réponses avec angles créatifs et exemples inédits`);
}
// Contexte métier créatif
if (csvData && csvData.mc0) {
instructions.push(`\nCRÉATIVITÉ CONTEXTUELLE ${csvData.mc0.toUpperCase()}:`);
instructions.push(`• Réinvente façon de parler de ${csvData.mc0}`);
instructions.push(`• Évite vocabulaire convenu du secteur, invente expressions`);
instructions.push(`• Trouve analogies originales spécifiques à ${csvData.mc0}`);
}
// Inventivité linguistique
instructions.push(`\nINVENTIVITÉ LINGUISTIQUE:`);
this.techniques.linguisticInventiveness.rules.forEach(rule => {
instructions.push(`${rule}`);
});
return instructions.join('\n');
}
/**
* Instructions modulaires pour AdversarialCore
*/
getInstructions(intensity = 1.0) {
const baseRules = [
'Vocabulaire TRÈS varié : évite répétitions même de synonymes',
'Structures phrases délibérément irrégulières et asymétriques',
'Changements angles fréquents : technique → personnel → général',
'Créativité sémantique : métaphores, comparaisons inattendues'
];
const intensiveRules = [
'Évite formulations académiques ou trop structurées',
'Intègre références culturelles, expressions régionales',
'Subvertis les attentes : commence par la fin, questionne l\'évidence',
'Réinvente façon de présenter informations basiques'
];
return intensity >= 1.0 ? [...baseRules, ...intensiveRules] : baseRules;
}
/**
* Conseils d'amélioration pour enhancement method
*/
getEnhancementTips(intensity = 1.0) {
return [
'Trouve synonymes créatifs et expressions détournées',
'Ajoute métaphores subtiles et comparaisons originales',
'Varie angles d\'approche dans même contenu',
'Utilise vocabulaire technique humanisé',
...(intensity > 0.8 ? [
'Insère références culturelles ou régionalismes',
'Crée néologismes justifiés et expressifs'
] : [])
];
}
analyzeContent(content) {
const baseMetrics = super.analyzeContent(content);
// Analyse diversité sémantique
const words = content.toLowerCase().split(/\s+/).filter(w => w.length > 3);
const uniqueWords = [...new Set(words)];
const semanticDiversity = uniqueWords.length / words.length;
// Détection créativité (heuristique)
const creativityIndicators = [
'comme', 'tel', 'sorte de', 'façon de', 'manière de', // métaphores
'(', ')', '"', // originalité structure
'?', '!', // variation tonale
];
const creativityCount = creativityIndicators.reduce((count, indicator) => {
return count + (content.match(new RegExp(indicator.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
}, 0);
const creativityScore = Math.min(100, (creativityCount / words.length) * 1000);
return {
...baseMetrics,
semanticDiversity: Math.round(semanticDiversity * 100) / 100,
creativityScore: Math.round(creativityScore),
uniqueWordRatio: Math.round((uniqueWords.length / words.length) * 100),
originalityRiskLevel: this.calculateOriginalityRisk(semanticDiversity, creativityScore)
};
}
calculateOriginalityRisk(semanticDiversity, creativityScore) {
const diversityScore = semanticDiversity * 100;
const combinedScore = (diversityScore + creativityScore) / 2;
if (combinedScore > 60) return 'low';
if (combinedScore > 35) return 'medium';
return 'high';
}
}
/**
* STRATÉGIE GÉNÉRALE
* Équilibre entre toutes les techniques
*/
class GeneralStrategy extends BaseDetectorStrategy {
constructor() {
super('General');
this.effectiveness = 0.75;
this.targetMetrics = ['overall_naturalness', 'human_likeness'];
}
generateInstructions(elementType, personality, csvData) {
const instructions = [];
instructions.push(`ANTI-DÉTECTION GÉNÉRALE - ÉQUILIBRE OPTIMAL:`);
// Instructions générales équilibrées
instructions.push(`\nNATUREL ET VARIÉ:`);
instructions.push(`• Évite mots typiques IA : "optimal", "comprehensive", "seamless", "robust"`);
instructions.push(`• Mélange phrases courtes (5-10 mots) et longues (15-25 mots) naturellement`);
instructions.push(`• Utilise expressions idiomatiques françaises authentiques`);
instructions.push(`• Intègre imperfections humaines : répétitions légères, hésitations`);
instructions.push(`• Change angle ou ton 1-2 fois selon longueur du contenu`);
// Adaptation personnalité équilibrée
if (personality) {
instructions.push(`\nSTYLE ${personality.nom} NATUREL:`);
instructions.push(`• Applique style ${personality.style} de façon authentique, pas forcée`);
instructions.push(`• Respecte vocabulaire naturel : ${personality.vocabulairePref || 'varié'}`);
instructions.push(`• Utilise connecteurs variés : ${personality.connecteursPref || 'naturels'}`);
}
return instructions.join('\n');
}
/**
* Instructions modulaires pour AdversarialCore
*/
getInstructions(intensity = 1.0) {
const baseRules = [
'Évite ces mots typiques IA : "optimal", "comprehensive", "seamless", "robust", "leverage"',
'Varie longueurs phrases : mélange phrases courtes (5-10 mots) ET longues (20-30 mots)',
'Utilise expressions françaises familières et tournures idiomatiques',
'Ajoute imperfections naturelles : répétitions légères, hésitations, reformulations'
];
const intensiveRules = [
'Change angle ou perspective 1-2 fois dans le texte',
'Préfère synonymes moins évidents : "bon" plutôt qu\'excellent", "solide" plutôt qu\'optimisé"',
'Intègre connecteurs variés et naturels selon le contexte',
'Simule variation naturelle d\'humeur et d\'énergie rédactionnelle'
];
return intensity >= 0.8 ? [...baseRules, ...intensiveRules] : baseRules;
}
/**
* Conseils d'amélioration pour enhancement method
*/
getEnhancementTips(intensity = 1.0) {
return [
'Remplace mots typiques IA par synonymes plus naturels',
'Ajoute nuances et hésitations : "peut-être", "généralement", "souvent"',
'Varie connecteurs pour éviter répétitions mécaniques',
'Personnalise avec observations subjectives légères',
...(intensity > 0.7 ? [
'Intègre "erreurs" humaines : corrections, précisions',
'Simule changement léger de ton ou d\'énergie'
] : [])
];
}
analyzeContent(content) {
const baseMetrics = super.analyzeContent(content);
// Métrique naturalité générale
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 5);
const avgWordsPerSentence = baseMetrics.wordCount / baseMetrics.sentenceCount;
// Détection mots typiques IA
const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage'];
const aiWordCount = aiWords.reduce((count, word) => {
return count + (content.toLowerCase().match(new RegExp(`\\b${word}\\b`, 'g')) || []).length;
}, 0);
const aiWordDensity = aiWordCount / baseMetrics.wordCount * 100;
const naturalness = Math.max(0, 100 - (aiWordDensity * 10) - Math.abs(avgWordsPerSentence - 15));
return {
...baseMetrics,
avgWordsPerSentence: Math.round(avgWordsPerSentence),
aiWordCount,
aiWordDensity: Math.round(aiWordDensity * 100) / 100,
naturalnessScore: Math.round(naturalness),
generalRiskLevel: naturalness > 70 ? 'low' : naturalness > 40 ? 'medium' : 'high'
};
}
}
/**
* FACTORY POUR CRÉER STRATÉGIES
*/
class DetectorStrategyFactory {
static strategies = {
'general': GeneralStrategy,
'gptZero': GPTZeroStrategy,
'originality': OriginalityStrategy
};
static createStrategy(detectorName) {
const StrategyClass = this.strategies[detectorName];
if (!StrategyClass) {
logSh(`⚠️ Stratégie inconnue: ${detectorName}, fallback vers général`, 'WARNING');
return new GeneralStrategy();
}
return new StrategyClass();
}
static getSupportedDetectors() {
return Object.keys(this.strategies).map(name => {
const strategy = this.createStrategy(name);
return {
name,
displayName: strategy.name,
effectiveness: strategy.effectiveness,
targetMetrics: strategy.targetMetrics
};
});
}
static analyzeContentAgainstAllDetectors(content) {
const results = {};
Object.keys(this.strategies).forEach(detectorName => {
const strategy = this.createStrategy(detectorName);
results[detectorName] = strategy.analyzeEffectiveness(content);
});
return results;
}
}
/**
* FONCTION UTILITAIRE - SÉLECTION STRATÉGIE OPTIMALE
*/
function selectOptimalStrategy(elementType, personality, previousResults = {}) {
// Logique de sélection intelligente
// Si résultats précédents disponibles, adapter
if (previousResults.gptZero && previousResults.gptZero.effectiveness < 0.6) {
return 'gptZero'; // Renforcer anti-GPTZero
}
if (previousResults.originality && previousResults.originality.effectiveness < 0.6) {
return 'originality'; // Renforcer anti-Originality
}
// Sélection par type d'élément
if (elementType === 'titre_h1' || elementType === 'titre_h2') {
return 'gptZero'; // Titres bénéficient imprévisibilité
}
if (elementType === 'intro' || elementType === 'texte') {
return 'originality'; // Corps bénéficie créativité sémantique
}
if (elementType.includes('faq')) {
return 'general'; // FAQ équilibre naturalité
}
// Par personnalité
if (personality) {
if (personality.style === 'créatif' || personality.style === 'original') {
return 'originality';
}
if (personality.style === 'technique' || personality.style === 'expert') {
return 'gptZero';
}
}
return 'general'; // Fallback
}
module.exports = {
DetectorStrategyFactory,
GPTZeroStrategy,
OriginalityStrategy,
GeneralStrategy,
selectOptimalStrategy,
BaseDetectorStrategy
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialCore.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ADVERSARIAL CORE - MOTEUR MODULAIRE
// Responsabilité: Moteur adversarial réutilisable sur tout contenu
// Architecture: Couches applicables à la demande
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { callLLM } = require('../LLMManager');
// Import stratégies et utilitaires
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - APPLICATION COUCHE ADVERSARIALE
* Input: contenu existant + configuration adversariale
* Output: contenu avec couche adversariale appliquée
*/
async function applyAdversarialLayer(existingContent, config = {}) {
return await tracer.run('AdversarialCore.applyAdversarialLayer()', async () => {
const {
detectorTarget = 'general',
intensity = 1.0,
method = 'regeneration', // 'regeneration' | 'enhancement' | 'hybrid'
preserveStructure = true,
csvData = null,
context = {}
} = config;
await tracer.annotate({
adversarialLayer: true,
detectorTarget,
intensity,
method,
elementsCount: Object.keys(existingContent).length
});
const startTime = Date.now();
logSh(`🎯 APPLICATION COUCHE ADVERSARIALE: ${detectorTarget} (${method})`, 'INFO');
logSh(` 📊 ${Object.keys(existingContent).length} éléments | Intensité: ${intensity}`, 'INFO');
try {
// Initialiser stratégie détecteur
const detectorManager = new DetectorStrategyManager(detectorTarget);
const strategy = detectorManager.getStrategy();
// Appliquer méthode adversariale choisie
let adversarialContent = {};
switch (method) {
case 'regeneration':
adversarialContent = await applyRegenerationMethod(existingContent, config, strategy);
break;
case 'enhancement':
adversarialContent = await applyEnhancementMethod(existingContent, config, strategy);
break;
case 'hybrid':
adversarialContent = await applyHybridMethod(existingContent, config, strategy);
break;
default:
throw new Error(`Méthode adversariale inconnue: ${method}`);
}
const duration = Date.now() - startTime;
const stats = {
elementsProcessed: Object.keys(existingContent).length,
elementsModified: countModifiedElements(existingContent, adversarialContent),
detectorTarget,
intensity,
method,
duration
};
logSh(`✅ COUCHE ADVERSARIALE APPLIQUÉE: ${stats.elementsModified}/${stats.elementsProcessed} modifiés (${duration}ms)`, 'INFO');
await tracer.event('Couche adversariale appliquée', stats);
return {
content: adversarialContent,
stats,
original: existingContent,
config
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ COUCHE ADVERSARIALE ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content: existingContent,
stats: { fallback: true, duration },
original: existingContent,
config,
error: error.message
};
}
}, { existingContent: Object.keys(existingContent), config });
}
/**
* MÉTHODE RÉGÉNÉRATION - Réécrire complètement avec prompts adversariaux
*/
async function applyRegenerationMethod(existingContent, config, strategy) {
logSh(`🔄 Méthode régénération adversariale`, 'DEBUG');
const results = {};
const contentEntries = Object.entries(existingContent);
// Traiter en chunks pour éviter timeouts
const chunks = chunkArray(contentEntries, 4);
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
logSh(` 📦 Régénération chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
try {
const regenerationPrompt = createRegenerationPrompt(chunk, config, strategy);
const response = await callLLM('claude', regenerationPrompt, {
temperature: 0.7 + (config.intensity * 0.2), // Température variable selon intensité
maxTokens: 2000 * chunk.length
}, config.csvData?.personality);
const chunkResults = parseRegenerationResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} éléments régénérés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: garder contenu original pour ce chunk
chunk.forEach(([tag, content]) => {
results[tag] = content;
});
}
}
return results;
}
/**
* MÉTHODE ENHANCEMENT - Améliorer sans réécrire complètement
*/
async function applyEnhancementMethod(existingContent, config, strategy) {
logSh(`🔧 Méthode enhancement adversarial`, 'DEBUG');
const results = { ...existingContent }; // Base: contenu original
const elementsToEnhance = selectElementsForEnhancement(existingContent, config);
if (elementsToEnhance.length === 0) {
logSh(` ⏭️ Aucun élément nécessite enhancement`, 'DEBUG');
return results;
}
logSh(` 📋 ${elementsToEnhance.length} éléments sélectionnés pour enhancement`, 'DEBUG');
const enhancementPrompt = createEnhancementPrompt(elementsToEnhance, config, strategy);
try {
const response = await callLLM('gpt4', enhancementPrompt, {
temperature: 0.5 + (config.intensity * 0.3),
maxTokens: 3000
}, config.csvData?.personality);
const enhancedResults = parseEnhancementResponse(response, elementsToEnhance);
// Appliquer améliorations
Object.keys(enhancedResults).forEach(tag => {
if (enhancedResults[tag] !== existingContent[tag]) {
results[tag] = enhancedResults[tag];
}
});
return results;
} catch (error) {
logSh(`❌ Enhancement échoué: ${error.message}`, 'ERROR');
return results; // Fallback: contenu original
}
}
/**
* MÉTHODE HYBRIDE - Combinaison régénération + enhancement
*/
async function applyHybridMethod(existingContent, config, strategy) {
logSh(`⚡ Méthode hybride adversariale`, 'DEBUG');
// 1. Enhancement léger sur tout le contenu
const enhancedContent = await applyEnhancementMethod(existingContent, {
...config,
intensity: config.intensity * 0.6 // Intensité réduite pour enhancement
}, strategy);
// 2. Régénération ciblée sur éléments clés
const keyElements = selectKeyElementsForRegeneration(enhancedContent, config);
if (keyElements.length === 0) {
return enhancedContent;
}
const keyElementsContent = {};
keyElements.forEach(tag => {
keyElementsContent[tag] = enhancedContent[tag];
});
const regeneratedElements = await applyRegenerationMethod(keyElementsContent, {
...config,
intensity: config.intensity * 1.2 // Intensité augmentée pour régénération
}, strategy);
// 3. Merger résultats
const hybridContent = { ...enhancedContent };
Object.keys(regeneratedElements).forEach(tag => {
hybridContent[tag] = regeneratedElements[tag];
});
return hybridContent;
}
// ============= HELPER FUNCTIONS =============
/**
* Créer prompt de régénération adversariale
*/
function createRegenerationPrompt(chunk, config, strategy) {
const { detectorTarget, intensity, csvData } = config;
let prompt = `MISSION: Réécris ces contenus pour éviter détection par ${detectorTarget}.
TECHNIQUE ANTI-${detectorTarget.toUpperCase()}:
${strategy.getInstructions(intensity).join('\n')}
CONTENUS À RÉÉCRIRE:
${chunk.map(([tag, content], i) => `[${i + 1}] TAG: ${tag}
ORIGINAL: "${content}"`).join('\n\n')}
CONSIGNES:
- GARDE exactement le même message et informations factuelles
- CHANGE structure, vocabulaire, style pour éviter détection ${detectorTarget}
- Intensité adversariale: ${intensity.toFixed(2)}
${csvData?.personality ? `- Style: ${csvData.personality.nom} (${csvData.personality.style})` : ''}
IMPORTANT: Réponse DIRECTE par les contenus réécrits, pas d'explication.
FORMAT:
[1] Contenu réécrit anti-${detectorTarget}
[2] Contenu réécrit anti-${detectorTarget}
etc...`;
return prompt;
}
/**
* Créer prompt d'enhancement adversarial
*/
function createEnhancementPrompt(elementsToEnhance, config, strategy) {
const { detectorTarget, intensity } = config;
let prompt = `MISSION: Améliore subtilement ces contenus pour réduire détection ${detectorTarget}.
AMÉLIORATIONS CIBLÉES:
${strategy.getEnhancementTips(intensity).join('\n')}
ÉLÉMENTS À AMÉLIORER:
${elementsToEnhance.map((element, i) => `[${i + 1}] TAG: ${element.tag}
CONTENU: "${element.content}"
PROBLÈME: ${element.detectionRisk}`).join('\n\n')}
CONSIGNES:
- Modifications LÉGÈRES et naturelles
- GARDE le fond du message intact
- Focus sur réduction détection ${detectorTarget}
- Intensité: ${intensity.toFixed(2)}
FORMAT:
[1] Contenu légèrement amélioré
[2] Contenu légèrement amélioré
etc...`;
return prompt;
}
/**
* Parser réponse régénération
*/
function parseRegenerationResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const index = parseInt(match[1]) - 1;
const content = cleanAdversarialContent(match[2].trim());
if (index >= 0 && index < chunk.length) {
parsedItems[index] = content;
}
}
// Mapper aux vrais tags
chunk.forEach(([tag, originalContent], index) => {
if (parsedItems[index] && parsedItems[index].length > 10) {
results[tag] = parsedItems[index];
} else {
results[tag] = originalContent; // Fallback
logSh(`⚠️ Fallback régénération pour [${tag}]`, 'WARNING');
}
});
return results;
}
/**
* Parser réponse enhancement
*/
function parseEnhancementResponse(response, elementsToEnhance) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < elementsToEnhance.length) {
let enhancedContent = cleanAdversarialContent(match[2].trim());
const element = elementsToEnhance[index];
if (enhancedContent && enhancedContent.length > 10) {
results[element.tag] = enhancedContent;
} else {
results[element.tag] = element.content; // Fallback
}
index++;
}
return results;
}
/**
* Sélectionner éléments pour enhancement
*/
function selectElementsForEnhancement(existingContent, config) {
const elements = [];
Object.entries(existingContent).forEach(([tag, content]) => {
const detectionRisk = assessDetectionRisk(content, config.detectorTarget);
if (detectionRisk.score > 0.6) { // Risque élevé
elements.push({
tag,
content,
detectionRisk: detectionRisk.reasons.join(', '),
priority: detectionRisk.score
});
}
});
// Trier par priorité (risque élevé en premier)
elements.sort((a, b) => b.priority - a.priority);
return elements;
}
/**
* Sélectionner éléments clés pour régénération (hybride)
*/
function selectKeyElementsForRegeneration(content, config) {
const keyTags = [];
Object.keys(content).forEach(tag => {
// Éléments clés: titres, intro, premiers paragraphes
if (tag.includes('Titre') || tag.includes('H1') || tag.includes('intro') ||
tag.includes('Introduction') || tag.includes('1')) {
keyTags.push(tag);
}
});
return keyTags.slice(0, 3); // Maximum 3 éléments clés
}
/**
* Évaluer risque de détection
*/
function assessDetectionRisk(content, detectorTarget) {
let score = 0;
const reasons = [];
// Indicateurs génériques de contenu IA
const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge'];
const aiCount = aiWords.reduce((count, word) => {
return count + (content.toLowerCase().includes(word) ? 1 : 0);
}, 0);
if (aiCount > 2) {
score += 0.4;
reasons.push('mots_typiques_ia');
}
// Structure trop parfaite
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length > 2) {
const avgLength = sentences.reduce((sum, s) => sum + s.length, 0) / sentences.length;
const variance = sentences.reduce((sum, s) => sum + Math.pow(s.length - avgLength, 2), 0) / sentences.length;
const uniformity = 1 - (Math.sqrt(variance) / avgLength);
if (uniformity > 0.8) {
score += 0.3;
reasons.push('structure_uniforme');
}
}
// Spécifique selon détecteur
if (detectorTarget === 'gptZero') {
// GPTZero détecte la prévisibilité
if (content.includes('par ailleurs') && content.includes('en effet')) {
score += 0.3;
reasons.push('connecteurs_prévisibles');
}
}
return { score: Math.min(1, score), reasons };
}
/**
* Nettoyer contenu adversarial généré
*/
function cleanAdversarialContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(voici\s+)?le\s+contenu\s+(réécrit|amélioré)[:\s]*/gi, '');
content = content.replace(/^(bon,?\s*)?(alors,?\s*)?/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
/**
* Compter éléments modifiés
*/
function countModifiedElements(original, modified) {
let count = 0;
Object.keys(original).forEach(tag => {
if (modified[tag] && modified[tag] !== original[tag]) {
count++;
}
});
return count;
}
/**
* Chunk array utility
*/
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
/**
* Sleep utility
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
applyAdversarialLayer, // ← MAIN ENTRY POINT MODULAIRE
applyRegenerationMethod,
applyEnhancementMethod,
applyHybridMethod,
assessDetectionRisk,
selectElementsForEnhancement
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialPromptEngine.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ADVERSARIAL PROMPT ENGINE - NIVEAU 3
// Responsabilité: Générer instructions anti-détection pour prompts
// Anti-détection: Prévention par modification des prompts LLM
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* INSTRUCTIONS ADVERSARIALES PAR DÉTECTEUR
* Stratégies spécialisées pour contrer chaque détecteur IA
*/
const ADVERSARIAL_INSTRUCTIONS = {
general: {
title: 'CONTRAINTES ANTI-DÉTECTION GÉNÉRALES',
rules: [
'Évite ces mots typiques IA : "optimal", "comprehensive", "seamless", "robust", "leverage"',
'Varie longueurs phrases : mélange phrases courtes (5-10 mots) ET longues (20-30 mots)',
'Utilise expressions françaises familières et tournures idiomatiques',
'Ajoute imperfections naturelles : répétitions légères, hésitations, reformulations',
'Change angle ou perspective 1-2 fois dans le texte',
'Préfère synonymes moins évidents : "bon" plutôt qu\'"excellent", "solide" plutôt qu\'optimisé"',
'Intègre connecteurs variés et naturels selon le contexte'
],
intensity: 1.0,
weight: 0.8
},
gptZero: {
title: 'SPÉCIAL ANTI-GPTZERO',
rules: [
'Surprends avec tournures inattendues et constructions atypiques',
'Évite phrases trop prévisibles ou patterns répétitifs',
'Utilise synonymes moins évidents et vocabulaire moins commun',
'Varie drastiquement la complexité syntaxique entre phrases',
'Intercale observations personnelles ou détours narratifs',
'Alterne registres de langue (soutenu → familier → technique)',
'Brise la logique linéaire avec des parenthèses, incises, apartés'
],
intensity: 1.2,
weight: 0.9,
targetMetric: 'unpredictability'
},
originality: {
title: 'SPÉCIAL ANTI-ORIGINALITY',
rules: [
'Vocabulaire TRÈS varié : évite répétitions même de synonymes',
'Structures phrases délibérément irrégulières et asymétriques',
'Changements angles fréquents : technique → personnel → général',
'Créativité sémantique : métaphores, comparaisons inattendues',
'Évite formulations académiques ou trop structurées',
'Intègre références culturelles, expressions régionales',
'Subvertis les attentes : commence par la fin, questionne l\'évidence'
],
intensity: 1.3,
weight: 0.95,
targetMetric: 'semantic_diversity'
},
copyLeaks: {
title: 'SPÉCIAL ANTI-COPYLEAKS',
rules: [
'Reformule idées communes avec angles totalement originaux',
'Évite phrases-types et formulations standard du secteur',
'Personnalise chaque assertion avec exemples spécifiques',
'Réinvente la façon de présenter informations basiques',
'Utilise analogies et métaphores plutôt que descriptions directes',
'Fragmente informations techniques en observations pratiques',
'Transforme données factuelles en récits ou témoignages'
],
intensity: 1.1,
weight: 0.85,
targetMetric: 'originality_score'
},
winston: {
title: 'SPÉCIAL ANTI-WINSTON',
rules: [
'Évite cohérence stylistique trop parfaite entre paragraphes',
'Simule variation naturelle d\'humeur et d\'énergie rédactionnelle',
'Intègre "erreurs" humaines : répétitions, corrections, précisions',
'Varie niveau de détail : parfois précis, parfois elliptique',
'Alterne registres émotionnels : enthousiaste → neutre → critique',
'Inclus hésitations et nuances : "peut-être", "généralement", "souvent"',
'Personnalise avec opinions subjectives et préférences'
],
intensity: 1.0,
weight: 0.9,
targetMetric: 'human_variation'
}
};
/**
* INSTRUCTIONS PERSONNALISÉES PAR TYPE D'ÉLÉMENT
*/
const ELEMENT_SPECIFIC_INSTRUCTIONS = {
titre_h1: {
base: 'Crée un titre percutant mais naturel',
adversarial: 'Évite formules marketing lisses, préfère authentique et direct'
},
titre_h2: {
base: 'Génère un sous-titre informatif',
adversarial: 'Varie structure : question, affirmation, exclamation selon contexte'
},
intro: {
base: 'Rédige introduction engageante',
adversarial: 'Commence par angle inattendu : anecdote, constat, question rhétorique'
},
texte: {
base: 'Développe paragraphe informatif',
adversarial: 'Mélange informations factuelles et observations personnelles'
},
faq_question: {
base: 'Formule question client naturelle',
adversarial: 'Utilise formulations vraiment utilisées par clients, pas académiques'
},
faq_reponse: {
base: 'Réponds de façon experte et rassurante',
adversarial: 'Ajoute nuances, "ça dépend", précisions contextuelles comme humain'
}
};
/**
* MAIN ENTRY POINT - GÉNÉRATEUR DE PROMPTS ADVERSARIAUX
* @param {string} basePrompt - Prompt de base
* @param {Object} config - Configuration adversariale
* @returns {string} - Prompt enrichi d'instructions anti-détection
*/
function createAdversarialPrompt(basePrompt, config = {}) {
return tracer.run('AdversarialPromptEngine.createAdversarialPrompt()', () => {
const {
detectorTarget = 'general',
intensity = 1.0,
elementType = 'generic',
personality = null,
contextualMode = true,
csvData = null,
debugMode = false
} = config;
tracer.annotate({
detectorTarget,
intensity,
elementType,
personalityStyle: personality?.style
});
try {
// 1. Sélectionner stratégie détecteur
const strategy = ADVERSARIAL_INSTRUCTIONS[detectorTarget] || ADVERSARIAL_INSTRUCTIONS.general;
// 2. Adapter intensité
const effectiveIntensity = intensity * (strategy.intensity || 1.0);
const shouldApplyStrategy = Math.random() < (strategy.weight || 0.8);
if (!shouldApplyStrategy && detectorTarget !== 'general') {
// Fallback sur stratégie générale
return createAdversarialPrompt(basePrompt, { ...config, detectorTarget: 'general' });
}
// 3. Construire instructions adversariales
const adversarialSection = buildAdversarialInstructions(strategy, {
elementType,
personality,
effectiveIntensity,
contextualMode,
csvData
});
// 4. Assembler prompt final
const enhancedPrompt = assembleEnhancedPrompt(basePrompt, adversarialSection, {
strategy,
elementType,
debugMode
});
if (debugMode) {
logSh(`🎯 Prompt adversarial généré: ${detectorTarget} (intensité: ${effectiveIntensity.toFixed(2)})`, 'DEBUG');
logSh(` Instructions: ${strategy.rules.length} règles appliquées`, 'DEBUG');
}
tracer.event('Prompt adversarial créé', {
detectorTarget,
rulesCount: strategy.rules.length,
promptLength: enhancedPrompt.length
});
return enhancedPrompt;
} catch (error) {
logSh(`❌ Erreur génération prompt adversarial: ${error.message}`, 'ERROR');
// Fallback: retourner prompt original
return basePrompt;
}
}, config);
}
/**
* Construire section instructions adversariales
*/
function buildAdversarialInstructions(strategy, config) {
const { elementType, personality, effectiveIntensity, contextualMode, csvData } = config;
let instructions = `\n\n=== ${strategy.title} ===\n`;
// Règles de base de la stratégie
const activeRules = selectActiveRules(strategy.rules, effectiveIntensity);
activeRules.forEach(rule => {
instructions += `${rule}\n`;
});
// Instructions spécifiques au type d'élément
if (ELEMENT_SPECIFIC_INSTRUCTIONS[elementType]) {
const elementInstructions = ELEMENT_SPECIFIC_INSTRUCTIONS[elementType];
instructions += `\nSPÉCIFIQUE ${elementType.toUpperCase()}:\n`;
instructions += `${elementInstructions.adversarial}\n`;
}
// Adaptations personnalité
if (personality && contextualMode) {
const personalityAdaptations = generatePersonalityAdaptations(personality, strategy);
if (personalityAdaptations) {
instructions += `\nADAPTATION PERSONNALITÉ ${personality.nom.toUpperCase()}:\n`;
instructions += personalityAdaptations;
}
}
// Contexte métier si disponible
if (csvData && contextualMode) {
const contextualInstructions = generateContextualInstructions(csvData, strategy);
if (contextualInstructions) {
instructions += `\nCONTEXTE MÉTIER:\n`;
instructions += contextualInstructions;
}
}
instructions += `\nIMPORTANT: Ces contraintes doivent sembler naturelles, pas forcées.\n`;
return instructions;
}
/**
* Sélectionner règles actives selon intensité
*/
function selectActiveRules(allRules, intensity) {
if (intensity >= 1.0) {
return allRules; // Toutes les règles
}
// Sélection proportionnelle à l'intensité
const ruleCount = Math.ceil(allRules.length * intensity);
return allRules.slice(0, ruleCount);
}
/**
* Générer adaptations personnalité
*/
function generatePersonalityAdaptations(personality, strategy) {
if (!personality) return null;
const adaptations = [];
// Style de la personnalité
if (personality.style) {
adaptations.push(`• Respecte le style ${personality.style} de ${personality.nom} tout en appliquant les contraintes`);
}
// Vocabulaire préféré
if (personality.vocabulairePref) {
adaptations.push(`• Intègre vocabulaire naturel: ${personality.vocabulairePref}`);
}
// Connecteurs préférés
if (personality.connecteursPref) {
adaptations.push(`• Utilise connecteurs variés: ${personality.connecteursPref}`);
}
// Longueur phrases selon personnalité
if (personality.longueurPhrases) {
adaptations.push(`• Longueur phrases: ${personality.longueurPhrases} mais avec variation anti-détection`);
}
return adaptations.length > 0 ? adaptations.join('\n') + '\n' : null;
}
/**
* Générer instructions contextuelles métier
*/
function generateContextualInstructions(csvData, strategy) {
if (!csvData.mc0) return null;
const instructions = [];
// Contexte sujet
instructions.push(`• Sujet: ${csvData.mc0} - utilise terminologie naturelle du domaine`);
// Éviter jargon selon détecteur
if (strategy.targetMetric === 'unpredictability') {
instructions.push(`• Évite jargon technique trop prévisible, privilégie explications accessibles`);
} else if (strategy.targetMetric === 'semantic_diversity') {
instructions.push(`• Varie façons de nommer/décrire ${csvData.mc0} - synonymes créatifs`);
}
return instructions.join('\n') + '\n';
}
/**
* Assembler prompt final
*/
function assembleEnhancedPrompt(basePrompt, adversarialSection, config) {
const { strategy, elementType, debugMode } = config;
// Structure du prompt amélioré
let enhancedPrompt = basePrompt;
// Injecter instructions adversariales
enhancedPrompt += adversarialSection;
// Rappel final selon stratégie
if (strategy.targetMetric) {
enhancedPrompt += `\nOBJECTIF PRIORITAIRE: Maximiser ${strategy.targetMetric} tout en conservant qualité.\n`;
}
// Instructions de réponse
enhancedPrompt += `\nRÉPONDS DIRECTEMENT par le contenu demandé, en appliquant naturellement ces contraintes.`;
return enhancedPrompt;
}
/**
* Analyser efficacité d'un prompt adversarial
*/
function analyzePromptEffectiveness(originalPrompt, adversarialPrompt, generatedContent) {
const analysis = {
promptEnhancement: {
originalLength: originalPrompt.length,
adversarialLength: adversarialPrompt.length,
enhancementRatio: adversarialPrompt.length / originalPrompt.length,
instructionsAdded: (adversarialPrompt.match(/•/g) || []).length
},
contentMetrics: analyzeGeneratedContent(generatedContent),
effectiveness: 0
};
// Score d'efficacité simple
analysis.effectiveness = Math.min(100,
(analysis.promptEnhancement.enhancementRatio - 1) * 50 +
analysis.contentMetrics.diversityScore
);
return analysis;
}
/**
* Analyser contenu généré
*/
function analyzeGeneratedContent(content) {
if (!content || typeof content !== 'string') {
return { diversityScore: 0, wordCount: 0, sentenceVariation: 0 };
}
const words = content.split(/\s+/).filter(w => w.length > 2);
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 5);
// Diversité vocabulaire
const uniqueWords = [...new Set(words.map(w => w.toLowerCase()))];
const diversityScore = uniqueWords.length / Math.max(1, words.length) * 100;
// Variation longueurs phrases
const sentenceLengths = sentences.map(s => s.split(/\s+/).length);
const avgLength = sentenceLengths.reduce((a, b) => a + b, 0) / Math.max(1, sentenceLengths.length);
const variance = sentenceLengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / Math.max(1, sentenceLengths.length);
const sentenceVariation = Math.sqrt(variance) / Math.max(1, avgLength) * 100;
return {
diversityScore: Math.round(diversityScore),
wordCount: words.length,
sentenceCount: sentences.length,
sentenceVariation: Math.round(sentenceVariation),
avgSentenceLength: Math.round(avgLength)
};
}
/**
* Obtenir liste des détecteurs supportés
*/
function getSupportedDetectors() {
return Object.keys(ADVERSARIAL_INSTRUCTIONS).map(key => ({
id: key,
name: ADVERSARIAL_INSTRUCTIONS[key].title,
intensity: ADVERSARIAL_INSTRUCTIONS[key].intensity,
weight: ADVERSARIAL_INSTRUCTIONS[key].weight,
rulesCount: ADVERSARIAL_INSTRUCTIONS[key].rules.length,
targetMetric: ADVERSARIAL_INSTRUCTIONS[key].targetMetric || 'general'
}));
}
module.exports = {
createAdversarialPrompt, // ← MAIN ENTRY POINT
buildAdversarialInstructions,
analyzePromptEffectiveness,
analyzeGeneratedContent,
getSupportedDetectors,
ADVERSARIAL_INSTRUCTIONS,
ELEMENT_SPECIFIC_INSTRUCTIONS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialInitialGeneration.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ÉTAPE 1: GÉNÉRATION INITIALE ADVERSARIALE
// Responsabilité: Créer le contenu de base avec Claude + anti-détection
// LLM: Claude Sonnet (température 0.7) + Prompts adversariaux
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { createAdversarialPrompt } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - GÉNÉRATION INITIALE ADVERSARIALE
* Input: { content: {}, csvData: {}, context: {}, adversarialConfig: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function generateInitialContentAdversarial(input) {
return await tracer.run('AdversarialInitialGeneration.generateInitialContentAdversarial()', async () => {
const { hierarchy, csvData, context = {}, adversarialConfig = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true,
contextualMode: adversarialConfig.contextualMode !== false,
...adversarialConfig
};
// Initialiser manager détecteur
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
await tracer.annotate({
step: '1/4',
llmProvider: 'claude',
elementsCount: Object.keys(hierarchy).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 ÉTAPE 1/4 ADVERSARIAL: Génération initiale (Claude + ${config.detectorTarget})`, 'INFO');
logSh(` 📊 ${Object.keys(hierarchy).length} éléments à générer`, 'INFO');
try {
// Collecter tous les éléments dans l'ordre XML
const allElements = collectElementsInXMLOrder(hierarchy);
// Séparer FAQ pairs et autres éléments
const { faqPairs, otherElements } = separateElementTypes(allElements);
// Générer en chunks pour éviter timeouts
const results = {};
// 1. Générer éléments normaux avec prompts adversariaux
if (otherElements.length > 0) {
const normalResults = await generateNormalElementsAdversarial(otherElements, csvData, config, detectorManager);
Object.assign(results, normalResults);
}
// 2. Générer paires FAQ adversariales si présentes
if (faqPairs.length > 0) {
const faqResults = await generateFAQPairsAdversarial(faqPairs, csvData, config, detectorManager);
Object.assign(results, faqResults);
}
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(results).length,
generated: Object.keys(results).length,
faqPairs: faqPairs.length,
duration
};
logSh(`✅ ÉTAPE 1/4 TERMINÉE: ${stats.generated} éléments générés (${duration}ms)`, 'INFO');
await tracer.event(`Génération initiale terminée`, stats);
return {
content: results,
stats,
debug: {
llmProvider: 'claude',
step: 1,
elementsGenerated: Object.keys(results),
adversarialConfig: config,
detectorTarget: config.detectorTarget,
intensity: config.intensity
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 1/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`InitialGeneration failed: ${error.message}`);
}
}, input);
}
/**
* Générer éléments normaux avec prompts adversariaux en chunks
*/
async function generateNormalElementsAdversarial(elements, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Génération éléments normaux adversariaux: ${elements.length} éléments`, 'DEBUG');
const results = {};
const chunks = chunkArray(elements, 4); // Chunks de 4 pour éviter timeouts
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
try {
const basePrompt = createBatchPrompt(chunk, csvData);
// Générer prompt adversarial
const adversarialPrompt = createAdversarialPrompt(basePrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity,
elementType: getElementTypeFromChunk(chunk),
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const response = await callLLM('claude', adversarialPrompt, {
temperature: 0.7,
maxTokens: 2000 * chunk.length
}, csvData.personality);
const chunkResults = parseBatchResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} éléments générés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
throw error;
}
}
return results;
}
/**
* Générer paires FAQ adversariales cohérentes
*/
async function generateFAQPairsAdversarial(faqPairs, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Génération paires FAQ adversariales: ${faqPairs.length} paires`, 'DEBUG');
const basePrompt = createFAQPairsPrompt(faqPairs, csvData);
// Générer prompt adversarial spécialisé FAQ
const adversarialPrompt = createAdversarialPrompt(basePrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity * 1.1, // Intensité légèrement plus élevée pour FAQ
elementType: 'faq_mixed',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const response = await callLLM('claude', adversarialPrompt, {
temperature: 0.8,
maxTokens: 3000
}, csvData.personality);
return parseFAQResponse(response, faqPairs);
}
/**
* Créer prompt batch pour éléments normaux
*/
function createBatchPrompt(elements, csvData) {
const personality = csvData.personality;
let prompt = `=== GÉNÉRATION CONTENU INITIAL ===
Entreprise: Autocollant.fr - signalétique personnalisée
Sujet: ${csvData.mc0}
Rédacteur: ${personality.nom} (${personality.style})
ÉLÉMENTS À GÉNÉRER:
`;
elements.forEach((elementInfo, index) => {
const cleanTag = elementInfo.tag.replace(/\|/g, '');
prompt += `${index + 1}. [${cleanTag}] - ${getElementDescription(elementInfo)}\n`;
});
prompt += `
STYLE ${personality.nom.toUpperCase()}:
- Vocabulaire: ${personality.vocabulairePref}
- Phrases: ${personality.longueurPhrases}
- Niveau: ${personality.niveauTechnique}
CONSIGNES:
- Contenu SEO optimisé pour ${csvData.mc0}
- Style ${personality.style} naturel
- Pas de références techniques dans contenu
- RÉPONSE DIRECTE par le contenu
FORMAT:
[${elements[0].tag.replace(/\|/g, '')}]
Contenu généré...
[${elements[1] ? elements[1].tag.replace(/\|/g, '') : 'element2'}]
Contenu généré...`;
return prompt;
}
/**
* Parser réponse batch
*/
function parseBatchResponse(response, elements) {
const results = {};
const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = cleanGeneratedContent(match[2].trim());
parsedItems[tag] = content;
}
// Mapper aux vrais tags
elements.forEach(element => {
const cleanTag = element.tag.replace(/\|/g, '');
if (parsedItems[cleanTag] && parsedItems[cleanTag].length > 10) {
results[element.tag] = parsedItems[cleanTag];
} else {
results[element.tag] = `Contenu professionnel pour ${element.element.name || cleanTag}`;
logSh(`⚠️ Fallback pour [${cleanTag}]`, 'WARNING');
}
});
return results;
}
/**
* Créer prompt pour paires FAQ
*/
function createFAQPairsPrompt(faqPairs, csvData) {
const personality = csvData.personality;
let prompt = `=== GÉNÉRATION PAIRES FAQ ===
Sujet: ${csvData.mc0}
Rédacteur: ${personality.nom} (${personality.style})
PAIRES À GÉNÉRER:
`;
faqPairs.forEach((pair, index) => {
const qTag = pair.question.tag.replace(/\|/g, '');
const aTag = pair.answer.tag.replace(/\|/g, '');
prompt += `${index + 1}. [${qTag}] + [${aTag}]\n`;
});
prompt += `
CONSIGNES:
- Questions naturelles de clients
- Réponses expertes ${personality.style}
- Couvrir: prix, livraison, personnalisation
FORMAT:
[${faqPairs[0].question.tag.replace(/\|/g, '')}]
Question client naturelle ?
[${faqPairs[0].answer.tag.replace(/\|/g, '')}]
Réponse utile et rassurante.`;
return prompt;
}
/**
* Parser réponse FAQ
*/
function parseFAQResponse(response, faqPairs) {
const results = {};
const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = cleanGeneratedContent(match[2].trim());
parsedItems[tag] = content;
}
// Mapper aux paires FAQ
faqPairs.forEach(pair => {
const qCleanTag = pair.question.tag.replace(/\|/g, '');
const aCleanTag = pair.answer.tag.replace(/\|/g, '');
if (parsedItems[qCleanTag]) results[pair.question.tag] = parsedItems[qCleanTag];
if (parsedItems[aCleanTag]) results[pair.answer.tag] = parsedItems[aCleanTag];
});
return results;
}
// ============= HELPER FUNCTIONS =============
function collectElementsInXMLOrder(hierarchy) {
const allElements = [];
Object.keys(hierarchy).forEach(path => {
const section = hierarchy[path];
if (section.title) {
allElements.push({
tag: section.title.originalElement.originalTag,
element: section.title.originalElement,
type: section.title.originalElement.type
});
}
if (section.text) {
allElements.push({
tag: section.text.originalElement.originalTag,
element: section.text.originalElement,
type: section.text.originalElement.type
});
}
section.questions.forEach(q => {
allElements.push({
tag: q.originalElement.originalTag,
element: q.originalElement,
type: q.originalElement.type
});
});
});
return allElements;
}
function separateElementTypes(allElements) {
const faqPairs = [];
const otherElements = [];
const faqQuestions = {};
const faqAnswers = {};
// Collecter FAQ questions et answers
allElements.forEach(element => {
if (element.type === 'faq_question') {
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqQuestions[faqNumber] = element;
} else if (element.type === 'faq_reponse') {
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqAnswers[faqNumber] = element;
} else {
otherElements.push(element);
}
});
// Créer paires FAQ
Object.keys(faqQuestions).forEach(number => {
const question = faqQuestions[number];
const answer = faqAnswers[number];
if (question && answer) {
faqPairs.push({ number, question, answer });
} else if (question) {
otherElements.push(question);
} else if (answer) {
otherElements.push(answer);
}
});
return { faqPairs, otherElements };
}
function getElementDescription(elementInfo) {
switch (elementInfo.type) {
case 'titre_h1': return 'Titre principal accrocheur';
case 'titre_h2': return 'Titre de section';
case 'titre_h3': return 'Sous-titre';
case 'intro': return 'Introduction engageante';
case 'texte': return 'Paragraphe informatif';
default: return 'Contenu pertinent';
}
}
function cleanGeneratedContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?Titre_[HU]\d+_\d+[.,\s]*/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Helper: Déterminer type d'élément dominant dans un chunk
*/
function getElementTypeFromChunk(chunk) {
if (!chunk || chunk.length === 0) return 'generic';
// Compter les types dans le chunk
const typeCounts = {};
chunk.forEach(element => {
const type = element.type || 'generic';
typeCounts[type] = (typeCounts[type] || 0) + 1;
});
// Retourner type le plus fréquent
return Object.keys(typeCounts).reduce((a, b) =>
typeCounts[a] > typeCounts[b] ? a : b
);
}
module.exports = {
generateInitialContentAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL
generateNormalElementsAdversarial,
generateFAQPairsAdversarial,
createBatchPrompt,
parseBatchResponse,
collectElementsInXMLOrder,
separateElementTypes,
getElementTypeFromChunk
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialLayers.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ADVERSARIAL LAYERS - COUCHES MODULAIRES
// Responsabilité: Couches adversariales composables et réutilisables
// Architecture: Fonction pipeline |> layer1 |> layer2 |> layer3
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { applyAdversarialLayer } = require('./AdversarialCore');
/**
* COUCHE ANTI-GPTZEERO - Spécialisée contre GPTZero
*/
async function applyAntiGPTZeroLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: 'gptZero',
intensity: options.intensity || 1.0,
method: options.method || 'regeneration',
...options
});
}
/**
* COUCHE ANTI-ORIGINALITY - Spécialisée contre Originality.ai
*/
async function applyAntiOriginalityLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: 'originality',
intensity: options.intensity || 1.1,
method: options.method || 'hybrid',
...options
});
}
/**
* COUCHE ANTI-WINSTON - Spécialisée contre Winston AI
*/
async function applyAntiWinstonLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: 'winston',
intensity: options.intensity || 0.9,
method: options.method || 'enhancement',
...options
});
}
/**
* COUCHE GÉNÉRALE - Protection généraliste multi-détecteurs
*/
async function applyGeneralAdversarialLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: 'general',
intensity: options.intensity || 0.8,
method: options.method || 'hybrid',
...options
});
}
/**
* COUCHE LÉGÈRE - Modifications subtiles pour préserver qualité
*/
async function applyLightAdversarialLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: options.detectorTarget || 'general',
intensity: 0.5,
method: 'enhancement',
preserveStructure: true,
...options
});
}
/**
* COUCHE INTENSIVE - Maximum anti-détection
*/
async function applyIntensiveAdversarialLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: options.detectorTarget || 'gptZero',
intensity: 1.5,
method: 'regeneration',
preserveStructure: false,
...options
});
}
/**
* PIPELINE COMPOSABLE - Application séquentielle de couches
*/
async function applyLayerPipeline(content, layers = [], globalOptions = {}) {
return await tracer.run('AdversarialLayers.applyLayerPipeline()', async () => {
await tracer.annotate({
layersPipeline: true,
layersCount: layers.length,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🔄 PIPELINE COUCHES ADVERSARIALES: ${layers.length} couches`, 'INFO');
let currentContent = content;
const pipelineStats = {
layers: [],
totalDuration: 0,
totalModifications: 0,
success: true
};
try {
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
const layerStartTime = Date.now();
logSh(` 🎯 Couche ${i + 1}/${layers.length}: ${layer.name || layer.type || 'anonyme'}`, 'DEBUG');
try {
const layerResult = await applyLayerByConfig(currentContent, layer, globalOptions);
currentContent = layerResult.content;
const layerStats = {
name: layer.name || `layer_${i + 1}`,
type: layer.type,
duration: Date.now() - layerStartTime,
modificationsCount: layerResult.stats?.elementsModified || 0,
success: true
};
pipelineStats.layers.push(layerStats);
pipelineStats.totalModifications += layerStats.modificationsCount;
logSh(`${layerStats.name}: ${layerStats.modificationsCount} modifs (${layerStats.duration}ms)`, 'DEBUG');
} catch (error) {
logSh(` ❌ Couche ${i + 1} échouée: ${error.message}`, 'ERROR');
pipelineStats.layers.push({
name: layer.name || `layer_${i + 1}`,
type: layer.type,
duration: Date.now() - layerStartTime,
success: false,
error: error.message
});
// Continuer avec le contenu précédent si une couche échoue
if (!globalOptions.stopOnError) {
continue;
} else {
throw error;
}
}
}
pipelineStats.totalDuration = Date.now() - startTime;
pipelineStats.success = pipelineStats.layers.every(layer => layer.success);
logSh(`🔄 PIPELINE TERMINÉ: ${pipelineStats.totalModifications} modifs totales (${pipelineStats.totalDuration}ms)`, 'INFO');
await tracer.event('Pipeline couches terminé', pipelineStats);
return {
content: currentContent,
stats: pipelineStats,
original: content
};
} catch (error) {
pipelineStats.totalDuration = Date.now() - startTime;
pipelineStats.success = false;
logSh(`❌ PIPELINE COUCHES ÉCHOUÉ après ${pipelineStats.totalDuration}ms: ${error.message}`, 'ERROR');
throw error;
}
}, { layers: layers.map(l => l.name || l.type), content: Object.keys(content) });
}
/**
* COUCHES PRÉDÉFINIES - Configurations courantes
*/
const PREDEFINED_LAYERS = {
// Stack défensif léger
lightDefense: [
{ type: 'general', name: 'General Light', intensity: 0.6, method: 'enhancement' },
{ type: 'anti-gptZero', name: 'GPTZero Light', intensity: 0.5, method: 'enhancement' }
],
// Stack défensif standard
standardDefense: [
{ type: 'general', name: 'General Standard', intensity: 0.8, method: 'hybrid' },
{ type: 'anti-gptZero', name: 'GPTZero Standard', intensity: 0.9, method: 'enhancement' },
{ type: 'anti-originality', name: 'Originality Standard', intensity: 0.8, method: 'enhancement' }
],
// Stack défensif intensif
heavyDefense: [
{ type: 'general', name: 'General Heavy', intensity: 1.0, method: 'regeneration' },
{ type: 'anti-gptZero', name: 'GPTZero Heavy', intensity: 1.2, method: 'regeneration' },
{ type: 'anti-originality', name: 'Originality Heavy', intensity: 1.1, method: 'hybrid' },
{ type: 'anti-winston', name: 'Winston Heavy', intensity: 1.0, method: 'enhancement' }
],
// Stack ciblé GPTZero
gptZeroFocused: [
{ type: 'anti-gptZero', name: 'GPTZero Primary', intensity: 1.3, method: 'regeneration' },
{ type: 'general', name: 'General Support', intensity: 0.7, method: 'enhancement' }
],
// Stack ciblé Originality
originalityFocused: [
{ type: 'anti-originality', name: 'Originality Primary', intensity: 1.4, method: 'hybrid' },
{ type: 'general', name: 'General Support', intensity: 0.8, method: 'enhancement' }
]
};
/**
* APPLIQUER STACK PRÉDÉFINI
*/
async function applyPredefinedStack(content, stackName, options = {}) {
const stack = PREDEFINED_LAYERS[stackName];
if (!stack) {
throw new Error(`Stack prédéfini inconnu: ${stackName}. Disponibles: ${Object.keys(PREDEFINED_LAYERS).join(', ')}`);
}
logSh(`📦 APPLICATION STACK PRÉDÉFINI: ${stackName}`, 'INFO');
return await applyLayerPipeline(content, stack, options);
}
/**
* COUCHES ADAPTATIVES - S'adaptent selon le contenu
*/
async function applyAdaptiveLayers(content, options = {}) {
const {
targetDetectors = ['gptZero', 'originality'],
maxIntensity = 1.0,
analysisMode = true
} = options;
logSh(`🧠 COUCHES ADAPTATIVES: Analyse + adaptation auto`, 'INFO');
// 1. Analyser le contenu pour détecter les risques
const contentAnalysis = analyzeContentRisks(content);
// 2. Construire pipeline adaptatif selon l'analyse
const adaptiveLayers = [];
// Niveau de base selon risque global
const baseIntensity = Math.min(maxIntensity, contentAnalysis.globalRisk * 1.2);
if (baseIntensity > 0.3) {
adaptiveLayers.push({
type: 'general',
name: 'Adaptive Base',
intensity: baseIntensity,
method: baseIntensity > 0.7 ? 'hybrid' : 'enhancement'
});
}
// Couches spécifiques selon détecteurs ciblés
targetDetectors.forEach(detector => {
const detectorRisk = contentAnalysis.detectorRisks[detector] || 0;
if (detectorRisk > 0.4) {
const intensity = Math.min(maxIntensity * 1.1, detectorRisk * 1.5);
adaptiveLayers.push({
type: `anti-${detector}`,
name: `Adaptive ${detector}`,
intensity,
method: intensity > 0.8 ? 'regeneration' : 'enhancement'
});
}
});
logSh(` 🎯 ${adaptiveLayers.length} couches adaptatives générées`, 'DEBUG');
if (adaptiveLayers.length === 0) {
logSh(` ✅ Contenu déjà optimal, aucune couche nécessaire`, 'INFO');
return { content, stats: { adaptive: true, layersApplied: 0 }, original: content };
}
return await applyLayerPipeline(content, adaptiveLayers, options);
}
// ============= HELPER FUNCTIONS =============
/**
* Appliquer couche selon configuration
*/
async function applyLayerByConfig(content, layerConfig, globalOptions = {}) {
const { type, intensity, method, ...layerOptions } = layerConfig;
const options = { ...globalOptions, ...layerOptions, intensity, method };
switch (type) {
case 'general':
return await applyGeneralAdversarialLayer(content, options);
case 'anti-gptZero':
return await applyAntiGPTZeroLayer(content, options);
case 'anti-originality':
return await applyAntiOriginalityLayer(content, options);
case 'anti-winston':
return await applyAntiWinstonLayer(content, options);
case 'light':
return await applyLightAdversarialLayer(content, options);
case 'intensive':
return await applyIntensiveAdversarialLayer(content, options);
default:
throw new Error(`Type de couche inconnu: ${type}`);
}
}
/**
* Analyser risques du contenu pour adaptation
*/
function analyzeContentRisks(content) {
const analysis = {
globalRisk: 0,
detectorRisks: {},
riskFactors: []
};
const allContent = Object.values(content).join(' ');
// Risques génériques
let riskScore = 0;
// 1. Mots typiques IA
const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge', 'furthermore', 'moreover'];
const aiWordCount = aiWords.filter(word => allContent.toLowerCase().includes(word)).length;
if (aiWordCount > 2) {
riskScore += 0.3;
analysis.riskFactors.push(`mots_ia: ${aiWordCount}`);
}
// 2. Structure uniforme
const contentLengths = Object.values(content).map(c => c.length);
const avgLength = contentLengths.reduce((a, b) => a + b, 0) / contentLengths.length;
const variance = contentLengths.reduce((sum, len) => sum + Math.pow(len - avgLength, 2), 0) / contentLengths.length;
const uniformity = 1 - (Math.sqrt(variance) / Math.max(avgLength, 1));
if (uniformity > 0.8) {
riskScore += 0.2;
analysis.riskFactors.push(`uniformité: ${uniformity.toFixed(2)}`);
}
// 3. Connecteurs répétitifs
const repetitiveConnectors = ['par ailleurs', 'en effet', 'de plus', 'cependant'];
const connectorCount = repetitiveConnectors.filter(conn =>
(allContent.match(new RegExp(conn, 'gi')) || []).length > 1
).length;
if (connectorCount > 2) {
riskScore += 0.2;
analysis.riskFactors.push(`connecteurs_répétitifs: ${connectorCount}`);
}
analysis.globalRisk = Math.min(1, riskScore);
// Risques spécifiques par détecteur
analysis.detectorRisks = {
gptZero: analysis.globalRisk + (uniformity > 0.7 ? 0.3 : 0),
originality: analysis.globalRisk + (aiWordCount > 3 ? 0.4 : 0),
winston: analysis.globalRisk + (connectorCount > 2 ? 0.2 : 0)
};
return analysis;
}
/**
* Obtenir informations sur les stacks disponibles
*/
function getAvailableStacks() {
return Object.keys(PREDEFINED_LAYERS).map(stackName => ({
name: stackName,
layersCount: PREDEFINED_LAYERS[stackName].length,
description: getStackDescription(stackName),
layers: PREDEFINED_LAYERS[stackName]
}));
}
/**
* Description des stacks prédéfinis
*/
function getStackDescription(stackName) {
const descriptions = {
lightDefense: 'Protection légère préservant la qualité',
standardDefense: 'Protection équilibrée multi-détecteurs',
heavyDefense: 'Protection maximale tous détecteurs',
gptZeroFocused: 'Optimisation spécifique anti-GPTZero',
originalityFocused: 'Optimisation spécifique anti-Originality.ai'
};
return descriptions[stackName] || 'Stack personnalisé';
}
module.exports = {
// Couches individuelles
applyAntiGPTZeroLayer,
applyAntiOriginalityLayer,
applyAntiWinstonLayer,
applyGeneralAdversarialLayer,
applyLightAdversarialLayer,
applyIntensiveAdversarialLayer,
// Pipeline et stacks
applyLayerPipeline, // ← MAIN ENTRY POINT PIPELINE
applyPredefinedStack, // ← MAIN ENTRY POINT STACKS
applyAdaptiveLayers, // ← MAIN ENTRY POINT ADAPTATIF
// Utilitaires
getAvailableStacks,
analyzeContentRisks,
PREDEFINED_LAYERS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialStyleEnhancement.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ÉTAPE 4: ENHANCEMENT STYLE PERSONNALITÉ ADVERSARIAL
// Responsabilité: Appliquer le style personnalité avec Mistral + anti-détection
// LLM: Mistral (température 0.8) + Prompts adversariaux
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { createAdversarialPrompt } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - ENHANCEMENT STYLE
* Input: { content: {}, csvData: {}, context: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function applyPersonalityStyleAdversarial(input) {
return await tracer.run('AdversarialStyleEnhancement.applyPersonalityStyleAdversarial()', async () => {
const { content, csvData, context = {}, adversarialConfig = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true,
contextualMode: adversarialConfig.contextualMode !== false,
...adversarialConfig
};
// Initialiser manager détecteur
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
await tracer.annotate({
step: '4/4',
llmProvider: 'mistral',
elementsCount: Object.keys(content).length,
personality: csvData.personality?.nom,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 ÉTAPE 4/4 ADVERSARIAL: Enhancement style ${csvData.personality?.nom} (Mistral + ${config.detectorTarget})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à styliser`, 'INFO');
try {
const personality = csvData.personality;
if (!personality) {
logSh(`⚠️ ÉTAPE 4/4: Aucune personnalité définie, style standard`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'mistral', step: 4, personalityApplied: 'none' }
};
}
// 1. Préparer éléments pour stylisation
const styleElements = prepareElementsForStyling(content);
// 2. Appliquer style en chunks avec prompts adversariaux
const styledResults = await applyStyleInChunksAdversarial(styleElements, csvData, config, detectorManager);
// 3. Merger résultats
const finalContent = { ...content };
let actuallyStyled = 0;
Object.keys(styledResults).forEach(tag => {
if (styledResults[tag] !== content[tag]) {
finalContent[tag] = styledResults[tag];
actuallyStyled++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyStyled,
personality: personality.nom,
duration
};
logSh(`✅ ÉTAPE 4/4 TERMINÉE: ${stats.enhanced} éléments stylisés ${personality.nom} (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement style terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'mistral',
step: 4,
personalityApplied: personality.nom,
styleCharacteristics: {
vocabulaire: personality.vocabulairePref,
connecteurs: personality.connecteursPref,
style: personality.style
},
adversarialConfig: config,
detectorTarget: config.detectorTarget,
intensity: config.intensity
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 4/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original si Mistral indisponible
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration },
debug: { llmProvider: 'mistral', step: 4, error: error.message, fallback: true }
};
}
}, input);
}
/**
* Préparer éléments pour stylisation
*/
function prepareElementsForStyling(content) {
const styleElements = [];
Object.keys(content).forEach(tag => {
const text = content[tag];
// Tous les éléments peuvent bénéficier d'adaptation personnalité
// Même les courts (titres) peuvent être adaptés au style
styleElements.push({
tag,
content: text,
priority: calculateStylePriority(text, tag)
});
});
// Trier par priorité (titres d'abord, puis textes longs)
styleElements.sort((a, b) => b.priority - a.priority);
return styleElements;
}
/**
* Calculer priorité de stylisation
*/
function calculateStylePriority(text, tag) {
let priority = 1.0;
// Titres = haute priorité (plus visible)
if (tag.includes('Titre') || tag.includes('H1') || tag.includes('H2')) {
priority += 0.5;
}
// Textes longs = priorité selon longueur
if (text.length > 200) {
priority += 0.3;
} else if (text.length > 100) {
priority += 0.2;
}
// Introduction = haute priorité
if (tag.includes('intro') || tag.includes('Introduction')) {
priority += 0.4;
}
return priority;
}
/**
* Appliquer style en chunks avec prompts adversariaux
*/
async function applyStyleInChunksAdversarial(styleElements, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Stylisation adversarial: ${styleElements.length} éléments selon ${csvData.personality.nom}`, 'DEBUG');
const results = {};
const chunks = chunkArray(styleElements, 8); // Chunks de 8 pour Mistral
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const basePrompt = createStylePrompt(chunk, csvData);
// Générer prompt adversarial pour stylisation
const adversarialPrompt = createAdversarialPrompt(basePrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity * 1.1, // Intensité plus élevée pour style (plus visible)
elementType: 'style_enhancement',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const styledResponse = await callLLM('mistral', adversarialPrompt, {
temperature: 0.8,
maxTokens: 3000
}, csvData.personality);
const chunkResults = parseStyleResponse(styledResponse, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} stylisés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: garder contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* Créer prompt de stylisation
*/
function createStylePrompt(chunk, csvData) {
const personality = csvData.personality;
let prompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}.
CONTEXTE: Article SEO e-commerce ${csvData.mc0}
PERSONNALITÉ: ${personality.nom}
DESCRIPTION: ${personality.description}
STYLE: ${personality.style} adapté web professionnel
VOCABULAIRE: ${personality.vocabulairePref}
CONNECTEURS: ${personality.connecteursPref}
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
LONGUEUR PHRASES: ${personality.longueurPhrases}
CONTENUS À STYLISER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} (Priorité: ${item.priority.toFixed(1)})
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS STYLISATION ${personality.nom.toUpperCase()}:
- Adapte le TON selon ${personality.style}
- Vocabulaire: ${personality.vocabulairePref}
- Connecteurs variés: ${personality.connecteursPref}
- Phrases: ${personality.longueurPhrases}
- Niveau: ${personality.niveauTechnique}
CONSIGNES STRICTES:
- GARDE le même contenu informatif et technique
- Adapte SEULEMENT ton, expressions, vocabulaire selon ${personality.nom}
- RESPECTE longueur approximative (±20%)
- ÉVITE répétitions excessives
- Style ${personality.nom} reconnaissable mais NATUREL web
- PAS de messages d'excuse
FORMAT RÉPONSE:
[1] Contenu stylisé selon ${personality.nom}
[2] Contenu stylisé selon ${personality.nom}
etc...`;
return prompt;
}
/**
* Parser réponse stylisation
*/
function parseStyleResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let styledContent = match[2].trim();
const element = chunk[index];
// Nettoyer le contenu stylisé
styledContent = cleanStyledContent(styledContent);
if (styledContent && styledContent.length > 10) {
results[element.tag] = styledContent;
logSh(`✅ Styled [${element.tag}]: "${styledContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: stylisation invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu stylisé
*/
function cleanStyledContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?voici\s+/gi, '');
content = content.replace(/^pour\s+ce\s+contenu[,\s]*/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
// Réduire répétitions excessives mais garder le style personnalité
content = content.replace(/(du coup[,\s]+){4,}/gi, 'du coup ');
content = content.replace(/(bon[,\s]+){4,}/gi, 'bon ');
content = content.replace(/(franchement[,\s]+){3,}/gi, 'franchement ');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
/**
* Obtenir instructions de style dynamiques
*/
function getPersonalityStyleInstructions(personality) {
if (!personality) return "Style professionnel standard";
return `STYLE ${personality.nom.toUpperCase()} (${personality.style}):
- Description: ${personality.description}
- Vocabulaire: ${personality.vocabulairePref || 'professionnel'}
- Connecteurs: ${personality.connecteursPref || 'par ailleurs, en effet'}
- Mots-clés: ${personality.motsClesSecteurs || 'technique, qualité'}
- Phrases: ${personality.longueurPhrases || 'Moyennes'}
- Niveau: ${personality.niveauTechnique || 'Accessible'}
- CTA: ${personality.ctaStyle || 'Professionnel'}`;
}
// ============= HELPER FUNCTIONS =============
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
applyPersonalityStyleAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL
prepareElementsForStyling,
calculateStylePriority,
applyStyleInChunksAdversarial,
createStylePrompt,
parseStyleResponse,
getPersonalityStyleInstructions
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialTechnicalEnhancem… │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ÉTAPE 2: ENHANCEMENT TECHNIQUE ADVERSARIAL
// Responsabilité: Améliorer la précision technique avec GPT-4 + anti-détection
// LLM: GPT-4o-mini (température 0.4) + Prompts adversariaux
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { createAdversarialPrompt } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - ENHANCEMENT TECHNIQUE ADVERSARIAL
* Input: { content: {}, csvData: {}, context: {}, adversarialConfig: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function enhanceTechnicalTermsAdversarial(input) {
return await tracer.run('AdversarialTechnicalEnhancement.enhanceTechnicalTermsAdversarial()', async () => {
const { content, csvData, context = {}, adversarialConfig = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true,
contextualMode: adversarialConfig.contextualMode !== false,
...adversarialConfig
};
// Initialiser manager détecteur
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
await tracer.annotate({
step: '2/4',
llmProvider: 'gpt4',
elementsCount: Object.keys(content).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 ÉTAPE 2/4 ADVERSARIAL: Enhancement technique (GPT-4 + ${config.detectorTarget})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'INFO');
try {
// 1. Analyser tous les éléments pour détecter termes techniques (adversarial)
const technicalAnalysis = await analyzeTechnicalTermsAdversarial(content, csvData, config, detectorManager);
// 2. Filter les éléments qui ont besoin d'enhancement
const elementsNeedingEnhancement = technicalAnalysis.filter(item => item.needsEnhancement);
logSh(` 📋 Analyse: ${elementsNeedingEnhancement.length}/${Object.keys(content).length} éléments nécessitent enhancement`, 'INFO');
if (elementsNeedingEnhancement.length === 0) {
logSh(`✅ ÉTAPE 2/4: Aucun enhancement nécessaire`, 'INFO');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'gpt4', step: 2, enhancementsApplied: [] }
};
}
// 3. Améliorer les éléments sélectionnés avec prompts adversariaux
const enhancedResults = await enhanceSelectedElementsAdversarial(elementsNeedingEnhancement, csvData, config, detectorManager);
// 4. Merger avec contenu original
const finalContent = { ...content };
let actuallyEnhanced = 0;
Object.keys(enhancedResults).forEach(tag => {
if (enhancedResults[tag] !== content[tag]) {
finalContent[tag] = enhancedResults[tag];
actuallyEnhanced++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyEnhanced,
candidate: elementsNeedingEnhancement.length,
duration
};
logSh(`✅ ÉTAPE 2/4 TERMINÉE: ${stats.enhanced} éléments améliorés (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement technique terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'gpt4',
step: 2,
enhancementsApplied: Object.keys(enhancedResults),
technicalTermsFound: elementsNeedingEnhancement.map(e => e.technicalTerms),
adversarialConfig: config,
detectorTarget: config.detectorTarget,
intensity: config.intensity
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 2/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`TechnicalEnhancement failed: ${error.message}`);
}
}, input);
}
/**
* Analyser tous les éléments pour détecter termes techniques (adversarial)
*/
async function analyzeTechnicalTermsAdversarial(content, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Analyse termes techniques adversarial batch`, 'DEBUG');
const contentEntries = Object.keys(content);
const analysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTENUS À ANALYSER:
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
CONTENU: "${content[tag]}"`).join('\n\n')}
CONSIGNES:
- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
- Évite mots génériques (qualité, service, pratique, personnalisé)
- Focus: matériaux, procédés, normes, dimensions, technologies
- Si aucun terme technique → "AUCUN"
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne
FORMAT RÉPONSE:
[1] dibond, impression UV OU AUCUN
[2] AUCUN
[3] aluminium, fraisage CNC OU AUCUN
etc...`;
try {
// Générer prompt adversarial pour analyse
const adversarialAnalysisPrompt = createAdversarialPrompt(analysisPrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity * 0.8, // Intensité modérée pour analyse
elementType: 'technical_analysis',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const analysisResponse = await callLLM('gpt4', adversarialAnalysisPrompt, {
temperature: 0.3,
maxTokens: 2000
}, csvData.personality);
return parseAnalysisResponse(analysisResponse, content, contentEntries);
} catch (error) {
logSh(`❌ Analyse termes techniques échouée: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Améliorer les éléments sélectionnés avec prompts adversariaux
*/
async function enhanceSelectedElementsAdversarial(elementsNeedingEnhancement, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Enhancement adversarial ${elementsNeedingEnhancement.length} éléments`, 'DEBUG');
const enhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces contenus.
CONTEXTE: ${csvData.mc0} - Secteur signalétique/impression
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style})
CONTENUS À AMÉLIORER:
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag}
CONTENU: "${item.content}"
TERMES TECHNIQUES: ${item.technicalTerms.join(', ')}`).join('\n\n')}
CONSIGNES:
- GARDE même longueur, structure et ton ${csvData.personality?.style}
- Intègre naturellement les termes techniques listés
- NE CHANGE PAS le fond du message
- Vocabulaire expert mais accessible
- Termes secteur: dibond, aluminium, impression UV, fraisage, PMMA
FORMAT RÉPONSE:
[1] Contenu avec amélioration technique
[2] Contenu avec amélioration technique
etc...`;
try {
// Générer prompt adversarial pour enhancement
const adversarialEnhancementPrompt = createAdversarialPrompt(enhancementPrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity,
elementType: 'technical_enhancement',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const enhancedResponse = await callLLM('gpt4', adversarialEnhancementPrompt, {
temperature: 0.4,
maxTokens: 5000
}, csvData.personality);
return parseEnhancementResponse(enhancedResponse, elementsNeedingEnhancement);
} catch (error) {
logSh(`❌ Enhancement éléments échoué: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Parser réponse analyse
*/
function parseAnalysisResponse(response, content, contentEntries) {
const results = [];
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const index = parseInt(match[1]) - 1;
const termsText = match[2].trim();
parsedItems[index] = termsText;
}
contentEntries.forEach((tag, index) => {
const termsText = parsedItems[index] || 'AUCUN';
const hasTerms = !termsText.toUpperCase().includes('AUCUN');
const technicalTerms = hasTerms ?
termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) :
[];
results.push({
tag,
content: content[tag],
technicalTerms,
needsEnhancement: hasTerms && technicalTerms.length > 0
});
logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'aucun terme technique'}`, 'DEBUG');
});
return results;
}
/**
* Parser réponse enhancement
*/
function parseEnhancementResponse(response, elementsNeedingEnhancement) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < elementsNeedingEnhancement.length) {
let enhancedContent = match[2].trim();
const element = elementsNeedingEnhancement[index];
// Nettoyer le contenu généré
enhancedContent = cleanEnhancedContent(enhancedContent);
if (enhancedContent && enhancedContent.length > 10) {
results[element.tag] = enhancedContent;
logSh(`✅ Enhanced [${element.tag}]: "${enhancedContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: contenu invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < elementsNeedingEnhancement.length) {
const element = elementsNeedingEnhancement[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu amélioré
*/
function cleanEnhancedContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?pour\s+/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
module.exports = {
enhanceTechnicalTermsAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL
analyzeTechnicalTermsAdversarial,
enhanceSelectedElementsAdversarial,
parseAnalysisResponse,
parseEnhancementResponse
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialTransitionEnhance… │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ÉTAPE 3: ENHANCEMENT TRANSITIONS ADVERSARIAL
// Responsabilité: Améliorer la fluidité avec Gemini + anti-détection
// LLM: Gemini (température 0.6) + Prompts adversariaux
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { createAdversarialPrompt } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - ENHANCEMENT TRANSITIONS ADVERSARIAL
* Input: { content: {}, csvData: {}, context: {}, adversarialConfig: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function enhanceTransitionsAdversarial(input) {
return await tracer.run('AdversarialTransitionEnhancement.enhanceTransitionsAdversarial()', async () => {
const { content, csvData, context = {}, adversarialConfig = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true,
contextualMode: adversarialConfig.contextualMode !== false,
...adversarialConfig
};
// Initialiser manager détecteur
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
await tracer.annotate({
step: '3/4',
llmProvider: 'gemini',
elementsCount: Object.keys(content).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 ÉTAPE 3/4 ADVERSARIAL: Enhancement transitions (Gemini + ${config.detectorTarget})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'INFO');
try {
// 1. Analyser quels éléments ont besoin d'amélioration transitions
const elementsNeedingTransitions = analyzeTransitionNeeds(content);
logSh(` 📋 Analyse: ${elementsNeedingTransitions.length}/${Object.keys(content).length} éléments nécessitent fluidité`, 'INFO');
if (elementsNeedingTransitions.length === 0) {
logSh(`✅ ÉTAPE 3/4: Transitions déjà optimales`, 'INFO');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'gemini', step: 3, enhancementsApplied: [] }
};
}
// 2. Améliorer en chunks avec prompts adversariaux pour Gemini
const improvedResults = await improveTransitionsInChunksAdversarial(elementsNeedingTransitions, csvData, config, detectorManager);
// 3. Merger avec contenu original
const finalContent = { ...content };
let actuallyImproved = 0;
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
finalContent[tag] = improvedResults[tag];
actuallyImproved++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyImproved,
candidate: elementsNeedingTransitions.length,
duration
};
logSh(`✅ ÉTAPE 3/4 TERMINÉE: ${stats.enhanced} éléments fluidifiés (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement transitions terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'gemini',
step: 3,
enhancementsApplied: Object.keys(improvedResults),
transitionIssues: elementsNeedingTransitions.map(e => e.issues),
adversarialConfig: config,
detectorTarget: config.detectorTarget,
intensity: config.intensity
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 3/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original si Gemini indisponible
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration },
debug: { llmProvider: 'gemini', step: 3, error: error.message, fallback: true }
};
}
}, input);
}
/**
* Analyser besoin d'amélioration transitions
*/
function analyzeTransitionNeeds(content) {
const elementsNeedingTransitions = [];
Object.keys(content).forEach(tag => {
const text = content[tag];
// Filtrer les éléments longs (>150 chars) qui peuvent bénéficier d'améliorations
if (text.length > 150) {
const needsTransitions = evaluateTransitionQuality(text);
if (needsTransitions.needsImprovement) {
elementsNeedingTransitions.push({
tag,
content: text,
issues: needsTransitions.issues,
score: needsTransitions.score
});
logSh(` 🔍 [${tag}]: Score=${needsTransitions.score.toFixed(2)}, Issues: ${needsTransitions.issues.join(', ')}`, 'DEBUG');
}
} else {
logSh(` ⏭️ [${tag}]: Trop court (${text.length}c), ignoré`, 'DEBUG');
}
});
// Trier par score (plus problématique en premier)
elementsNeedingTransitions.sort((a, b) => a.score - b.score);
return elementsNeedingTransitions;
}
/**
* Évaluer qualité transitions d'un texte
*/
function evaluateTransitionQuality(text) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) {
return { needsImprovement: false, score: 1.0, issues: [] };
}
const issues = [];
let score = 1.0; // Score parfait = 1.0, problématique = 0.0
// Analyse 1: Connecteurs répétitifs
const repetitiveConnectors = analyzeRepetitiveConnectors(text);
if (repetitiveConnectors > 0.3) {
issues.push('connecteurs_répétitifs');
score -= 0.3;
}
// Analyse 2: Transitions abruptes
const abruptTransitions = analyzeAbruptTransitions(sentences);
if (abruptTransitions > 0.4) {
issues.push('transitions_abruptes');
score -= 0.4;
}
// Analyse 3: Manque de variété dans longueurs
const sentenceVariety = analyzeSentenceVariety(sentences);
if (sentenceVariety < 0.3) {
issues.push('phrases_uniformes');
score -= 0.2;
}
// Analyse 4: Trop formel ou trop familier
const formalityIssues = analyzeFormalityBalance(text);
if (formalityIssues > 0.5) {
issues.push('formalité_déséquilibrée');
score -= 0.1;
}
return {
needsImprovement: score < 0.6,
score: Math.max(0, score),
issues
};
}
/**
* Améliorer transitions en chunks avec prompts adversariaux
*/
async function improveTransitionsInChunksAdversarial(elementsNeedingTransitions, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Amélioration transitions adversarial: ${elementsNeedingTransitions.length} éléments`, 'DEBUG');
const results = {};
const chunks = chunkArray(elementsNeedingTransitions, 6); // Chunks plus petits pour Gemini
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const basePrompt = createTransitionImprovementPrompt(chunk, csvData);
// Générer prompt adversarial pour amélioration transitions
const adversarialPrompt = createAdversarialPrompt(basePrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity * 0.9, // Intensité légèrement réduite pour transitions
elementType: 'transition_enhancement',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const improvedResponse = await callLLM('gemini', adversarialPrompt, {
temperature: 0.6,
maxTokens: 2500
}, csvData.personality);
const chunkResults = parseTransitionResponse(improvedResponse, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} améliorés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: garder contenu original pour ce chunk
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* Créer prompt amélioration transitions
*/
function createTransitionImprovementPrompt(chunk, csvData) {
const personality = csvData.personality;
let prompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.
CONTEXTE: Article SEO ${csvData.mc0}
PERSONNALITÉ: ${personality?.nom} (${personality?.style} web professionnel)
CONNECTEURS PRÉFÉRÉS: ${personality?.connecteursPref}
CONTENUS À FLUIDIFIER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
PROBLÈMES: ${item.issues.join(', ')}
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS:
- Connecteurs plus naturels et variés: ${personality?.connecteursPref}
- Transitions fluides entre idées
- ÉVITE répétitions excessives ("du coup", "franchement", "par ailleurs")
- Style ${personality?.style} mais professionnel web
CONSIGNES STRICTES:
- NE CHANGE PAS le fond du message
- GARDE même structure et longueur
- Améliore SEULEMENT la fluidité
- RESPECTE le style ${personality?.nom}
FORMAT RÉPONSE:
[1] Contenu avec transitions améliorées
[2] Contenu avec transitions améliorées
etc...`;
return prompt;
}
/**
* Parser réponse amélioration transitions
*/
function parseTransitionResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let improvedContent = match[2].trim();
const element = chunk[index];
// Nettoyer le contenu amélioré
improvedContent = cleanImprovedContent(improvedContent);
if (improvedContent && improvedContent.length > 10) {
results[element.tag] = improvedContent;
logSh(`✅ Improved [${element.tag}]: "${improvedContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
// ============= HELPER FUNCTIONS =============
function analyzeRepetitiveConnectors(content) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc'];
let totalConnectors = 0;
let repetitions = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
totalConnectors += matches.length;
if (matches.length > 1) repetitions += matches.length - 1;
});
return totalConnectors > 0 ? repetitions / totalConnectors : 0;
}
function analyzeAbruptTransitions(sentences) {
if (sentences.length < 2) return 0;
let abruptCount = 0;
for (let i = 1; i < sentences.length; i++) {
const current = sentences[i].trim();
const hasConnector = hasTransitionWord(current);
if (!hasConnector && current.length > 30) {
abruptCount++;
}
}
return abruptCount / (sentences.length - 1);
}
function analyzeSentenceVariety(sentences) {
if (sentences.length < 2) return 1;
const lengths = sentences.map(s => s.trim().length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
return Math.min(1, stdDev / avgLength);
}
function analyzeFormalityBalance(content) {
const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois'];
const casualIndicators = ['du coup', 'bon', 'franchement', 'nickel'];
let formalCount = 0;
let casualCount = 0;
formalIndicators.forEach(indicator => {
if (content.toLowerCase().includes(indicator)) formalCount++;
});
casualIndicators.forEach(indicator => {
if (content.toLowerCase().includes(indicator)) casualCount++;
});
const total = formalCount + casualCount;
if (total === 0) return 0;
// Déséquilibre si trop d'un côté
const balance = Math.abs(formalCount - casualCount) / total;
return balance;
}
function hasTransitionWord(sentence) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite', 'puis', 'également', 'aussi'];
return connectors.some(connector => sentence.toLowerCase().includes(connector));
}
function cleanImprovedContent(content) {
if (!content) return content;
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?/, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
enhanceTransitionsAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL
analyzeTransitionNeeds,
evaluateTransitionQuality,
improveTransitionsInChunksAdversarial,
createTransitionImprovementPrompt,
parseTransitionResponse
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/AdversarialUtils.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ADVERSARIAL UTILS - UTILITAIRES MODULAIRES
// Responsabilité: Fonctions utilitaires partagées par tous les modules adversariaux
// Architecture: Helper functions réutilisables et composables
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* ANALYSEURS DE CONTENU
*/
/**
* Analyser score de diversité lexicale
*/
function analyzeLexicalDiversity(content) {
if (!content || typeof content !== 'string') return 0;
const words = content.toLowerCase()
.split(/\s+/)
.filter(word => word.length > 2)
.map(word => word.replace(/[^\w]/g, ''));
if (words.length === 0) return 0;
const uniqueWords = [...new Set(words)];
return (uniqueWords.length / words.length) * 100;
}
/**
* Analyser variation des longueurs de phrases
*/
function analyzeSentenceVariation(content) {
if (!content || typeof content !== 'string') return 0;
const sentences = content.split(/[.!?]+/)
.map(s => s.trim())
.filter(s => s.length > 5);
if (sentences.length < 2) return 0;
const lengths = sentences.map(s => s.split(/\s+/).length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
return Math.min(100, (stdDev / avgLength) * 100);
}
/**
* Détecter mots typiques IA
*/
function detectAIFingerprints(content) {
const aiFingerprints = {
words: ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge', 'state-of-the-art', 'furthermore', 'moreover'],
phrases: ['it is important to note', 'it should be noted', 'it is worth mentioning', 'in conclusion', 'to summarize'],
connectors: ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc']
};
const results = {
words: 0,
phrases: 0,
connectors: 0,
totalScore: 0
};
const lowerContent = content.toLowerCase();
// Compter mots IA
aiFingerprints.words.forEach(word => {
const matches = (lowerContent.match(new RegExp(`\\b${word}\\b`, 'g')) || []);
results.words += matches.length;
});
// Compter phrases typiques
aiFingerprints.phrases.forEach(phrase => {
if (lowerContent.includes(phrase)) {
results.phrases += 1;
}
});
// Compter connecteurs répétitifs
aiFingerprints.connectors.forEach(connector => {
const matches = (lowerContent.match(new RegExp(`\\b${connector}\\b`, 'g')) || []);
if (matches.length > 1) {
results.connectors += matches.length - 1; // Pénalité répétition
}
});
// Score total (sur 100)
const wordCount = content.split(/\s+/).length;
results.totalScore = Math.min(100,
(results.words * 5 + results.phrases * 10 + results.connectors * 3) / Math.max(wordCount, 1) * 100
);
return results;
}
/**
* Analyser uniformité structurelle
*/
function analyzeStructuralUniformity(content) {
const sentences = content.split(/[.!?]+/)
.map(s => s.trim())
.filter(s => s.length > 5);
if (sentences.length < 3) return 0;
const structures = sentences.map(sentence => {
const words = sentence.split(/\s+/);
return {
length: words.length,
startsWithConnector: /^(par ailleurs|en effet|de plus|cependant|ainsi|donc|ensuite|puis)/i.test(sentence),
hasComma: sentence.includes(','),
hasSubordinate: /qui|que|dont|où|quand|comme|parce que|puisque|bien que/i.test(sentence)
};
});
// Calculer uniformité
const avgLength = structures.reduce((sum, s) => sum + s.length, 0) / structures.length;
const lengthVariance = structures.reduce((sum, s) => sum + Math.pow(s.length - avgLength, 2), 0) / structures.length;
const connectorRatio = structures.filter(s => s.startsWithConnector).length / structures.length;
const commaRatio = structures.filter(s => s.hasComma).length / structures.length;
// Plus c'est uniforme, plus le score est élevé (mauvais pour anti-détection)
const uniformityScore = 100 - (Math.sqrt(lengthVariance) / avgLength * 100) -
(Math.abs(0.3 - connectorRatio) * 50) - (Math.abs(0.5 - commaRatio) * 30);
return Math.max(0, Math.min(100, uniformityScore));
}
/**
* COMPARATEURS DE CONTENU
*/
/**
* Comparer deux contenus et calculer taux de modification
*/
function compareContentModification(original, modified) {
if (!original || !modified) return 0;
const originalWords = original.toLowerCase().split(/\s+/).filter(w => w.length > 2);
const modifiedWords = modified.toLowerCase().split(/\s+/).filter(w => w.length > 2);
// Calcul de distance Levenshtein approximative (par mots)
let changes = 0;
const maxLength = Math.max(originalWords.length, modifiedWords.length);
for (let i = 0; i < maxLength; i++) {
if (originalWords[i] !== modifiedWords[i]) {
changes++;
}
}
return (changes / maxLength) * 100;
}
/**
* Évaluer amélioration adversariale
*/
function evaluateAdversarialImprovement(original, modified, detectorTarget = 'general') {
const originalFingerprints = detectAIFingerprints(original);
const modifiedFingerprints = detectAIFingerprints(modified);
const originalDiversity = analyzeLexicalDiversity(original);
const modifiedDiversity = analyzeLexicalDiversity(modified);
const originalVariation = analyzeSentenceVariation(original);
const modifiedVariation = analyzeSentenceVariation(modified);
const fingerprintReduction = originalFingerprints.totalScore - modifiedFingerprints.totalScore;
const diversityIncrease = modifiedDiversity - originalDiversity;
const variationIncrease = modifiedVariation - originalVariation;
const improvementScore = (
fingerprintReduction * 0.4 +
diversityIncrease * 0.3 +
variationIncrease * 0.3
);
return {
fingerprintReduction,
diversityIncrease,
variationIncrease,
improvementScore: Math.round(improvementScore * 100) / 100,
modificationRate: compareContentModification(original, modified),
recommendation: getImprovementRecommendation(improvementScore, detectorTarget)
};
}
/**
* UTILITAIRES DE CONTENU
*/
/**
* Nettoyer contenu adversarial généré
*/
function cleanAdversarialContent(content) {
if (!content || typeof content !== 'string') return content;
let cleaned = content;
// Supprimer préfixes de génération
cleaned = cleaned.replace(/^(voici\s+)?le\s+contenu\s+(réécrit|amélioré|modifié)[:\s]*/gi, '');
cleaned = cleaned.replace(/^(bon,?\s*)?(alors,?\s*)?(pour\s+)?(ce\s+contenu[,\s]*)?/gi, '');
// Nettoyer formatage
cleaned = cleaned.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
cleaned = cleaned.replace(/\s{2,}/g, ' '); // Espaces multiples
cleaned = cleaned.replace(/([.!?])\s*([.!?])/g, '$1 '); // Double ponctuation
// Nettoyer début/fin
cleaned = cleaned.trim();
cleaned = cleaned.replace(/^[,.\s]+/, '');
cleaned = cleaned.replace(/[,\s]+$/, '');
return cleaned;
}
/**
* Valider qualité du contenu adversarial
*/
function validateAdversarialContent(content, originalContent, minLength = 10, maxModificationRate = 90) {
const validation = {
isValid: true,
issues: [],
suggestions: []
};
// Vérifier longueur minimale
if (!content || content.length < minLength) {
validation.isValid = false;
validation.issues.push('Contenu trop court');
validation.suggestions.push('Augmenter la longueur du contenu généré');
}
// Vérifier cohérence
if (originalContent) {
const modificationRate = compareContentModification(originalContent, content);
if (modificationRate > maxModificationRate) {
validation.issues.push('Modification trop importante');
validation.suggestions.push('Réduire l\'intensité adversariale pour préserver le sens');
}
if (modificationRate < 5) {
validation.issues.push('Modification insuffisante');
validation.suggestions.push('Augmenter l\'intensité adversariale');
}
}
// Vérifier empreintes IA résiduelles
const fingerprints = detectAIFingerprints(content);
if (fingerprints.totalScore > 15) {
validation.issues.push('Empreintes IA encore présentes');
validation.suggestions.push('Appliquer post-processing anti-fingerprints');
}
return validation;
}
/**
* UTILITAIRES TECHNIQUES
*/
/**
* Chunk array avec préservation des paires
*/
function chunkArraySmart(array, size, preservePairs = false) {
if (!preservePairs) {
return chunkArray(array, size);
}
const chunks = [];
for (let i = 0; i < array.length; i += size) {
let chunk = array.slice(i, i + size);
// Si on coupe au milieu d'une paire (nombre impair), ajuster
if (chunk.length % 2 !== 0 && i + size < array.length) {
chunk = array.slice(i, i + size - 1);
}
chunks.push(chunk);
}
return chunks;
}
/**
* Chunk array standard
*/
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
/**
* Sleep avec variation
*/
function sleep(ms, variation = 0.2) {
const actualMs = ms + (Math.random() - 0.5) * ms * variation;
return new Promise(resolve => setTimeout(resolve, Math.max(100, actualMs)));
}
/**
* RECOMMANDATIONS
*/
/**
* Obtenir recommandation d'amélioration
*/
function getImprovementRecommendation(score, detectorTarget) {
const recommendations = {
general: {
good: "Bon niveau d'amélioration générale",
medium: "Appliquer techniques de variation syntaxique",
poor: "Nécessite post-processing intensif"
},
gptZero: {
good: "Imprévisibilité suffisante contre GPTZero",
medium: "Ajouter plus de ruptures narratives",
poor: "Intensifier variation syntaxique et lexicale"
},
originality: {
good: "Créativité suffisante contre Originality",
medium: "Enrichir diversité sémantique",
poor: "Réinventer présentation des informations"
}
};
const category = score > 10 ? 'good' : score > 5 ? 'medium' : 'poor';
return recommendations[detectorTarget]?.[category] || recommendations.general[category];
}
/**
* MÉTRIQUES ET STATS
*/
/**
* Calculer score composite anti-détection
*/
function calculateAntiDetectionScore(content, detectorTarget = 'general') {
const diversity = analyzeLexicalDiversity(content);
const variation = analyzeSentenceVariation(content);
const fingerprints = detectAIFingerprints(content);
const uniformity = analyzeStructuralUniformity(content);
const baseScore = (diversity * 0.3 + variation * 0.3 + (100 - fingerprints.totalScore) * 0.2 + (100 - uniformity) * 0.2);
// Ajustements selon détecteur
let adjustedScore = baseScore;
switch (detectorTarget) {
case 'gptZero':
adjustedScore = baseScore * (variation / 100) * 1.2; // Favorise variation
break;
case 'originality':
adjustedScore = baseScore * (diversity / 100) * 1.2; // Favorise diversité
break;
}
return Math.min(100, Math.max(0, Math.round(adjustedScore)));
}
module.exports = {
// Analyseurs
analyzeLexicalDiversity,
analyzeSentenceVariation,
detectAIFingerprints,
analyzeStructuralUniformity,
// Comparateurs
compareContentModification,
evaluateAdversarialImprovement,
// Utilitaires contenu
cleanAdversarialContent,
validateAdversarialContent,
// Utilitaires techniques
chunkArray,
chunkArraySmart,
sleep,
// Métriques
calculateAntiDetectionScore,
getImprovementRecommendation
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/ContentGenerationAdversarial.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// ORCHESTRATEUR CONTENU ADVERSARIAL - NIVEAU 3
// Responsabilité: Pipeline complet de génération anti-détection
// Architecture: 4 étapes adversariales séparées et modulaires
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
// Importation des 4 étapes adversariales
const { generateInitialContentAdversarial } = require('./AdversarialInitialGeneration');
const { enhanceTechnicalTermsAdversarial } = require('./AdversarialTechnicalEnhancement');
const { enhanceTransitionsAdversarial } = require('./AdversarialTransitionEnhancement');
const { applyPersonalityStyleAdversarial } = require('./AdversarialStyleEnhancement');
// Importation du moteur adversarial
const { createAdversarialPrompt, getSupportedDetectors, analyzePromptEffectiveness } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - PIPELINE ADVERSARIAL COMPLET
* Input: { hierarchy, csvData, adversarialConfig, context }
* Output: { content, stats, debug, adversarialMetrics }
*/
async function generateWithAdversarialContext(input) {
return await tracer.run('ContentGenerationAdversarial.generateWithAdversarialContext()', async () => {
const { hierarchy, csvData, adversarialConfig = {}, context = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy !== false,
contextualMode: adversarialConfig.contextualMode !== false,
enableAllSteps: adversarialConfig.enableAllSteps !== false,
// Configuration par étape
steps: {
initial: adversarialConfig.steps?.initial !== false,
technical: adversarialConfig.steps?.technical !== false,
transitions: adversarialConfig.steps?.transitions !== false,
style: adversarialConfig.steps?.style !== false
},
...adversarialConfig
};
await tracer.annotate({
adversarialPipeline: true,
detectorTarget: config.detectorTarget,
intensity: config.intensity,
enabledSteps: Object.keys(config.steps).filter(k => config.steps[k]),
elementsCount: Object.keys(hierarchy).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 PIPELINE ADVERSARIAL NIVEAU 3: Anti-détection ${config.detectorTarget}`, 'INFO');
logSh(` 🎚️ Intensité: ${config.intensity.toFixed(2)} | Étapes: ${Object.keys(config.steps).filter(k => config.steps[k]).join(', ')}`, 'INFO');
// Initialiser manager détecteur global
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
try {
let currentContent = {};
let pipelineStats = {
steps: {},
totalDuration: 0,
elementsProcessed: 0,
adversarialMetrics: {
promptsGenerated: 0,
detectorTarget: config.detectorTarget,
averageIntensity: config.intensity,
effectivenessScore: 0
}
};
// ========================================
// ÉTAPE 1: GÉNÉRATION INITIALE ADVERSARIALE
// ========================================
if (config.steps.initial) {
logSh(`🎯 ÉTAPE 1/4: Génération initiale adversariale`, 'INFO');
const step1Result = await generateInitialContentAdversarial({
hierarchy,
csvData,
context,
adversarialConfig: config
});
currentContent = step1Result.content;
pipelineStats.steps.initial = step1Result.stats;
pipelineStats.adversarialMetrics.promptsGenerated += Object.keys(currentContent).length;
logSh(`✅ ÉTAPE 1/4: ${step1Result.stats.generated} éléments générés (${step1Result.stats.duration}ms)`, 'INFO');
} else {
logSh(`⏭️ ÉTAPE 1/4: Ignorée (configuration)`, 'INFO');
}
// ========================================
// ÉTAPE 2: ENHANCEMENT TECHNIQUE ADVERSARIAL
// ========================================
if (config.steps.technical && Object.keys(currentContent).length > 0) {
logSh(`🎯 ÉTAPE 2/4: Enhancement technique adversarial`, 'INFO');
const step2Result = await enhanceTechnicalTermsAdversarial({
content: currentContent,
csvData,
context,
adversarialConfig: config
});
currentContent = step2Result.content;
pipelineStats.steps.technical = step2Result.stats;
pipelineStats.adversarialMetrics.promptsGenerated += step2Result.stats.enhanced;
logSh(`✅ ÉTAPE 2/4: ${step2Result.stats.enhanced} éléments améliorés (${step2Result.stats.duration}ms)`, 'INFO');
} else {
logSh(`⏭️ ÉTAPE 2/4: Ignorée (configuration ou pas de contenu)`, 'INFO');
}
// ========================================
// ÉTAPE 3: ENHANCEMENT TRANSITIONS ADVERSARIAL
// ========================================
if (config.steps.transitions && Object.keys(currentContent).length > 0) {
logSh(`🎯 ÉTAPE 3/4: Enhancement transitions adversarial`, 'INFO');
const step3Result = await enhanceTransitionsAdversarial({
content: currentContent,
csvData,
context,
adversarialConfig: config
});
currentContent = step3Result.content;
pipelineStats.steps.transitions = step3Result.stats;
pipelineStats.adversarialMetrics.promptsGenerated += step3Result.stats.enhanced;
logSh(`✅ ÉTAPE 3/4: ${step3Result.stats.enhanced} éléments fluidifiés (${step3Result.stats.duration}ms)`, 'INFO');
} else {
logSh(`⏭️ ÉTAPE 3/4: Ignorée (configuration ou pas de contenu)`, 'INFO');
}
// ========================================
// ÉTAPE 4: ENHANCEMENT STYLE ADVERSARIAL
// ========================================
if (config.steps.style && Object.keys(currentContent).length > 0 && csvData.personality) {
logSh(`🎯 ÉTAPE 4/4: Enhancement style adversarial`, 'INFO');
const step4Result = await applyPersonalityStyleAdversarial({
content: currentContent,
csvData,
context,
adversarialConfig: config
});
currentContent = step4Result.content;
pipelineStats.steps.style = step4Result.stats;
pipelineStats.adversarialMetrics.promptsGenerated += step4Result.stats.enhanced;
logSh(`✅ ÉTAPE 4/4: ${step4Result.stats.enhanced} éléments stylisés (${step4Result.stats.duration}ms)`, 'INFO');
} else {
logSh(`⏭️ ÉTAPE 4/4: Ignorée (configuration, pas de contenu ou pas de personnalité)`, 'INFO');
}
// ========================================
// FINALISATION PIPELINE
// ========================================
const totalDuration = Date.now() - startTime;
pipelineStats.totalDuration = totalDuration;
pipelineStats.elementsProcessed = Object.keys(currentContent).length;
// Calculer score d'efficacité adversarial
pipelineStats.adversarialMetrics.effectivenessScore = calculateAdversarialEffectiveness(
pipelineStats,
config,
currentContent
);
logSh(`🎯 PIPELINE ADVERSARIAL TERMINÉ: ${pipelineStats.elementsProcessed} éléments (${totalDuration}ms)`, 'INFO');
logSh(` 📊 Score efficacité: ${pipelineStats.adversarialMetrics.effectivenessScore.toFixed(2)}%`, 'INFO');
await tracer.event(`Pipeline adversarial terminé`, {
...pipelineStats,
detectorTarget: config.detectorTarget,
intensity: config.intensity
});
return {
content: currentContent,
stats: pipelineStats,
debug: {
adversarialPipeline: true,
detectorTarget: config.detectorTarget,
intensity: config.intensity,
stepsExecuted: Object.keys(config.steps).filter(k => config.steps[k]),
detectorManager: detectorManager.getStrategyInfo()
},
adversarialMetrics: pipelineStats.adversarialMetrics
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ PIPELINE ADVERSARIAL ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`AdversarialContentGeneration failed: ${error.message}`);
}
}, input);
}
/**
* MODE SIMPLE ADVERSARIAL (équivalent à generateSimple mais adversarial)
*/
async function generateSimpleAdversarial(hierarchy, csvData, adversarialConfig = {}) {
return await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget: 'general',
intensity: 0.8,
enableAllSteps: false,
steps: {
initial: true,
technical: false,
transitions: false,
style: true
},
...adversarialConfig
}
});
}
/**
* MODE AVANCÉ ADVERSARIAL (configuration personnalisée)
*/
async function generateAdvancedAdversarial(hierarchy, csvData, options = {}) {
const {
detectorTarget = 'general',
intensity = 1.0,
technical = true,
transitions = true,
style = true,
...otherConfig
} = options;
return await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget,
intensity,
enableAdaptiveStrategy: true,
contextualMode: true,
steps: {
initial: true,
technical,
transitions,
style
},
...otherConfig
}
});
}
/**
* DIAGNOSTIC PIPELINE ADVERSARIAL
*/
async function diagnosticAdversarialPipeline(hierarchy, csvData, detectorTargets = ['general', 'gptZero', 'originality']) {
logSh(`🔬 DIAGNOSTIC ADVERSARIAL: Testing ${detectorTargets.length} détecteurs`, 'INFO');
const results = {};
for (const target of detectorTargets) {
try {
logSh(` 🎯 Test détecteur: ${target}`, 'DEBUG');
const result = await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget: target,
intensity: 1.0,
enableAllSteps: true
}
});
results[target] = {
success: true,
content: result.content,
stats: result.stats,
effectivenessScore: result.adversarialMetrics.effectivenessScore
};
logSh(`${target}: Score ${result.adversarialMetrics.effectivenessScore.toFixed(2)}%`, 'DEBUG');
} catch (error) {
results[target] = {
success: false,
error: error.message,
effectivenessScore: 0
};
logSh(`${target}: Échec - ${error.message}`, 'ERROR');
}
}
return results;
}
// ============= HELPER FUNCTIONS =============
/**
* Calculer efficacité adversariale
*/
function calculateAdversarialEffectiveness(pipelineStats, config, content) {
let effectiveness = 0;
// Base score selon intensité
effectiveness += config.intensity * 30;
// Bonus selon nombre d'étapes
const stepsExecuted = Object.keys(config.steps).filter(k => config.steps[k]).length;
effectiveness += stepsExecuted * 10;
// Bonus selon prompts adversariaux générés
const promptRatio = pipelineStats.adversarialMetrics.promptsGenerated / Math.max(1, pipelineStats.elementsProcessed);
effectiveness += promptRatio * 20;
// Analyse contenu si disponible
if (Object.keys(content).length > 0) {
const contentSample = Object.values(content).join(' ').substring(0, 1000);
const diversityScore = analyzeDiversityScore(contentSample);
effectiveness += diversityScore * 0.3;
}
return Math.min(100, Math.max(0, effectiveness));
}
/**
* Analyser score de diversité
*/
function analyzeDiversityScore(content) {
if (!content || typeof content !== 'string') return 0;
const words = content.split(/\s+/).filter(w => w.length > 2);
if (words.length === 0) return 0;
const uniqueWords = [...new Set(words.map(w => w.toLowerCase()))];
const diversityRatio = uniqueWords.length / words.length;
return diversityRatio * 100;
}
/**
* Obtenir informations détecteurs supportés
*/
function getAdversarialDetectorInfo() {
return getSupportedDetectors();
}
/**
* Comparer efficacité de différents détecteurs
*/
async function compareAdversarialStrategies(hierarchy, csvData, detectorTargets = ['general', 'gptZero', 'originality', 'winston']) {
const results = await diagnosticAdversarialPipeline(hierarchy, csvData, detectorTargets);
const comparison = {
bestStrategy: null,
bestScore: 0,
strategies: [],
averageScore: 0
};
let totalScore = 0;
let successCount = 0;
detectorTargets.forEach(target => {
const result = results[target];
if (result.success) {
const strategyInfo = {
detector: target,
effectivenessScore: result.effectivenessScore,
duration: result.stats.totalDuration,
elementsProcessed: result.stats.elementsProcessed
};
comparison.strategies.push(strategyInfo);
totalScore += result.effectivenessScore;
successCount++;
if (result.effectivenessScore > comparison.bestScore) {
comparison.bestStrategy = target;
comparison.bestScore = result.effectivenessScore;
}
}
});
comparison.averageScore = successCount > 0 ? totalScore / successCount : 0;
return comparison;
}
module.exports = {
generateWithAdversarialContext, // ← MAIN ENTRY POINT
generateSimpleAdversarial,
generateAdvancedAdversarial,
diagnosticAdversarialPipeline,
compareAdversarialStrategies,
getAdversarialDetectorInfo,
calculateAdversarialEffectiveness
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/ComparisonFramework.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FRAMEWORK DE COMPARAISON ADVERSARIAL
// Responsabilité: Comparer pipelines normales vs adversariales
// Utilisation: A/B testing et validation efficacité anti-détection
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
// Pipelines à comparer
const { generateWithContext } = require('../ContentGeneration'); // Pipeline normale
const { generateWithAdversarialContext, compareAdversarialStrategies } = require('./ContentGenerationAdversarial'); // Pipeline adversariale
/**
* MAIN ENTRY POINT - COMPARAISON A/B PIPELINE
* Compare pipeline normale vs adversariale sur même input
*/
async function compareNormalVsAdversarial(input, options = {}) {
return await tracer.run('ComparisonFramework.compareNormalVsAdversarial()', async () => {
const {
hierarchy,
csvData,
adversarialConfig = {},
runBothPipelines = true,
analyzeContent = true
} = input;
const {
detectorTarget = 'general',
intensity = 1.0,
iterations = 1
} = options;
await tracer.annotate({
comparisonType: 'normal_vs_adversarial',
detectorTarget,
intensity,
iterations,
elementsCount: Object.keys(hierarchy).length
});
const startTime = Date.now();
logSh(`🆚 COMPARAISON A/B: Pipeline normale vs adversariale`, 'INFO');
logSh(` 🎯 Détecteur cible: ${detectorTarget} | Intensité: ${intensity} | Itérations: ${iterations}`, 'INFO');
const results = {
normal: null,
adversarial: null,
comparison: null,
iterations: []
};
try {
for (let i = 0; i < iterations; i++) {
logSh(`🔄 Itération ${i + 1}/${iterations}`, 'INFO');
const iterationResults = {
iteration: i + 1,
normal: null,
adversarial: null,
metrics: {}
};
// ========================================
// PIPELINE NORMALE
// ========================================
if (runBothPipelines) {
logSh(` 📊 Génération pipeline normale...`, 'DEBUG');
const normalStartTime = Date.now();
try {
const normalResult = await generateWithContext(hierarchy, csvData, {
technical: true,
transitions: true,
style: true
});
iterationResults.normal = {
success: true,
content: normalResult,
duration: Date.now() - normalStartTime,
elementsCount: Object.keys(normalResult).length
};
logSh(` ✅ Pipeline normale: ${iterationResults.normal.elementsCount} éléments (${iterationResults.normal.duration}ms)`, 'DEBUG');
} catch (error) {
iterationResults.normal = {
success: false,
error: error.message,
duration: Date.now() - normalStartTime
};
logSh(` ❌ Pipeline normale échouée: ${error.message}`, 'ERROR');
}
}
// ========================================
// PIPELINE ADVERSARIALE
// ========================================
logSh(` 🎯 Génération pipeline adversariale...`, 'DEBUG');
const adversarialStartTime = Date.now();
try {
const adversarialResult = await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget,
intensity,
enableAllSteps: true,
...adversarialConfig
}
});
iterationResults.adversarial = {
success: true,
content: adversarialResult.content,
stats: adversarialResult.stats,
adversarialMetrics: adversarialResult.adversarialMetrics,
duration: Date.now() - adversarialStartTime,
elementsCount: Object.keys(adversarialResult.content).length
};
logSh(` ✅ Pipeline adversariale: ${iterationResults.adversarial.elementsCount} éléments (${iterationResults.adversarial.duration}ms)`, 'DEBUG');
logSh(` 📊 Score efficacité: ${adversarialResult.adversarialMetrics.effectivenessScore.toFixed(2)}%`, 'DEBUG');
} catch (error) {
iterationResults.adversarial = {
success: false,
error: error.message,
duration: Date.now() - adversarialStartTime
};
logSh(` ❌ Pipeline adversariale échouée: ${error.message}`, 'ERROR');
}
// ========================================
// ANALYSE COMPARATIVE ITÉRATION
// ========================================
if (analyzeContent && iterationResults.normal?.success && iterationResults.adversarial?.success) {
iterationResults.metrics = analyzeContentComparison(
iterationResults.normal.content,
iterationResults.adversarial.content
);
logSh(` 📈 Diversité: Normal=${iterationResults.metrics.diversity.normal.toFixed(2)}% | Adversarial=${iterationResults.metrics.diversity.adversarial.toFixed(2)}%`, 'DEBUG');
}
results.iterations.push(iterationResults);
}
// ========================================
// CONSOLIDATION RÉSULTATS
// ========================================
const totalDuration = Date.now() - startTime;
// Prendre les meilleurs résultats ou derniers si une seule itération
const lastIteration = results.iterations[results.iterations.length - 1];
results.normal = lastIteration.normal;
results.adversarial = lastIteration.adversarial;
// Analyse comparative globale
results.comparison = generateGlobalComparison(results.iterations, options);
logSh(`🆚 COMPARAISON TERMINÉE: ${iterations} itérations (${totalDuration}ms)`, 'INFO');
if (results.comparison.winner) {
logSh(`🏆 Gagnant: ${results.comparison.winner} (score: ${results.comparison.bestScore.toFixed(2)})`, 'INFO');
}
await tracer.event('Comparaison A/B terminée', {
iterations,
winner: results.comparison.winner,
totalDuration
});
return results;
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ COMPARAISON A/B ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`ComparisonFramework failed: ${error.message}`);
}
}, input);
}
/**
* COMPARAISON MULTI-DÉTECTEURS
*/
async function compareMultiDetectors(hierarchy, csvData, detectorTargets = ['general', 'gptZero', 'originality']) {
logSh(`🎯 COMPARAISON MULTI-DÉTECTEURS: ${detectorTargets.length} stratégies`, 'INFO');
const results = {};
const startTime = Date.now();
for (const detector of detectorTargets) {
logSh(` 🔍 Test détecteur: ${detector}`, 'DEBUG');
try {
const comparison = await compareNormalVsAdversarial({
hierarchy,
csvData,
adversarialConfig: { detectorTarget: detector }
}, {
detectorTarget: detector,
intensity: 1.0,
iterations: 1
});
results[detector] = {
success: true,
comparison,
effectivenessGain: comparison.adversarial?.adversarialMetrics?.effectivenessScore || 0
};
logSh(`${detector}: +${results[detector].effectivenessGain.toFixed(2)}% efficacité`, 'DEBUG');
} catch (error) {
results[detector] = {
success: false,
error: error.message,
effectivenessGain: 0
};
logSh(`${detector}: Échec - ${error.message}`, 'ERROR');
}
}
// Analyse du meilleur détecteur
const bestDetector = Object.keys(results).reduce((best, current) => {
if (!results[best]?.success) return current;
if (!results[current]?.success) return best;
return results[current].effectivenessGain > results[best].effectivenessGain ? current : best;
});
const totalDuration = Date.now() - startTime;
logSh(`🎯 MULTI-DÉTECTEURS TERMINÉ: Meilleur=${bestDetector} (${totalDuration}ms)`, 'INFO');
return {
results,
bestDetector,
bestScore: results[bestDetector]?.effectivenessGain || 0,
totalDuration
};
}
/**
* BENCHMARK PERFORMANCE
*/
async function benchmarkPerformance(hierarchy, csvData, configurations = []) {
const defaultConfigs = [
{ name: 'Normal', type: 'normal' },
{ name: 'Simple Adversarial', type: 'adversarial', detectorTarget: 'general', intensity: 0.5 },
{ name: 'Intense Adversarial', type: 'adversarial', detectorTarget: 'gptZero', intensity: 1.0 },
{ name: 'Max Adversarial', type: 'adversarial', detectorTarget: 'originality', intensity: 1.5 }
];
const configs = configurations.length > 0 ? configurations : defaultConfigs;
logSh(`⚡ BENCHMARK PERFORMANCE: ${configs.length} configurations`, 'INFO');
const results = [];
for (const config of configs) {
logSh(` 🔧 Test: ${config.name}`, 'DEBUG');
const startTime = Date.now();
try {
let result;
if (config.type === 'normal') {
result = await generateWithContext(hierarchy, csvData);
} else {
const adversarialResult = await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget: config.detectorTarget || 'general',
intensity: config.intensity || 1.0
}
});
result = adversarialResult.content;
}
const duration = Date.now() - startTime;
results.push({
name: config.name,
type: config.type,
success: true,
duration,
elementsCount: Object.keys(result).length,
performance: Object.keys(result).length / (duration / 1000) // éléments par seconde
});
logSh(`${config.name}: ${Object.keys(result).length} éléments (${duration}ms)`, 'DEBUG');
} catch (error) {
results.push({
name: config.name,
type: config.type,
success: false,
error: error.message,
duration: Date.now() - startTime
});
logSh(`${config.name}: Échec - ${error.message}`, 'ERROR');
}
}
// Analyser les résultats
const successfulResults = results.filter(r => r.success);
const fastest = successfulResults.reduce((best, current) =>
current.duration < best.duration ? current : best, successfulResults[0]);
const mostEfficient = successfulResults.reduce((best, current) =>
current.performance > best.performance ? current : best, successfulResults[0]);
logSh(`⚡ BENCHMARK TERMINÉ: Fastest=${fastest?.name} | Most efficient=${mostEfficient?.name}`, 'INFO');
return {
results,
fastest,
mostEfficient,
summary: {
totalConfigs: configs.length,
successful: successfulResults.length,
failed: results.length - successfulResults.length
}
};
}
// ============= HELPER FUNCTIONS =============
/**
* Analyser différences de contenu entre normal et adversarial
*/
function analyzeContentComparison(normalContent, adversarialContent) {
const metrics = {
diversity: {
normal: analyzeDiversityScore(Object.values(normalContent).join(' ')),
adversarial: analyzeDiversityScore(Object.values(adversarialContent).join(' '))
},
length: {
normal: Object.values(normalContent).join(' ').length,
adversarial: Object.values(adversarialContent).join(' ').length
},
elementsCount: {
normal: Object.keys(normalContent).length,
adversarial: Object.keys(adversarialContent).length
},
differences: compareContentElements(normalContent, adversarialContent)
};
return metrics;
}
/**
* Score de diversité lexicale
*/
function analyzeDiversityScore(content) {
if (!content || typeof content !== 'string') return 0;
const words = content.split(/\s+/).filter(w => w.length > 2);
if (words.length === 0) return 0;
const uniqueWords = [...new Set(words.map(w => w.toLowerCase()))];
return (uniqueWords.length / words.length) * 100;
}
/**
* Comparer éléments de contenu
*/
function compareContentElements(normalContent, adversarialContent) {
const differences = {
modified: 0,
identical: 0,
totalElements: Math.max(Object.keys(normalContent).length, Object.keys(adversarialContent).length)
};
const allTags = [...new Set([...Object.keys(normalContent), ...Object.keys(adversarialContent)])];
allTags.forEach(tag => {
if (normalContent[tag] && adversarialContent[tag]) {
if (normalContent[tag] === adversarialContent[tag]) {
differences.identical++;
} else {
differences.modified++;
}
}
});
differences.modificationRate = differences.totalElements > 0 ?
(differences.modified / differences.totalElements) * 100 : 0;
return differences;
}
/**
* Générer analyse comparative globale
*/
function generateGlobalComparison(iterations, options) {
const successfulIterations = iterations.filter(it =>
it.normal?.success && it.adversarial?.success);
if (successfulIterations.length === 0) {
return {
winner: null,
bestScore: 0,
summary: 'Aucune itération réussie'
};
}
// Moyenner les métriques
const avgMetrics = {
diversity: {
normal: 0,
adversarial: 0
},
performance: {
normal: 0,
adversarial: 0
}
};
successfulIterations.forEach(iteration => {
if (iteration.metrics) {
avgMetrics.diversity.normal += iteration.metrics.diversity.normal;
avgMetrics.diversity.adversarial += iteration.metrics.diversity.adversarial;
}
avgMetrics.performance.normal += iteration.normal.elementsCount / (iteration.normal.duration / 1000);
avgMetrics.performance.adversarial += iteration.adversarial.elementsCount / (iteration.adversarial.duration / 1000);
});
const iterCount = successfulIterations.length;
avgMetrics.diversity.normal /= iterCount;
avgMetrics.diversity.adversarial /= iterCount;
avgMetrics.performance.normal /= iterCount;
avgMetrics.performance.adversarial /= iterCount;
// Déterminer le gagnant
const diversityGain = avgMetrics.diversity.adversarial - avgMetrics.diversity.normal;
const performanceLoss = avgMetrics.performance.normal - avgMetrics.performance.adversarial;
// Score composite (favorise diversité avec pénalité performance)
const adversarialScore = diversityGain * 2 - (performanceLoss * 0.5);
return {
winner: adversarialScore > 5 ? 'adversarial' : 'normal',
bestScore: Math.max(avgMetrics.diversity.normal, avgMetrics.diversity.adversarial),
diversityGain,
performanceLoss,
avgMetrics,
summary: `Diversité: +${diversityGain.toFixed(2)}%, Performance: ${performanceLoss > 0 ? '-' : '+'}${Math.abs(performanceLoss).toFixed(2)} elem/s`
};
}
module.exports = {
compareNormalVsAdversarial, // ← MAIN ENTRY POINT
compareMultiDetectors,
benchmarkPerformance,
analyzeContentComparison,
analyzeDiversityScore
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/adversarial-generation/demo-modulaire.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// DÉMONSTRATION ARCHITECTURE MODULAIRE
// Usage: node lib/adversarial-generation/demo-modulaire.js
// Objectif: Valider l'intégration modulaire adversariale
// ========================================
const { logSh } = require('../ErrorReporting');
// Import modules adversariaux modulaires
const { applyAdversarialLayer } = require('./AdversarialCore');
const {
applyPredefinedStack,
applyAdaptiveLayers,
getAvailableStacks
} = require('./AdversarialLayers');
const { calculateAntiDetectionScore, evaluateAdversarialImprovement } = require('./AdversarialUtils');
/**
* EXEMPLE D'UTILISATION MODULAIRE
*/
async function demoModularAdversarial() {
console.log('\n🎯 === DÉMONSTRATION ADVERSARIAL MODULAIRE ===\n');
// Contenu d'exemple (simulé contenu généré normal)
const exempleContenu = {
'|Titre_Principal_1|': 'Guide complet pour choisir votre plaque personnalisée',
'|Introduction_1|': 'La personnalisation d\'une plaque signalétique représente un enjeu optimal pour votre entreprise. Cette solution comprehensive permet de créer une identité visuelle robuste et seamless.',
'|Texte_1|': 'Il est important de noter que les matériaux utilisés sont cutting-edge. Par ailleurs, la qualité est optimal. En effet, nos solutions sont comprehensive et robust.',
'|FAQ_Question_1|': 'Quels sont les matériaux disponibles ?',
'|FAQ_Reponse_1|': 'Nos matériaux sont optimal : dibond, aluminium, PMMA. Ces solutions comprehensive garantissent une qualité robust et seamless.'
};
console.log('📊 CONTENU ORIGINAL:');
Object.entries(exempleContenu).forEach(([tag, content]) => {
console.log(` ${tag}: "${content.substring(0, 60)}..."`);
});
// Analyser contenu original
const scoreOriginal = calculateAntiDetectionScore(Object.values(exempleContenu).join(' '));
console.log(`\n📈 Score anti-détection original: ${scoreOriginal}/100`);
try {
// ========================================
// TEST 1: COUCHE SIMPLE
// ========================================
console.log('\n🔧 TEST 1: Application couche adversariale simple');
const result1 = await applyAdversarialLayer(exempleContenu, {
detectorTarget: 'general',
intensity: 0.8,
method: 'enhancement'
});
console.log(`✅ Résultat: ${result1.stats.elementsModified}/${result1.stats.elementsProcessed} éléments modifiés`);
const scoreAmeliore = calculateAntiDetectionScore(Object.values(result1.content).join(' '));
console.log(`📈 Score anti-détection amélioré: ${scoreAmeliore}/100 (+${scoreAmeliore - scoreOriginal})`);
// ========================================
// TEST 2: STACK PRÉDÉFINI
// ========================================
console.log('\n📦 TEST 2: Application stack prédéfini');
// Lister stacks disponibles
const stacks = getAvailableStacks();
console.log(' Stacks disponibles:');
stacks.forEach(stack => {
console.log(` - ${stack.name}: ${stack.description} (${stack.layersCount} couches)`);
});
const result2 = await applyPredefinedStack(exempleContenu, 'standardDefense', {
csvData: {
personality: { nom: 'Marc', style: 'technique' },
mc0: 'plaque personnalisée'
}
});
console.log(`✅ Stack standard: ${result2.stats.totalModifications} modifications totales`);
console.log(` 📊 Couches appliquées: ${result2.stats.layers.filter(l => l.success).length}/${result2.stats.layers.length}`);
const scoreStack = calculateAntiDetectionScore(Object.values(result2.content).join(' '));
console.log(`📈 Score anti-détection stack: ${scoreStack}/100 (+${scoreStack - scoreOriginal})`);
// ========================================
// TEST 3: COUCHES ADAPTATIVES
// ========================================
console.log('\n🧠 TEST 3: Application couches adaptatives');
const result3 = await applyAdaptiveLayers(exempleContenu, {
targetDetectors: ['gptZero', 'originality'],
maxIntensity: 1.2
});
if (result3.stats.adaptive) {
console.log(`✅ Adaptatif: ${result3.stats.layersApplied || result3.stats.totalModifications} modifications`);
const scoreAdaptatif = calculateAntiDetectionScore(Object.values(result3.content).join(' '));
console.log(`📈 Score anti-détection adaptatif: ${scoreAdaptatif}/100 (+${scoreAdaptatif - scoreOriginal})`);
}
// ========================================
// COMPARAISON FINALE
// ========================================
console.log('\n📊 COMPARAISON FINALE:');
const evaluation = evaluateAdversarialImprovement(
Object.values(exempleContenu).join(' '),
Object.values(result2.content).join(' '),
'general'
);
console.log(` 🔹 Réduction empreintes IA: ${evaluation.fingerprintReduction.toFixed(2)}%`);
console.log(` 🔹 Augmentation diversité: ${evaluation.diversityIncrease.toFixed(2)}%`);
console.log(` 🔹 Amélioration variation: ${evaluation.variationIncrease.toFixed(2)}%`);
console.log(` 🔹 Score amélioration global: ${evaluation.improvementScore}`);
console.log(` 🔹 Taux modification: ${evaluation.modificationRate.toFixed(2)}%`);
console.log(` 💡 Recommandation: ${evaluation.recommendation}`);
// ========================================
// EXEMPLES DE CONTENU TRANSFORMÉ
// ========================================
console.log('\n✨ EXEMPLES DE TRANSFORMATION:');
const exempleTransforme = result2.content['|Introduction_1|'] || result1.content['|Introduction_1|'];
console.log('\n📝 AVANT:');
console.log(` "${exempleContenu['|Introduction_1|']}"`);
console.log('\n📝 APRÈS:');
console.log(` "${exempleTransforme}"`);
console.log('\n✅ === DÉMONSTRATION MODULAIRE TERMINÉE ===\n');
return {
success: true,
originalScore: scoreOriginal,
improvedScore: Math.max(scoreAmeliore, scoreStack),
improvement: evaluation.improvementScore
};
} catch (error) {
console.error('\n❌ ERREUR DÉMONSTRATION:', error.message);
return { success: false, error: error.message };
}
}
/**
* EXEMPLE D'INTÉGRATION AVEC PIPELINE NORMALE
*/
async function demoIntegrationPipeline() {
console.log('\n🔗 === DÉMONSTRATION INTÉGRATION PIPELINE ===\n');
// Simuler résultat pipeline normale (Level 1)
const contenuNormal = {
'|Titre_H1_1|': 'Solutions de plaques personnalisées professionnelles',
'|Intro_1|': 'Notre expertise en signalétique permet de créer des plaques sur mesure adaptées à vos besoins spécifiques.',
'|Texte_1|': 'Les matériaux proposés incluent l\'aluminium, le dibond et le PMMA. Chaque solution présente des avantages particuliers selon l\'usage prévu.'
};
console.log('💼 SCÉNARIO: Application adversarial post-pipeline normale');
try {
// Exemple Level 6 - Post-processing adversarial
console.log('\n🎯 Étape 1: Contenu généré par pipeline normale');
console.log(' ✅ Contenu de base: qualité préservée');
console.log('\n🎯 Étape 2: Application couche adversariale modulaire');
const resultAdversarial = await applyAdversarialLayer(contenuNormal, {
detectorTarget: 'gptZero',
intensity: 0.9,
method: 'hybrid',
preserveStructure: true
});
console.log(` ✅ Couche adversariale: ${resultAdversarial.stats.elementsModified} éléments modifiés`);
console.log('\n📊 RÉSULTAT FINAL:');
Object.entries(resultAdversarial.content).forEach(([tag, content]) => {
console.log(` ${tag}:`);
console.log(` AVANT: "${contenuNormal[tag]}"`);
console.log(` APRÈS: "${content}"`);
console.log('');
});
return { success: true, result: resultAdversarial };
} catch (error) {
console.error('❌ ERREUR INTÉGRATION:', error.message);
return { success: false, error: error.message };
}
}
// Exécuter démonstrations si fichier appelé directement
if (require.main === module) {
(async () => {
await demoModularAdversarial();
await demoIntegrationPipeline();
})().catch(console.error);
}
module.exports = {
demoModularAdversarial,
demoIntegrationPipeline
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/SelectiveUtils.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// SELECTIVE UTILS - UTILITAIRES MODULAIRES
// Responsabilité: Fonctions utilitaires partagées par tous les modules selective
// Architecture: Helper functions réutilisables et composables
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* ANALYSEURS DE CONTENU SELECTIVE
*/
/**
* Analyser qualité technique d'un contenu
*/
function analyzeTechnicalQuality(content, contextualTerms = []) {
if (!content || typeof content !== 'string') return { score: 0, details: {} };
const analysis = {
score: 0,
details: {
technicalTermsFound: 0,
technicalTermsExpected: contextualTerms.length,
genericWordsCount: 0,
hasSpecifications: false,
hasDimensions: false,
contextIntegration: 0
}
};
const lowerContent = content.toLowerCase();
// 1. Compter termes techniques présents
contextualTerms.forEach(term => {
if (lowerContent.includes(term.toLowerCase())) {
analysis.details.technicalTermsFound++;
}
});
// 2. Détecter mots génériques
const genericWords = ['produit', 'solution', 'service', 'offre', 'article', 'élément'];
analysis.details.genericWordsCount = genericWords.filter(word =>
lowerContent.includes(word)
).length;
// 3. Vérifier spécifications techniques
analysis.details.hasSpecifications = /\b(norme|iso|din|ce)\b/i.test(content);
// 4. Vérifier dimensions/données techniques
analysis.details.hasDimensions = /\d+\s*(mm|cm|m|%|°|kg|g)\b/i.test(content);
// 5. Calculer score global (0-100)
const termRatio = contextualTerms.length > 0 ?
(analysis.details.technicalTermsFound / contextualTerms.length) * 40 : 20;
const genericPenalty = Math.min(20, analysis.details.genericWordsCount * 5);
const specificationBonus = analysis.details.hasSpecifications ? 15 : 0;
const dimensionBonus = analysis.details.hasDimensions ? 15 : 0;
const lengthBonus = content.length > 100 ? 10 : 0;
analysis.score = Math.max(0, Math.min(100,
termRatio + specificationBonus + dimensionBonus + lengthBonus - genericPenalty
));
return analysis;
}
/**
* Analyser fluidité des transitions
*/
function analyzeTransitionFluidity(content) {
if (!content || typeof content !== 'string') return { score: 0, details: {} };
const sentences = content.split(/[.!?]+/)
.map(s => s.trim())
.filter(s => s.length > 5);
if (sentences.length < 2) {
return { score: 100, details: { reason: 'Contenu trop court pour analyse transitions' } };
}
const analysis = {
score: 0,
details: {
sentencesCount: sentences.length,
connectorsFound: 0,
repetitiveConnectors: 0,
abruptTransitions: 0,
averageSentenceLength: 0,
lengthVariation: 0
}
};
// 1. Analyser connecteurs
const commonConnectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite'];
const connectorCounts = {};
commonConnectors.forEach(connector => {
const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
connectorCounts[connector] = matches.length;
analysis.details.connectorsFound += matches.length;
if (matches.length > 1) analysis.details.repetitiveConnectors++;
});
// 2. Détecter transitions abruptes
for (let i = 1; i < sentences.length; i++) {
const sentence = sentences[i].toLowerCase().trim();
const hasConnector = commonConnectors.some(connector =>
sentence.startsWith(connector) || sentence.includes(` ${connector} `)
);
if (!hasConnector && sentence.length > 20) {
analysis.details.abruptTransitions++;
}
}
// 3. Analyser variation de longueur
const lengths = sentences.map(s => s.split(/\s+/).length);
analysis.details.averageSentenceLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) =>
acc + Math.pow(len - analysis.details.averageSentenceLength, 2), 0
) / lengths.length;
analysis.details.lengthVariation = Math.sqrt(variance);
// 4. Calculer score fluidité (0-100)
const connectorScore = Math.min(30, (analysis.details.connectorsFound / sentences.length) * 100);
const repetitionPenalty = Math.min(20, analysis.details.repetitiveConnectors * 5);
const abruptPenalty = Math.min(30, (analysis.details.abruptTransitions / sentences.length) * 50);
const variationScore = Math.min(20, analysis.details.lengthVariation * 2);
analysis.score = Math.max(0, Math.min(100,
connectorScore + variationScore - repetitionPenalty - abruptPenalty + 50
));
return analysis;
}
/**
* Analyser cohérence de style
*/
function analyzeStyleConsistency(content, expectedPersonality = null) {
if (!content || typeof content !== 'string') return { score: 0, details: {} };
const analysis = {
score: 0,
details: {
personalityAlignment: 0,
toneConsistency: 0,
vocabularyLevel: 'standard',
formalityScore: 0,
personalityWordsFound: 0
}
};
// 1. Analyser alignement personnalité
if (expectedPersonality && expectedPersonality.vocabulairePref) {
const personalityWords = expectedPersonality.vocabulairePref.toLowerCase().split(',');
const contentLower = content.toLowerCase();
personalityWords.forEach(word => {
if (word.trim() && contentLower.includes(word.trim())) {
analysis.details.personalityWordsFound++;
}
});
analysis.details.personalityAlignment = personalityWords.length > 0 ?
(analysis.details.personalityWordsFound / personalityWords.length) * 100 : 0;
}
// 2. Analyser niveau vocabulaire
const technicalWords = content.match(/\b\w{8,}\b/g) || [];
const totalWords = content.split(/\s+/).length;
const techRatio = technicalWords.length / totalWords;
if (techRatio > 0.15) analysis.details.vocabularyLevel = 'expert';
else if (techRatio < 0.05) analysis.details.vocabularyLevel = 'accessible';
else analysis.details.vocabularyLevel = 'standard';
// 3. Analyser formalité
const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois'];
const casualIndicators = ['du coup', 'sympa', 'cool', 'nickel'];
let formalCount = formalIndicators.filter(indicator =>
content.toLowerCase().includes(indicator)
).length;
let casualCount = casualIndicators.filter(indicator =>
content.toLowerCase().includes(indicator)
).length;
analysis.details.formalityScore = formalCount - casualCount; // Positif = formel, négatif = casual
// 4. Calculer score cohérence (0-100)
let baseScore = 50;
if (expectedPersonality) {
baseScore += analysis.details.personalityAlignment * 0.3;
// Ajustements selon niveau technique attendu
const expectedLevel = expectedPersonality.niveauTechnique || 'standard';
if (expectedLevel === analysis.details.vocabularyLevel) {
baseScore += 20;
} else {
baseScore -= 10;
}
}
// Bonus cohérence tonale
const sentences = content.split(/[.!?]+/).filter(s => s.length > 10);
if (sentences.length > 1) {
baseScore += Math.min(20, analysis.details.lengthVariation || 10);
}
analysis.score = Math.max(0, Math.min(100, baseScore));
return analysis;
}
/**
* COMPARATEURS ET MÉTRIQUES
*/
/**
* Comparer deux contenus et calculer taux amélioration
*/
function compareContentImprovement(original, enhanced, analysisType = 'general') {
if (!original || !enhanced) return { improvementRate: 0, details: {} };
const comparison = {
improvementRate: 0,
details: {
lengthChange: ((enhanced.length - original.length) / original.length) * 100,
wordCountChange: 0,
structuralChanges: 0,
contentPreserved: true
}
};
// 1. Analyser changements structurels
const originalSentences = original.split(/[.!?]+/).length;
const enhancedSentences = enhanced.split(/[.!?]+/).length;
comparison.details.structuralChanges = Math.abs(enhancedSentences - originalSentences);
// 2. Analyser changements de mots
const originalWords = original.toLowerCase().split(/\s+/).filter(w => w.length > 2);
const enhancedWords = enhanced.toLowerCase().split(/\s+/).filter(w => w.length > 2);
comparison.details.wordCountChange = enhancedWords.length - originalWords.length;
// 3. Vérifier préservation du contenu principal
const originalKeyWords = originalWords.filter(w => w.length > 4);
const preservedWords = originalKeyWords.filter(w => enhanced.toLowerCase().includes(w));
comparison.details.contentPreserved = (preservedWords.length / originalKeyWords.length) > 0.7;
// 4. Calculer taux amélioration selon type d'analyse
switch (analysisType) {
case 'technical':
const originalTech = analyzeTechnicalQuality(original);
const enhancedTech = analyzeTechnicalQuality(enhanced);
comparison.improvementRate = enhancedTech.score - originalTech.score;
break;
case 'transitions':
const originalFluid = analyzeTransitionFluidity(original);
const enhancedFluid = analyzeTransitionFluidity(enhanced);
comparison.improvementRate = enhancedFluid.score - originalFluid.score;
break;
case 'style':
const originalStyle = analyzeStyleConsistency(original);
const enhancedStyle = analyzeStyleConsistency(enhanced);
comparison.improvementRate = enhancedStyle.score - originalStyle.score;
break;
default:
// Amélioration générale (moyenne pondérée)
comparison.improvementRate = Math.min(50, Math.abs(comparison.details.lengthChange) * 0.1 +
(comparison.details.contentPreserved ? 20 : -20) +
Math.min(15, Math.abs(comparison.details.wordCountChange)));
}
return comparison;
}
/**
* UTILITAIRES DE CONTENU
*/
/**
* Nettoyer contenu généré par LLM
*/
function cleanGeneratedContent(content, cleaningLevel = 'standard') {
if (!content || typeof content !== 'string') return content;
let cleaned = content.trim();
// Nettoyage de base
cleaned = cleaned.replace(/^(voici\s+)?le\s+contenu\s+(amélioré|modifié|réécrit)[:\s]*/gi, '');
cleaned = cleaned.replace(/^(bon,?\s*)?(alors,?\s*)?(voici\s+)?/gi, '');
cleaned = cleaned.replace(/^(avec\s+les?\s+)?améliorations?\s*[:\s]*/gi, '');
// Nettoyage formatage
cleaned = cleaned.replace(/\*\*([^*]+)\*\*/g, '$1'); // Gras markdown → texte normal
cleaned = cleaned.replace(/\s{2,}/g, ' '); // Espaces multiples
cleaned = cleaned.replace(/([.!?])\s*([.!?])/g, '$1 '); // Double ponctuation
if (cleaningLevel === 'intensive') {
// Nettoyage intensif
cleaned = cleaned.replace(/^\s*[-*+]\s*/gm, ''); // Puces en début de ligne
cleaned = cleaned.replace(/^(pour\s+)?(ce\s+)?(contenu\s*)?[,:]?\s*/gi, '');
cleaned = cleaned.replace(/\([^)]*\)/g, ''); // Parenthèses et contenu
}
// Nettoyage final
cleaned = cleaned.replace(/^[,.\s]+/, ''); // Début
cleaned = cleaned.replace(/[,\s]+$/, ''); // Fin
cleaned = cleaned.trim();
return cleaned;
}
/**
* Valider contenu selective
*/
function validateSelectiveContent(content, originalContent, criteria = {}) {
const validation = {
isValid: true,
score: 0,
issues: [],
suggestions: []
};
const {
minLength = 20,
maxLengthChange = 50, // % de changement maximum
preserveContent = true,
checkTechnicalTerms = true
} = criteria;
// 1. Vérifier longueur
if (!content || content.length < minLength) {
validation.isValid = false;
validation.issues.push('Contenu trop court');
validation.suggestions.push('Augmenter la longueur du contenu généré');
} else {
validation.score += 25;
}
// 2. Vérifier changements de longueur
if (originalContent) {
const lengthChange = Math.abs((content.length - originalContent.length) / originalContent.length) * 100;
if (lengthChange > maxLengthChange) {
validation.issues.push('Changement de longueur excessif');
validation.suggestions.push('Réduire l\'intensité d\'amélioration');
} else {
validation.score += 25;
}
// 3. Vérifier préservation du contenu
if (preserveContent) {
const preservation = compareContentImprovement(originalContent, content);
if (!preservation.details.contentPreserved) {
validation.isValid = false;
validation.issues.push('Contenu original non préservé');
validation.suggestions.push('Améliorer conservation du sens original');
} else {
validation.score += 25;
}
}
}
// 4. Vérifications spécifiques
if (checkTechnicalTerms) {
const technicalQuality = analyzeTechnicalQuality(content);
if (technicalQuality.score > 60) {
validation.score += 25;
} else if (technicalQuality.score < 30) {
validation.issues.push('Qualité technique insuffisante');
validation.suggestions.push('Ajouter plus de termes techniques spécialisés');
}
}
// Score final et validation
validation.score = Math.min(100, validation.score);
validation.isValid = validation.isValid && validation.score >= 60;
return validation;
}
/**
* UTILITAIRES TECHNIQUES
*/
/**
* Chunk array avec gestion intelligente
*/
function chunkArray(array, chunkSize, smartChunking = false) {
if (!Array.isArray(array)) return [];
if (array.length <= chunkSize) return [array];
const chunks = [];
if (smartChunking) {
// Chunking intelligent : éviter de séparer éléments liés
let currentChunk = [];
for (let i = 0; i < array.length; i++) {
currentChunk.push(array[i]);
// Conditions de fin de chunk intelligente
const isChunkFull = currentChunk.length >= chunkSize;
const isLastElement = i === array.length - 1;
const nextElementRelated = i < array.length - 1 &&
array[i].tag && array[i + 1].tag &&
array[i].tag.includes('FAQ') && array[i + 1].tag.includes('FAQ');
if ((isChunkFull && !nextElementRelated) || isLastElement) {
chunks.push([...currentChunk]);
currentChunk = [];
}
}
// Ajouter chunk restant si non vide
if (currentChunk.length > 0) {
if (chunks.length > 0 && chunks[chunks.length - 1].length + currentChunk.length <= chunkSize * 1.2) {
// Merger avec dernier chunk si pas trop gros
chunks[chunks.length - 1].push(...currentChunk);
} else {
chunks.push(currentChunk);
}
}
} else {
// Chunking standard
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
}
return chunks;
}
/**
* Sleep avec logging optionnel
*/
async function sleep(ms, logMessage = null) {
if (logMessage) {
logSh(`${logMessage} (${ms}ms)`, 'DEBUG');
}
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Mesurer performance d'opération
*/
function measurePerformance(operationName, startTime = Date.now()) {
const endTime = Date.now();
const duration = endTime - startTime;
const performance = {
operationName,
startTime,
endTime,
duration,
durationFormatted: formatDuration(duration)
};
return performance;
}
/**
* Formater durée en format lisible
*/
function formatDuration(ms) {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
}
/**
* STATISTIQUES ET RAPPORTS
*/
/**
* Générer rapport amélioration
*/
function generateImprovementReport(originalContent, enhancedContent, layerType = 'general') {
const report = {
layerType,
timestamp: new Date().toISOString(),
summary: {
elementsProcessed: 0,
elementsImproved: 0,
averageImprovement: 0,
totalExecutionTime: 0
},
details: {
byElement: [],
qualityMetrics: {},
recommendations: []
}
};
// Analyser chaque élément
Object.keys(originalContent).forEach(tag => {
const original = originalContent[tag];
const enhanced = enhancedContent[tag];
if (original && enhanced) {
report.summary.elementsProcessed++;
const improvement = compareContentImprovement(original, enhanced, layerType);
if (improvement.improvementRate > 0) {
report.summary.elementsImproved++;
}
report.summary.averageImprovement += improvement.improvementRate;
report.details.byElement.push({
tag,
improvementRate: improvement.improvementRate,
lengthChange: improvement.details.lengthChange,
contentPreserved: improvement.details.contentPreserved
});
}
});
// Calculer moyennes
if (report.summary.elementsProcessed > 0) {
report.summary.averageImprovement = report.summary.averageImprovement / report.summary.elementsProcessed;
}
// Métriques qualité globales
const fullOriginal = Object.values(originalContent).join(' ');
const fullEnhanced = Object.values(enhancedContent).join(' ');
report.details.qualityMetrics = {
technical: analyzeTechnicalQuality(fullEnhanced),
transitions: analyzeTransitionFluidity(fullEnhanced),
style: analyzeStyleConsistency(fullEnhanced)
};
// Recommandations
if (report.summary.averageImprovement < 10) {
report.details.recommendations.push('Augmenter l\'intensité d\'amélioration');
}
if (report.details.byElement.some(e => !e.contentPreserved)) {
report.details.recommendations.push('Améliorer préservation du contenu original');
}
return report;
}
module.exports = {
// Analyseurs
analyzeTechnicalQuality,
analyzeTransitionFluidity,
analyzeStyleConsistency,
// Comparateurs
compareContentImprovement,
// Utilitaires contenu
cleanGeneratedContent,
validateSelectiveContent,
// Utilitaires techniques
chunkArray,
sleep,
measurePerformance,
formatDuration,
// Rapports
generateImprovementReport
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/TechnicalLayer.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// TECHNICAL LAYER - COUCHE TECHNIQUE MODULAIRE
// Responsabilité: Amélioration technique modulaire réutilisable
// LLM: GPT-4o-mini (précision technique optimale)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { chunkArray, sleep } = require('./SelectiveUtils');
/**
* COUCHE TECHNIQUE MODULAIRE
*/
class TechnicalLayer {
constructor() {
this.name = 'TechnicalEnhancement';
this.defaultLLM = 'openai';
this.priority = 1; // Haute priorité - appliqué en premier généralement
}
/**
* MAIN METHOD - Appliquer amélioration technique
*/
async apply(content, config = {}) {
return await tracer.run('TechnicalLayer.apply()', async () => {
const {
llmProvider = this.defaultLLM,
intensity = 1.0, // 0.0-2.0 intensité d'amélioration
analysisMode = true, // Analyser avant d'appliquer
csvData = null,
preserveStructure = true,
targetTerms = null // Termes techniques ciblés
} = config;
await tracer.annotate({
technicalLayer: true,
llmProvider,
intensity,
elementsCount: Object.keys(content).length,
mc0: csvData?.mc0
});
const startTime = Date.now();
logSh(`⚙️ TECHNICAL LAYER: Amélioration technique (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Intensité: ${intensity}`, 'INFO');
try {
let enhancedContent = {};
let elementsProcessed = 0;
let elementsEnhanced = 0;
if (analysisMode) {
// 1. Analyser éléments nécessitant amélioration technique
const analysis = await this.analyzeTechnicalNeeds(content, csvData, targetTerms);
logSh(` 📋 Analyse: ${analysis.candidates.length}/${Object.keys(content).length} éléments candidats`, 'DEBUG');
if (analysis.candidates.length === 0) {
logSh(`✅ TECHNICAL LAYER: Aucune amélioration nécessaire`, 'INFO');
return {
content,
stats: {
processed: Object.keys(content).length,
enhanced: 0,
analysisSkipped: true,
duration: Date.now() - startTime
}
};
}
// 2. Améliorer les éléments sélectionnés
const improvedResults = await this.enhanceTechnicalElements(
analysis.candidates,
csvData,
{ llmProvider, intensity, preserveStructure }
);
// 3. Merger avec contenu original
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = analysis.candidates.length;
} else {
// Mode direct : améliorer tous les éléments
enhancedContent = await this.enhanceAllElementsDirect(
content,
csvData,
{ llmProvider, intensity, preserveStructure }
);
elementsProcessed = Object.keys(content).length;
elementsEnhanced = this.countDifferences(content, enhancedContent);
}
const duration = Date.now() - startTime;
const stats = {
processed: elementsProcessed,
enhanced: elementsEnhanced,
total: Object.keys(content).length,
enhancementRate: (elementsEnhanced / Math.max(elementsProcessed, 1)) * 100,
duration,
llmProvider,
intensity
};
logSh(`✅ TECHNICAL LAYER TERMINÉE: ${elementsEnhanced}/${elementsProcessed} améliorés (${duration}ms)`, 'INFO');
await tracer.event('Technical layer appliquée', stats);
return { content: enhancedContent, stats };
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ TECHNICAL LAYER ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw error;
}
}, { content: Object.keys(content), config });
}
/**
* ANALYSER BESOINS TECHNIQUES
*/
async analyzeTechnicalNeeds(content, csvData, targetTerms = null) {
logSh(`🔍 Analyse besoins techniques`, 'DEBUG');
const analysis = {
candidates: [],
technicalTermsFound: [],
missingTerms: [],
globalScore: 0
};
// Définir termes techniques selon contexte
const contextualTerms = this.getContextualTechnicalTerms(csvData?.mc0, targetTerms);
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
const elementAnalysis = this.analyzeTechnicalElement(text, contextualTerms, csvData);
if (elementAnalysis.needsImprovement) {
analysis.candidates.push({
tag,
content: text,
technicalTerms: elementAnalysis.foundTerms,
missingTerms: elementAnalysis.missingTerms,
score: elementAnalysis.score,
improvements: elementAnalysis.improvements
});
analysis.globalScore += elementAnalysis.score;
}
analysis.technicalTermsFound.push(...elementAnalysis.foundTerms);
});
analysis.globalScore = analysis.globalScore / Math.max(Object.keys(content).length, 1);
analysis.technicalTermsFound = [...new Set(analysis.technicalTermsFound)];
logSh(` 📊 Score global technique: ${analysis.globalScore.toFixed(2)}`, 'DEBUG');
return analysis;
}
/**
* AMÉLIORER ÉLÉMENTS TECHNIQUES SÉLECTIONNÉS
*/
async enhanceTechnicalElements(candidates, csvData, config) {
logSh(`🛠️ Amélioration ${candidates.length} éléments techniques`, 'DEBUG');
const results = {};
const chunks = chunkArray(candidates, 4); // Chunks de 4 pour GPT-4
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk technique ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const enhancementPrompt = this.createTechnicalEnhancementPrompt(chunk, csvData, config);
const response = await callLLM(config.llmProvider, enhancementPrompt, {
temperature: 0.4, // Précision technique
maxTokens: 3000
}, csvData?.personality);
const chunkResults = this.parseTechnicalResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk technique ${chunkIndex + 1}: ${Object.keys(chunkResults).length} améliorés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk technique ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: conserver contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* AMÉLIORER TOUS ÉLÉMENTS MODE DIRECT
*/
async enhanceAllElementsDirect(content, csvData, config) {
const allElements = Object.entries(content).map(([tag, text]) => ({
tag,
content: text,
technicalTerms: [],
improvements: ['amélioration_générale_technique']
}));
return await this.enhanceTechnicalElements(allElements, csvData, config);
}
// ============= HELPER METHODS =============
/**
* Analyser élément technique individuel
*/
analyzeTechnicalElement(text, contextualTerms, csvData) {
let score = 0;
const foundTerms = [];
const missingTerms = [];
const improvements = [];
// 1. Détecter termes techniques présents
contextualTerms.forEach(term => {
if (text.toLowerCase().includes(term.toLowerCase())) {
foundTerms.push(term);
} else if (text.length > 100) { // Seulement pour textes longs
missingTerms.push(term);
}
});
// 2. Évaluer manque de précision technique
if (foundTerms.length === 0 && text.length > 80) {
score += 0.4;
improvements.push('ajout_termes_techniques');
}
// 3. Détecter vocabulaire trop générique
const genericWords = ['produit', 'solution', 'service', 'offre', 'article'];
const genericCount = genericWords.filter(word =>
text.toLowerCase().includes(word)
).length;
if (genericCount > 1) {
score += 0.3;
improvements.push('spécialisation_vocabulaire');
}
// 4. Manque de données techniques (dimensions, etc.)
if (text.length > 50 && !(/\d+\s*(mm|cm|m|%|°|kg|g)/.test(text))) {
score += 0.2;
improvements.push('ajout_données_techniques');
}
// 5. Contexte métier spécifique
if (csvData?.mc0 && !text.toLowerCase().includes(csvData.mc0.toLowerCase().split(' ')[0])) {
score += 0.1;
improvements.push('intégration_contexte_métier');
}
return {
needsImprovement: score > 0.3,
score,
foundTerms,
missingTerms: missingTerms.slice(0, 3), // Limiter à 3 termes manquants
improvements
};
}
/**
* Obtenir termes techniques contextuels
*/
getContextualTechnicalTerms(mc0, targetTerms) {
// Termes de base signalétique
const baseTerms = [
'dibond', 'aluminium', 'PMMA', 'acrylique', 'plexiglas',
'impression', 'gravure', 'découpe', 'fraisage', 'perçage',
'adhésif', 'fixation', 'visserie', 'support'
];
// Termes spécifiques selon contexte
const contextualTerms = [];
if (mc0) {
const mc0Lower = mc0.toLowerCase();
if (mc0Lower.includes('plaque')) {
contextualTerms.push('épaisseur 3mm', 'format standard', 'finition brossée', 'anodisation');
}
if (mc0Lower.includes('signalétique')) {
contextualTerms.push('norme ISO', 'pictogramme', 'contraste visuel', 'lisibilité');
}
if (mc0Lower.includes('personnalisée')) {
contextualTerms.push('découpe forme', 'impression numérique', 'quadrichromie', 'pantone');
}
}
// Ajouter termes ciblés si fournis
if (targetTerms && Array.isArray(targetTerms)) {
contextualTerms.push(...targetTerms);
}
return [...baseTerms, ...contextualTerms];
}
/**
* Créer prompt amélioration technique
*/
createTechnicalEnhancementPrompt(chunk, csvData, config) {
const personality = csvData?.personality;
let prompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces contenus.
CONTEXTE: ${csvData?.mc0 || 'Signalétique personnalisée'} - Secteur: impression/signalétique
${personality ? `PERSONNALITÉ: ${personality.nom} (${personality.style})` : ''}
INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif)
ÉLÉMENTS À AMÉLIORER TECHNIQUEMENT:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
CONTENU: "${item.content}"
AMÉLIORATIONS: ${item.improvements.join(', ')}
${item.missingTerms.length > 0 ? `TERMES À INTÉGRER: ${item.missingTerms.join(', ')}` : ''}`).join('\n\n')}
CONSIGNES TECHNIQUES:
- GARDE exactement le même message et ton${personality ? ` ${personality.style}` : ''}
- AJOUTE précision technique naturelle et vocabulaire spécialisé
- INTÈGRE termes métier : matériaux, procédés, normes, dimensions
- REMPLACE vocabulaire générique par termes techniques appropriés
- ÉVITE jargon incompréhensible, reste accessible
- PRESERVE longueur approximative (±15%)
VOCABULAIRE TECHNIQUE RECOMMANDÉ:
- Matériaux: dibond, aluminium anodisé, PMMA coulé, PVC expansé
- Procédés: impression UV, gravure laser, découpe numérique, fraisage CNC
- Finitions: brossé, poli, texturé, laqué
- Fixations: perçage, adhésif double face, vis inox, plots de fixation
FORMAT RÉPONSE:
[1] Contenu avec amélioration technique précise
[2] Contenu avec amélioration technique précise
etc...
IMPORTANT: Réponse DIRECTE par les contenus améliorés, pas d'explication.`;
return prompt;
}
/**
* Parser réponse technique
*/
parseTechnicalResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let technicalContent = match[2].trim();
const element = chunk[index];
// Nettoyer contenu technique
technicalContent = this.cleanTechnicalContent(technicalContent);
if (technicalContent && technicalContent.length > 10) {
results[element.tag] = technicalContent;
logSh(`✅ Amélioré technique [${element.tag}]: "${technicalContent.substring(0, 60)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content; // Fallback
logSh(`⚠️ Fallback technique [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu technique généré
*/
cleanTechnicalContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(voici\s+)?le\s+contenu\s+amélioré\s*[:.]?\s*/gi, '');
content = content.replace(/^(avec\s+)?amélioration\s+technique\s*[:.]?\s*/gi, '');
content = content.replace(/^(bon,?\s*)?(alors,?\s*)?pour\s+/gi, '');
// Nettoyer formatage
content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
content = content.replace(/\s{2,}/g, ' '); // Espaces multiples
content = content.trim();
return content;
}
/**
* Compter différences entre contenus
*/
countDifferences(original, enhanced) {
let count = 0;
Object.keys(original).forEach(tag => {
if (enhanced[tag] && enhanced[tag] !== original[tag]) {
count++;
}
});
return count;
}
}
module.exports = { TechnicalLayer };
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/TransitionLayer.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// TRANSITION LAYER - COUCHE TRANSITIONS MODULAIRE
// Responsabilité: Amélioration fluidité modulaire réutilisable
// LLM: Gemini (fluidité linguistique optimale)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { chunkArray, sleep } = require('./SelectiveUtils');
/**
* COUCHE TRANSITIONS MODULAIRE
*/
class TransitionLayer {
constructor() {
this.name = 'TransitionEnhancement';
this.defaultLLM = 'gemini';
this.priority = 2; // Priorité moyenne - appliqué après technique
}
/**
* MAIN METHOD - Appliquer amélioration transitions
*/
async apply(content, config = {}) {
return await tracer.run('TransitionLayer.apply()', async () => {
const {
llmProvider = this.defaultLLM,
intensity = 1.0, // 0.0-2.0 intensité d'amélioration
analysisMode = true, // Analyser avant d'appliquer
csvData = null,
preserveStructure = true,
targetIssues = null // Issues spécifiques à corriger
} = config;
await tracer.annotate({
transitionLayer: true,
llmProvider,
intensity,
elementsCount: Object.keys(content).length,
mc0: csvData?.mc0
});
const startTime = Date.now();
logSh(`🔗 TRANSITION LAYER: Amélioration fluidité (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Intensité: ${intensity}`, 'INFO');
try {
let enhancedContent = {};
let elementsProcessed = 0;
let elementsEnhanced = 0;
if (analysisMode) {
// 1. Analyser éléments nécessitant amélioration transitions
const analysis = await this.analyzeTransitionNeeds(content, csvData, targetIssues);
logSh(` 📋 Analyse: ${analysis.candidates.length}/${Object.keys(content).length} éléments candidats`, 'DEBUG');
if (analysis.candidates.length === 0) {
logSh(`✅ TRANSITION LAYER: Fluidité déjà optimale`, 'INFO');
return {
content,
stats: {
processed: Object.keys(content).length,
enhanced: 0,
analysisSkipped: true,
duration: Date.now() - startTime
}
};
}
// 2. Améliorer les éléments sélectionnés
const improvedResults = await this.enhanceTransitionElements(
analysis.candidates,
csvData,
{ llmProvider, intensity, preserveStructure }
);
// 3. Merger avec contenu original
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = analysis.candidates.length;
} else {
// Mode direct : améliorer tous les éléments longs
const longElements = Object.entries(content)
.filter(([tag, text]) => text.length > 150)
.map(([tag, text]) => ({ tag, content: text, issues: ['amélioration_générale'] }));
if (longElements.length === 0) {
return { content, stats: { processed: 0, enhanced: 0, duration: Date.now() - startTime } };
}
const improvedResults = await this.enhanceTransitionElements(
longElements,
csvData,
{ llmProvider, intensity, preserveStructure }
);
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = longElements.length;
}
const duration = Date.now() - startTime;
const stats = {
processed: elementsProcessed,
enhanced: elementsEnhanced,
total: Object.keys(content).length,
enhancementRate: (elementsEnhanced / Math.max(elementsProcessed, 1)) * 100,
duration,
llmProvider,
intensity
};
logSh(`✅ TRANSITION LAYER TERMINÉE: ${elementsEnhanced}/${elementsProcessed} fluidifiés (${duration}ms)`, 'INFO');
await tracer.event('Transition layer appliquée', stats);
return { content: enhancedContent, stats };
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ TRANSITION LAYER ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback gracieux : retourner contenu original
logSh(`🔄 Fallback: contenu original préservé`, 'WARNING');
return {
content,
stats: { fallback: true, duration },
error: error.message
};
}
}, { content: Object.keys(content), config });
}
/**
* ANALYSER BESOINS TRANSITIONS
*/
async analyzeTransitionNeeds(content, csvData, targetIssues = null) {
logSh(`🔍 Analyse besoins transitions`, 'DEBUG');
const analysis = {
candidates: [],
globalScore: 0,
issuesFound: {
repetitiveConnectors: 0,
abruptTransitions: 0,
uniformSentences: 0,
formalityImbalance: 0
}
};
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
const elementAnalysis = this.analyzeTransitionElement(text, csvData);
if (elementAnalysis.needsImprovement) {
analysis.candidates.push({
tag,
content: text,
issues: elementAnalysis.issues,
score: elementAnalysis.score,
improvements: elementAnalysis.improvements
});
analysis.globalScore += elementAnalysis.score;
// Compter types d'issues
elementAnalysis.issues.forEach(issue => {
if (analysis.issuesFound.hasOwnProperty(issue)) {
analysis.issuesFound[issue]++;
}
});
}
});
analysis.globalScore = analysis.globalScore / Math.max(Object.keys(content).length, 1);
logSh(` 📊 Score global transitions: ${analysis.globalScore.toFixed(2)}`, 'DEBUG');
logSh(` 🔍 Issues trouvées: ${JSON.stringify(analysis.issuesFound)}`, 'DEBUG');
return analysis;
}
/**
* AMÉLIORER ÉLÉMENTS TRANSITIONS SÉLECTIONNÉS
*/
async enhanceTransitionElements(candidates, csvData, config) {
logSh(`🔄 Amélioration ${candidates.length} éléments transitions`, 'DEBUG');
const results = {};
const chunks = chunkArray(candidates, 6); // Chunks plus petits pour Gemini
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk transitions ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const enhancementPrompt = this.createTransitionEnhancementPrompt(chunk, csvData, config);
const response = await callLLM(config.llmProvider, enhancementPrompt, {
temperature: 0.6, // Créativité modérée pour fluidité
maxTokens: 2500
}, csvData?.personality);
const chunkResults = this.parseTransitionResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk transitions ${chunkIndex + 1}: ${Object.keys(chunkResults).length} fluidifiés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk transitions ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: conserver contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
// ============= HELPER METHODS =============
/**
* Analyser élément transition individuel
*/
analyzeTransitionElement(text, csvData) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) {
return { needsImprovement: false, score: 0, issues: [], improvements: [] };
}
let score = 0;
const issues = [];
const improvements = [];
// 1. Analyser connecteurs répétitifs
const repetitiveScore = this.analyzeRepetitiveConnectors(text);
if (repetitiveScore > 0.3) {
score += 0.3;
issues.push('repetitiveConnectors');
improvements.push('varier_connecteurs');
}
// 2. Analyser transitions abruptes
const abruptScore = this.analyzeAbruptTransitions(sentences);
if (abruptScore > 0.4) {
score += 0.4;
issues.push('abruptTransitions');
improvements.push('ajouter_transitions_fluides');
}
// 3. Analyser uniformité des phrases
const uniformityScore = this.analyzeSentenceUniformity(sentences);
if (uniformityScore < 0.3) {
score += 0.2;
issues.push('uniformSentences');
improvements.push('varier_longueurs_phrases');
}
// 4. Analyser équilibre formalité
const formalityScore = this.analyzeFormalityBalance(text);
if (formalityScore > 0.5) {
score += 0.1;
issues.push('formalityImbalance');
improvements.push('équilibrer_registre_langue');
}
return {
needsImprovement: score > 0.3,
score,
issues,
improvements
};
}
/**
* Analyser connecteurs répétitifs
*/
analyzeRepetitiveConnectors(text) {
const commonConnectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc'];
let totalConnectors = 0;
let repetitions = 0;
commonConnectors.forEach(connector => {
const matches = (text.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
totalConnectors += matches.length;
if (matches.length > 1) repetitions += matches.length - 1;
});
return totalConnectors > 0 ? repetitions / totalConnectors : 0;
}
/**
* Analyser transitions abruptes
*/
analyzeAbruptTransitions(sentences) {
if (sentences.length < 2) return 0;
let abruptCount = 0;
for (let i = 1; i < sentences.length; i++) {
const current = sentences[i].trim().toLowerCase();
const hasConnector = this.hasTransitionWord(current);
if (!hasConnector && current.length > 30) {
abruptCount++;
}
}
return abruptCount / (sentences.length - 1);
}
/**
* Analyser uniformité des phrases
*/
analyzeSentenceUniformity(sentences) {
if (sentences.length < 2) return 1;
const lengths = sentences.map(s => s.trim().length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
return Math.min(1, stdDev / avgLength);
}
/**
* Analyser équilibre formalité
*/
analyzeFormalityBalance(text) {
const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois', 'cependant'];
const casualIndicators = ['du coup', 'bon', 'franchement', 'nickel', 'sympa'];
let formalCount = 0;
let casualCount = 0;
formalIndicators.forEach(indicator => {
if (text.toLowerCase().includes(indicator)) formalCount++;
});
casualIndicators.forEach(indicator => {
if (text.toLowerCase().includes(indicator)) casualCount++;
});
const total = formalCount + casualCount;
if (total === 0) return 0;
// Déséquilibre si trop d'un côté
return Math.abs(formalCount - casualCount) / total;
}
/**
* Vérifier présence mots de transition
*/
hasTransitionWord(sentence) {
const transitionWords = [
'par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc',
'ensuite', 'puis', 'également', 'aussi', 'néanmoins', 'toutefois',
'd\'ailleurs', 'en outre', 'par contre', 'en revanche'
];
return transitionWords.some(word => sentence.includes(word));
}
/**
* Créer prompt amélioration transitions
*/
createTransitionEnhancementPrompt(chunk, csvData, config) {
const personality = csvData?.personality;
let prompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.
CONTEXTE: Article SEO ${csvData?.mc0 || 'signalétique personnalisée'}
${personality ? `PERSONNALITÉ: ${personality.nom} (${personality.style} web professionnel)` : ''}
${personality?.connecteursPref ? `CONNECTEURS PRÉFÉRÉS: ${personality.connecteursPref}` : ''}
INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif)
CONTENUS À FLUIDIFIER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
PROBLÈMES: ${item.issues.join(', ')}
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS FLUIDITÉ:
- Connecteurs plus naturels et variés${personality?.connecteursPref ? `: ${personality.connecteursPref}` : ''}
- Transitions fluides entre idées et paragraphes
- Variation naturelle longueurs phrases
- ÉVITE répétitions excessives ("du coup", "par ailleurs", "en effet")
- Style ${personality?.style || 'professionnel'} mais naturel web
CONSIGNES STRICTES:
- NE CHANGE PAS le fond du message ni les informations
- GARDE même structure générale et longueur approximative (±20%)
- Améliore SEULEMENT la fluidité et les enchaînements
- RESPECTE le style ${personality?.nom || 'professionnel'}${personality?.style ? ` (${personality.style})` : ''}
- ÉVITE sur-correction qui rendrait artificiel
TECHNIQUES FLUIDITÉ:
- Varier connecteurs logiques sans répétition
- Alterner phrases courtes (8-12 mots) et moyennes (15-20 mots)
- Utiliser pronoms et reprises pour cohésion
- Ajouter transitions implicites par reformulation
- Équilibrer registre soutenu/accessible
FORMAT RÉPONSE:
[1] Contenu avec transitions améliorées
[2] Contenu avec transitions améliorées
etc...
IMPORTANT: Réponse DIRECTE par les contenus fluidifiés, pas d'explication.`;
return prompt;
}
/**
* Parser réponse transitions
*/
parseTransitionResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let fluidContent = match[2].trim();
const element = chunk[index];
// Nettoyer contenu fluidifié
fluidContent = this.cleanTransitionContent(fluidContent);
if (fluidContent && fluidContent.length > 10) {
results[element.tag] = fluidContent;
logSh(`✅ Fluidifié [${element.tag}]: "${fluidContent.substring(0, 60)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content; // Fallback
logSh(`⚠️ Fallback transitions [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu transitions généré
*/
cleanTransitionContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(voici\s+)?le\s+contenu\s+(fluidifié|amélioré)\s*[:.]?\s*/gi, '');
content = content.replace(/^(avec\s+)?transitions\s+améliorées\s*[:.]?\s*/gi, '');
content = content.replace(/^(bon,?\s*)?(alors,?\s*)?/, '');
// Nettoyer formatage
content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
content = content.replace(/\s{2,}/g, ' '); // Espaces multiples
content = content.trim();
return content;
}
}
module.exports = { TransitionLayer };
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/StyleLayer.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// STYLE LAYER - COUCHE STYLE MODULAIRE
// Responsabilité: Adaptation personnalité modulaire réutilisable
// LLM: Mistral (excellence style et personnalité)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { chunkArray, sleep } = require('./SelectiveUtils');
/**
* COUCHE STYLE MODULAIRE
*/
class StyleLayer {
constructor() {
this.name = 'StyleEnhancement';
this.defaultLLM = 'mistral';
this.priority = 3; // Priorité basse - appliqué en dernier
}
/**
* MAIN METHOD - Appliquer amélioration style
*/
async apply(content, config = {}) {
return await tracer.run('StyleLayer.apply()', async () => {
const {
llmProvider = this.defaultLLM,
intensity = 1.0, // 0.0-2.0 intensité d'amélioration
analysisMode = true, // Analyser avant d'appliquer
csvData = null,
preserveStructure = true,
targetStyle = null // Style spécifique à appliquer
} = config;
await tracer.annotate({
styleLayer: true,
llmProvider,
intensity,
elementsCount: Object.keys(content).length,
personality: csvData?.personality?.nom
});
const startTime = Date.now();
logSh(`🎨 STYLE LAYER: Amélioration personnalité (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Style: ${csvData?.personality?.nom || 'standard'}`, 'INFO');
try {
let enhancedContent = {};
let elementsProcessed = 0;
let elementsEnhanced = 0;
// Vérifier présence personnalité
if (!csvData?.personality && !targetStyle) {
logSh(`⚠️ STYLE LAYER: Pas de personnalité définie, style générique appliqué`, 'WARNING');
}
if (analysisMode) {
// 1. Analyser éléments nécessitant amélioration style
const analysis = await this.analyzeStyleNeeds(content, csvData, targetStyle);
logSh(` 📋 Analyse: ${analysis.candidates.length}/${Object.keys(content).length} éléments candidats`, 'DEBUG');
if (analysis.candidates.length === 0) {
logSh(`✅ STYLE LAYER: Style déjà cohérent`, 'INFO');
return {
content,
stats: {
processed: Object.keys(content).length,
enhanced: 0,
analysisSkipped: true,
duration: Date.now() - startTime
}
};
}
// 2. Améliorer les éléments sélectionnés
const improvedResults = await this.enhanceStyleElements(
analysis.candidates,
csvData,
{ llmProvider, intensity, preserveStructure, targetStyle }
);
// 3. Merger avec contenu original
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = analysis.candidates.length;
} else {
// Mode direct : améliorer tous les éléments textuels
const textualElements = Object.entries(content)
.filter(([tag, text]) => text.length > 50 && !tag.includes('FAQ_Question'))
.map(([tag, text]) => ({ tag, content: text, styleIssues: ['adaptation_générale'] }));
if (textualElements.length === 0) {
return { content, stats: { processed: 0, enhanced: 0, duration: Date.now() - startTime } };
}
const improvedResults = await this.enhanceStyleElements(
textualElements,
csvData,
{ llmProvider, intensity, preserveStructure, targetStyle }
);
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = textualElements.length;
}
const duration = Date.now() - startTime;
const stats = {
processed: elementsProcessed,
enhanced: elementsEnhanced,
total: Object.keys(content).length,
enhancementRate: (elementsEnhanced / Math.max(elementsProcessed, 1)) * 100,
duration,
llmProvider,
intensity,
personalityApplied: csvData?.personality?.nom || targetStyle || 'générique'
};
logSh(`✅ STYLE LAYER TERMINÉE: ${elementsEnhanced}/${elementsProcessed} stylisés (${duration}ms)`, 'INFO');
await tracer.event('Style layer appliquée', stats);
return { content: enhancedContent, stats };
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ STYLE LAYER ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback gracieux : retourner contenu original
logSh(`🔄 Fallback: style original préservé`, 'WARNING');
return {
content,
stats: { fallback: true, duration },
error: error.message
};
}
}, { content: Object.keys(content), config });
}
/**
* ANALYSER BESOINS STYLE
*/
async analyzeStyleNeeds(content, csvData, targetStyle = null) {
logSh(`🎨 Analyse besoins style`, 'DEBUG');
const analysis = {
candidates: [],
globalScore: 0,
styleIssues: {
genericLanguage: 0,
personalityMismatch: 0,
inconsistentTone: 0,
missingVocabulary: 0
}
};
const personality = csvData?.personality;
const expectedStyle = targetStyle || personality;
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
const elementAnalysis = this.analyzeStyleElement(text, expectedStyle, csvData);
if (elementAnalysis.needsImprovement) {
analysis.candidates.push({
tag,
content: text,
styleIssues: elementAnalysis.issues,
score: elementAnalysis.score,
improvements: elementAnalysis.improvements
});
analysis.globalScore += elementAnalysis.score;
// Compter types d'issues
elementAnalysis.issues.forEach(issue => {
if (analysis.styleIssues.hasOwnProperty(issue)) {
analysis.styleIssues[issue]++;
}
});
}
});
analysis.globalScore = analysis.globalScore / Math.max(Object.keys(content).length, 1);
logSh(` 📊 Score global style: ${analysis.globalScore.toFixed(2)}`, 'DEBUG');
logSh(` 🎭 Issues style: ${JSON.stringify(analysis.styleIssues)}`, 'DEBUG');
return analysis;
}
/**
* AMÉLIORER ÉLÉMENTS STYLE SÉLECTIONNÉS
*/
async enhanceStyleElements(candidates, csvData, config) {
logSh(`🎨 Amélioration ${candidates.length} éléments style`, 'DEBUG');
const results = {};
const chunks = chunkArray(candidates, 5); // Chunks optimisés pour Mistral
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk style ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const enhancementPrompt = this.createStyleEnhancementPrompt(chunk, csvData, config);
const response = await callLLM(config.llmProvider, enhancementPrompt, {
temperature: 0.8, // Créativité élevée pour style
maxTokens: 3000
}, csvData?.personality);
const chunkResults = this.parseStyleResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk style ${chunkIndex + 1}: ${Object.keys(chunkResults).length} stylisés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1800);
}
} catch (error) {
logSh(` ❌ Chunk style ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: conserver contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
// ============= HELPER METHODS =============
/**
* Analyser élément style individuel
*/
analyzeStyleElement(text, expectedStyle, csvData) {
let score = 0;
const issues = [];
const improvements = [];
// Si pas de style attendu, score faible
if (!expectedStyle) {
return { needsImprovement: false, score: 0.1, issues: ['pas_style_défini'], improvements: [] };
}
// 1. Analyser langage générique
const genericScore = this.analyzeGenericLanguage(text);
if (genericScore > 0.4) {
score += 0.3;
issues.push('genericLanguage');
improvements.push('personnaliser_vocabulaire');
}
// 2. Analyser adéquation personnalité
if (expectedStyle.vocabulairePref) {
const personalityScore = this.analyzePersonalityAlignment(text, expectedStyle);
if (personalityScore < 0.3) {
score += 0.4;
issues.push('personalityMismatch');
improvements.push('appliquer_style_personnalité');
}
}
// 3. Analyser cohérence de ton
const toneScore = this.analyzeToneConsistency(text, expectedStyle);
if (toneScore > 0.5) {
score += 0.2;
issues.push('inconsistentTone');
improvements.push('unifier_ton');
}
// 4. Analyser vocabulaire spécialisé
if (expectedStyle.niveauTechnique) {
const vocabScore = this.analyzeVocabularyLevel(text, expectedStyle);
if (vocabScore > 0.4) {
score += 0.1;
issues.push('missingVocabulary');
improvements.push('ajuster_niveau_vocabulaire');
}
}
return {
needsImprovement: score > 0.3,
score,
issues,
improvements
};
}
/**
* Analyser langage générique
*/
analyzeGenericLanguage(text) {
const genericPhrases = [
'nos solutions', 'notre expertise', 'notre savoir-faire',
'nous vous proposons', 'nous mettons à votre disposition',
'qualité optimale', 'service de qualité', 'expertise reconnue'
];
let genericCount = 0;
genericPhrases.forEach(phrase => {
if (text.toLowerCase().includes(phrase)) genericCount++;
});
const wordCount = text.split(/\s+/).length;
return Math.min(1, (genericCount / Math.max(wordCount / 50, 1)));
}
/**
* Analyser alignement personnalité
*/
analyzePersonalityAlignment(text, personality) {
if (!personality.vocabulairePref) return 1;
const preferredWords = personality.vocabulairePref.toLowerCase().split(',');
const contentLower = text.toLowerCase();
let alignmentScore = 0;
preferredWords.forEach(word => {
if (word.trim() && contentLower.includes(word.trim())) {
alignmentScore++;
}
});
return Math.min(1, alignmentScore / Math.max(preferredWords.length, 1));
}
/**
* Analyser cohérence de ton
*/
analyzeToneConsistency(text, expectedStyle) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) return 0;
const tones = sentences.map(sentence => this.detectSentenceTone(sentence));
const expectedTone = this.getExpectedTone(expectedStyle);
let inconsistencies = 0;
tones.forEach(tone => {
if (tone !== expectedTone && tone !== 'neutral') {
inconsistencies++;
}
});
return inconsistencies / tones.length;
}
/**
* Analyser niveau vocabulaire
*/
analyzeVocabularyLevel(text, expectedStyle) {
const technicalWords = text.match(/\b\w{8,}\b/g) || [];
const expectedLevel = expectedStyle.niveauTechnique || 'standard';
const techRatio = technicalWords.length / text.split(/\s+/).length;
switch (expectedLevel) {
case 'accessible':
return techRatio > 0.1 ? techRatio : 0; // Trop technique
case 'expert':
return techRatio < 0.05 ? 1 - techRatio : 0; // Pas assez technique
default:
return techRatio > 0.15 || techRatio < 0.02 ? Math.abs(0.08 - techRatio) : 0;
}
}
/**
* Détecter ton de phrase
*/
detectSentenceTone(sentence) {
const lowerSentence = sentence.toLowerCase();
if (/\b(excellent|remarquable|exceptionnel|parfait)\b/.test(lowerSentence)) return 'enthusiastic';
if (/\b(il convient|nous recommandons|il est conseillé)\b/.test(lowerSentence)) return 'formal';
if (/\b(sympa|cool|nickel|top)\b/.test(lowerSentence)) return 'casual';
if (/\b(technique|précision|spécification)\b/.test(lowerSentence)) return 'technical';
return 'neutral';
}
/**
* Obtenir ton attendu selon personnalité
*/
getExpectedTone(personality) {
if (!personality || !personality.style) return 'neutral';
const style = personality.style.toLowerCase();
if (style.includes('technique') || style.includes('expert')) return 'technical';
if (style.includes('commercial') || style.includes('vente')) return 'enthusiastic';
if (style.includes('décontracté') || style.includes('moderne')) return 'casual';
if (style.includes('professionnel') || style.includes('formel')) return 'formal';
return 'neutral';
}
/**
* Créer prompt amélioration style
*/
createStyleEnhancementPrompt(chunk, csvData, config) {
const personality = csvData?.personality || config.targetStyle;
let prompt = `MISSION: Adapte UNIQUEMENT le style et la personnalité de ces contenus.
CONTEXTE: Article SEO ${csvData?.mc0 || 'signalétique personnalisée'}
${personality ? `PERSONNALITÉ CIBLE: ${personality.nom} (${personality.style})` : 'STYLE: Professionnel standard'}
${personality?.description ? `DESCRIPTION: ${personality.description}` : ''}
INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif)
CONTENUS À STYLISER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
PROBLÈMES: ${item.styleIssues.join(', ')}
CONTENU: "${item.content}"`).join('\n\n')}
PROFIL PERSONNALITÉ ${personality?.nom || 'Standard'}:
${personality ? `- Style: ${personality.style}
- Niveau: ${personality.niveauTechnique || 'standard'}
- Vocabulaire préféré: ${personality.vocabulairePref || 'professionnel'}
- Connecteurs: ${personality.connecteursPref || 'variés'}
${personality.specificites ? `- Spécificités: ${personality.specificites}` : ''}` : '- Style professionnel web standard'}
OBJECTIFS STYLE:
- Appliquer personnalité ${personality?.nom || 'standard'} de façon naturelle
- Utiliser vocabulaire et expressions caractéristiques
- Maintenir cohérence de ton sur tout le contenu
- Adapter niveau technique selon profil (${personality?.niveauTechnique || 'standard'})
- Style web ${personality?.style || 'professionnel'} mais authentique
CONSIGNES STRICTES:
- NE CHANGE PAS le fond du message ni les informations factuelles
- GARDE même structure et longueur approximative (±15%)
- Applique SEULEMENT style et personnalité sur la forme
- RESPECTE impérativement le niveau ${personality?.niveauTechnique || 'standard'}
- ÉVITE exagération qui rendrait artificiel
TECHNIQUES STYLE:
${personality?.vocabulairePref ? `- Intégrer naturellement: ${personality.vocabulairePref}` : '- Vocabulaire professionnel équilibré'}
- Adapter registre de langue selon ${personality?.style || 'professionnel'}
- Expressions et tournures caractéristiques personnalité
- Ton cohérent: ${this.getExpectedTone(personality)} mais naturel
- Connecteurs préférés: ${personality?.connecteursPref || 'variés et naturels'}
FORMAT RÉPONSE:
[1] Contenu avec style personnalisé
[2] Contenu avec style personnalisé
etc...
IMPORTANT: Réponse DIRECTE par les contenus stylisés, pas d'explication.`;
return prompt;
}
/**
* Parser réponse style
*/
parseStyleResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let styledContent = match[2].trim();
const element = chunk[index];
// Nettoyer contenu stylisé
styledContent = this.cleanStyleContent(styledContent);
if (styledContent && styledContent.length > 10) {
results[element.tag] = styledContent;
logSh(`✅ Stylisé [${element.tag}]: "${styledContent.substring(0, 60)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content; // Fallback
logSh(`⚠️ Fallback style [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu style généré
*/
cleanStyleContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(voici\s+)?le\s+contenu\s+(stylisé|adapté|personnalisé)\s*[:.]?\s*/gi, '');
content = content.replace(/^(avec\s+)?style\s+[^:]*\s*[:.]?\s*/gi, '');
content = content.replace(/^(dans\s+le\s+style\s+de\s+)[^:]*[:.]?\s*/gi, '');
// Nettoyer formatage
content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
content = content.replace(/\s{2,}/g, ' '); // Espaces multiples
content = content.trim();
return content;
}
}
module.exports = { StyleLayer };
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/SelectiveCore.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// SELECTIVE CORE - MOTEUR MODULAIRE
// Responsabilité: Moteur selective enhancement réutilisable sur tout contenu
// Architecture: Couches applicables à la demande
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - APPLICATION COUCHE SELECTIVE ENHANCEMENT
* Input: contenu existant + configuration selective
* Output: contenu avec couche selective appliquée
*/
async function applySelectiveLayer(existingContent, config = {}) {
return await tracer.run('SelectiveCore.applySelectiveLayer()', async () => {
const {
layerType = 'technical', // 'technical' | 'transitions' | 'style' | 'all'
llmProvider = 'auto', // 'claude' | 'gpt4' | 'gemini' | 'mistral' | 'auto'
analysisMode = true, // Analyser avant d'appliquer
preserveStructure = true,
csvData = null,
context = {}
} = config;
await tracer.annotate({
selectiveLayer: true,
layerType,
llmProvider,
analysisMode,
elementsCount: Object.keys(existingContent).length
});
const startTime = Date.now();
logSh(`🔧 APPLICATION COUCHE SELECTIVE: ${layerType} (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(existingContent).length} éléments | Mode: ${analysisMode ? 'analysé' : 'direct'}`, 'INFO');
try {
let enhancedContent = {};
let layerStats = {};
// Sélection automatique du LLM si 'auto'
const selectedLLM = selectOptimalLLM(layerType, llmProvider);
// Application selon type de couche
switch (layerType) {
case 'technical':
const technicalResult = await applyTechnicalEnhancement(existingContent, { ...config, llmProvider: selectedLLM });
enhancedContent = technicalResult.content;
layerStats = technicalResult.stats;
break;
case 'transitions':
const transitionResult = await applyTransitionEnhancement(existingContent, { ...config, llmProvider: selectedLLM });
enhancedContent = transitionResult.content;
layerStats = transitionResult.stats;
break;
case 'style':
const styleResult = await applyStyleEnhancement(existingContent, { ...config, llmProvider: selectedLLM });
enhancedContent = styleResult.content;
layerStats = styleResult.stats;
break;
case 'all':
const allResult = await applyAllSelectiveLayers(existingContent, config);
enhancedContent = allResult.content;
layerStats = allResult.stats;
break;
default:
throw new Error(`Type de couche selective inconnue: ${layerType}`);
}
const duration = Date.now() - startTime;
const stats = {
layerType,
llmProvider: selectedLLM,
elementsProcessed: Object.keys(existingContent).length,
elementsEnhanced: countEnhancedElements(existingContent, enhancedContent),
duration,
...layerStats
};
logSh(`✅ COUCHE SELECTIVE APPLIQUÉE: ${stats.elementsEnhanced}/${stats.elementsProcessed} améliorés (${duration}ms)`, 'INFO');
await tracer.event('Couche selective appliquée', stats);
return {
content: enhancedContent,
stats,
original: existingContent,
config: { ...config, llmProvider: selectedLLM }
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ COUCHE SELECTIVE ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content: existingContent,
stats: { fallback: true, duration },
original: existingContent,
config,
error: error.message
};
}
}, { existingContent: Object.keys(existingContent), config });
}
/**
* APPLICATION TECHNIQUE MODULAIRE
*/
async function applyTechnicalEnhancement(content, config = {}) {
const { TechnicalLayer } = require('./TechnicalLayer');
const layer = new TechnicalLayer();
return await layer.apply(content, config);
}
/**
* APPLICATION TRANSITIONS MODULAIRE
*/
async function applyTransitionEnhancement(content, config = {}) {
const { TransitionLayer } = require('./TransitionLayer');
const layer = new TransitionLayer();
return await layer.apply(content, config);
}
/**
* APPLICATION STYLE MODULAIRE
*/
async function applyStyleEnhancement(content, config = {}) {
const { StyleLayer } = require('./StyleLayer');
const layer = new StyleLayer();
return await layer.apply(content, config);
}
/**
* APPLICATION TOUTES COUCHES SÉQUENTIELLES
*/
async function applyAllSelectiveLayers(content, config = {}) {
logSh(`🔄 Application séquentielle toutes couches selective`, 'DEBUG');
let currentContent = content;
const allStats = {
steps: [],
totalDuration: 0,
totalEnhancements: 0
};
const steps = [
{ name: 'technical', llm: 'gpt4' },
{ name: 'transitions', llm: 'gemini' },
{ name: 'style', llm: 'mistral' }
];
for (const step of steps) {
try {
logSh(` 🔧 Étape: ${step.name} (${step.llm})`, 'DEBUG');
const stepResult = await applySelectiveLayer(currentContent, {
...config,
layerType: step.name,
llmProvider: step.llm
});
currentContent = stepResult.content;
allStats.steps.push({
name: step.name,
llm: step.llm,
...stepResult.stats
});
allStats.totalDuration += stepResult.stats.duration;
allStats.totalEnhancements += stepResult.stats.elementsEnhanced;
} catch (error) {
logSh(` ❌ Étape ${step.name} échouée: ${error.message}`, 'ERROR');
allStats.steps.push({
name: step.name,
llm: step.llm,
error: error.message,
duration: 0,
elementsEnhanced: 0
});
}
}
return {
content: currentContent,
stats: allStats
};
}
/**
* ANALYSE BESOIN D'ENHANCEMENT
*/
async function analyzeEnhancementNeeds(content, config = {}) {
logSh(`🔍 Analyse besoins selective enhancement`, 'DEBUG');
const analysis = {
technical: { needed: false, score: 0, elements: [] },
transitions: { needed: false, score: 0, elements: [] },
style: { needed: false, score: 0, elements: [] },
recommendation: 'none'
};
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
// Analyse technique (termes techniques manquants)
const technicalNeed = assessTechnicalNeed(text, config.csvData);
if (technicalNeed.score > 0.3) {
analysis.technical.needed = true;
analysis.technical.score += technicalNeed.score;
analysis.technical.elements.push({ tag, score: technicalNeed.score, reason: technicalNeed.reason });
}
// Analyse transitions (fluidité)
const transitionNeed = assessTransitionNeed(text);
if (transitionNeed.score > 0.4) {
analysis.transitions.needed = true;
analysis.transitions.score += transitionNeed.score;
analysis.transitions.elements.push({ tag, score: transitionNeed.score, reason: transitionNeed.reason });
}
// Analyse style (personnalité)
const styleNeed = assessStyleNeed(text, config.csvData?.personality);
if (styleNeed.score > 0.3) {
analysis.style.needed = true;
analysis.style.score += styleNeed.score;
analysis.style.elements.push({ tag, score: styleNeed.score, reason: styleNeed.reason });
}
});
// Normaliser scores
const elementCount = Object.keys(content).length;
analysis.technical.score = analysis.technical.score / elementCount;
analysis.transitions.score = analysis.transitions.score / elementCount;
analysis.style.score = analysis.style.score / elementCount;
// Recommandation
const scores = [
{ type: 'technical', score: analysis.technical.score },
{ type: 'transitions', score: analysis.transitions.score },
{ type: 'style', score: analysis.style.score }
].sort((a, b) => b.score - a.score);
if (scores[0].score > 0.6) {
analysis.recommendation = scores[0].type;
} else if (scores[0].score > 0.4) {
analysis.recommendation = 'light_' + scores[0].type;
}
logSh(` 📊 Analyse: Tech=${analysis.technical.score.toFixed(2)} | Trans=${analysis.transitions.score.toFixed(2)} | Style=${analysis.style.score.toFixed(2)}`, 'DEBUG');
logSh(` 💡 Recommandation: ${analysis.recommendation}`, 'DEBUG');
return analysis;
}
// ============= HELPER FUNCTIONS =============
/**
* Sélectionner LLM optimal selon type de couche
*/
function selectOptimalLLM(layerType, llmProvider) {
if (llmProvider !== 'auto') return llmProvider;
const optimalMapping = {
'technical': 'openai', // OpenAI GPT-4 excellent pour précision technique
'transitions': 'gemini', // Gemini bon pour fluidité
'style': 'mistral', // Mistral excellent pour style personnalité
'all': 'claude' // Claude polyvalent pour tout
};
return optimalMapping[layerType] || 'claude';
}
/**
* Compter éléments améliorés
*/
function countEnhancedElements(original, enhanced) {
let count = 0;
Object.keys(original).forEach(tag => {
if (enhanced[tag] && enhanced[tag] !== original[tag]) {
count++;
}
});
return count;
}
/**
* Évaluer besoin technique
*/
function assessTechnicalNeed(content, csvData) {
let score = 0;
let reason = [];
// Manque de termes techniques spécifiques
if (csvData?.mc0) {
const technicalTerms = ['dibond', 'pmma', 'aluminium', 'fraisage', 'impression', 'gravure', 'découpe'];
const contentLower = content.toLowerCase();
const foundTerms = technicalTerms.filter(term => contentLower.includes(term));
if (foundTerms.length === 0 && content.length > 100) {
score += 0.4;
reason.push('manque_termes_techniques');
}
}
// Vocabulaire trop générique
const genericWords = ['produit', 'solution', 'service', 'qualité', 'offre'];
const genericCount = genericWords.filter(word => content.toLowerCase().includes(word)).length;
if (genericCount > 2) {
score += 0.3;
reason.push('vocabulaire_générique');
}
// Manque de précision dimensionnelle/technique
if (content.length > 50 && !(/\d+\s*(mm|cm|m|%|°)/.test(content))) {
score += 0.2;
reason.push('manque_précision_technique');
}
return { score: Math.min(1, score), reason: reason.join(',') };
}
/**
* Évaluer besoin transitions
*/
function assessTransitionNeed(content) {
let score = 0;
let reason = [];
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) return { score: 0, reason: '' };
// Connecteurs répétitifs
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant'];
let repetitiveConnectors = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(connector, 'gi')) || []);
if (matches.length > 1) repetitiveConnectors++;
});
if (repetitiveConnectors > 1) {
score += 0.4;
reason.push('connecteurs_répétitifs');
}
// Transitions abruptes (phrases sans connecteurs logiques)
let abruptTransitions = 0;
for (let i = 1; i < sentences.length; i++) {
const sentence = sentences[i].trim().toLowerCase();
const hasConnector = connectors.some(conn => sentence.startsWith(conn)) ||
/^(puis|ensuite|également|aussi|donc|ainsi)/.test(sentence);
if (!hasConnector && sentence.length > 30) {
abruptTransitions++;
}
}
if (abruptTransitions / sentences.length > 0.6) {
score += 0.3;
reason.push('transitions_abruptes');
}
return { score: Math.min(1, score), reason: reason.join(',') };
}
/**
* Évaluer besoin style
*/
function assessStyleNeed(content, personality) {
let score = 0;
let reason = [];
if (!personality) {
score += 0.2;
reason.push('pas_personnalité');
return { score, reason: reason.join(',') };
}
// Style générique (pas de personnalité visible)
const personalityWords = (personality.vocabulairePref || '').toLowerCase().split(',');
const contentLower = content.toLowerCase();
const personalityFound = personalityWords.some(word =>
word.trim() && contentLower.includes(word.trim())
);
if (!personalityFound && content.length > 50) {
score += 0.4;
reason.push('style_générique');
}
// Niveau technique inadapté
if (personality.niveauTechnique === 'accessible' && /\b(optimisation|implémentation|méthodologie)\b/i.test(content)) {
score += 0.3;
reason.push('trop_technique');
}
return { score: Math.min(1, score), reason: reason.join(',') };
}
module.exports = {
applySelectiveLayer, // ← MAIN ENTRY POINT MODULAIRE
applyTechnicalEnhancement,
applyTransitionEnhancement,
applyStyleEnhancement,
applyAllSelectiveLayers,
analyzeEnhancementNeeds,
selectOptimalLLM
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/SelectiveLayers.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// SELECTIVE LAYERS - COUCHES COMPOSABLES
// Responsabilité: Stacks prédéfinis et couches adaptatives pour selective enhancement
// Architecture: Composable layers avec orchestration intelligente
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { applySelectiveLayer } = require('./SelectiveCore');
/**
* STACKS PRÉDÉFINIS SELECTIVE ENHANCEMENT
*/
const PREDEFINED_STACKS = {
// Stack léger - Amélioration technique uniquement
lightEnhancement: {
name: 'lightEnhancement',
description: 'Amélioration technique légère avec OpenAI',
layers: [
{ type: 'technical', llm: 'openai', intensity: 0.7 }
],
layersCount: 1
},
// Stack standard - Technique + Transitions
standardEnhancement: {
name: 'standardEnhancement',
description: 'Amélioration technique et fluidité (OpenAI + Gemini)',
layers: [
{ type: 'technical', llm: 'openai', intensity: 0.9 },
{ type: 'transitions', llm: 'gemini', intensity: 0.8 }
],
layersCount: 2
},
// Stack complet - Toutes couches séquentielles
fullEnhancement: {
name: 'fullEnhancement',
description: 'Enhancement complet multi-LLM (OpenAI + Gemini + Mistral)',
layers: [
{ type: 'technical', llm: 'openai', intensity: 1.0 },
{ type: 'transitions', llm: 'gemini', intensity: 0.9 },
{ type: 'style', llm: 'mistral', intensity: 0.8 }
],
layersCount: 3
},
// Stack personnalité - Style prioritaire
personalityFocus: {
name: 'personalityFocus',
description: 'Focus personnalité et style avec Mistral + technique légère',
layers: [
{ type: 'style', llm: 'mistral', intensity: 1.2 },
{ type: 'technical', llm: 'openai', intensity: 0.6 }
],
layersCount: 2
},
// Stack fluidité - Transitions prioritaires
fluidityFocus: {
name: 'fluidityFocus',
description: 'Focus fluidité avec Gemini + enhancements légers',
layers: [
{ type: 'transitions', llm: 'gemini', intensity: 1.1 },
{ type: 'technical', llm: 'openai', intensity: 0.7 },
{ type: 'style', llm: 'mistral', intensity: 0.6 }
],
layersCount: 3
}
};
/**
* APPLIQUER STACK PRÉDÉFINI
*/
async function applyPredefinedStack(content, stackName, config = {}) {
return await tracer.run('SelectiveLayers.applyPredefinedStack()', async () => {
const stack = PREDEFINED_STACKS[stackName];
if (!stack) {
throw new Error(`Stack selective prédéfini inconnu: ${stackName}. Disponibles: ${Object.keys(PREDEFINED_STACKS).join(', ')}`);
}
await tracer.annotate({
selectivePredefinedStack: true,
stackName,
layersCount: stack.layersCount,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`📦 APPLICATION STACK SELECTIVE: ${stack.name} (${stack.layersCount} couches)`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Description: ${stack.description}`, 'INFO');
try {
let currentContent = content;
const stackStats = {
stackName,
layers: [],
totalModifications: 0,
totalDuration: 0,
success: true
};
// Appliquer chaque couche séquentiellement
for (let i = 0; i < stack.layers.length; i++) {
const layer = stack.layers[i];
try {
logSh(` 🔧 Couche ${i + 1}/${stack.layersCount}: ${layer.type} (${layer.llm})`, 'DEBUG');
const layerResult = await applySelectiveLayer(currentContent, {
...config,
layerType: layer.type,
llmProvider: layer.llm,
intensity: layer.intensity,
analysisMode: true
});
currentContent = layerResult.content;
stackStats.layers.push({
order: i + 1,
type: layer.type,
llm: layer.llm,
intensity: layer.intensity,
elementsEnhanced: layerResult.stats.elementsEnhanced,
duration: layerResult.stats.duration,
success: !layerResult.stats.fallback
});
stackStats.totalModifications += layerResult.stats.elementsEnhanced;
stackStats.totalDuration += layerResult.stats.duration;
logSh(` ✅ Couche ${layer.type}: ${layerResult.stats.elementsEnhanced} améliorations`, 'DEBUG');
} catch (layerError) {
logSh(` ❌ Couche ${layer.type} échouée: ${layerError.message}`, 'ERROR');
stackStats.layers.push({
order: i + 1,
type: layer.type,
llm: layer.llm,
error: layerError.message,
duration: 0,
success: false
});
// Continuer avec les autres couches
}
}
const duration = Date.now() - startTime;
const successfulLayers = stackStats.layers.filter(l => l.success).length;
logSh(`✅ STACK SELECTIVE ${stackName}: ${successfulLayers}/${stack.layersCount} couches | ${stackStats.totalModifications} modifications (${duration}ms)`, 'INFO');
await tracer.event('Stack selective appliqué', { ...stackStats, totalDuration: duration });
return {
content: currentContent,
stats: { ...stackStats, totalDuration: duration },
original: content,
stackApplied: stackName
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ STACK SELECTIVE ${stackName} ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
return {
content,
stats: { stackName, error: error.message, duration, success: false },
original: content,
fallback: true
};
}
}, { content: Object.keys(content), stackName, config });
}
/**
* APPLIQUER COUCHES ADAPTATIVES
*/
async function applyAdaptiveLayers(content, config = {}) {
return await tracer.run('SelectiveLayers.applyAdaptiveLayers()', async () => {
const {
maxIntensity = 1.0,
analysisThreshold = 0.4,
csvData = null
} = config;
await tracer.annotate({
selectiveAdaptiveLayers: true,
maxIntensity,
analysisThreshold,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🧠 APPLICATION COUCHES ADAPTATIVES SELECTIVE`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Seuil: ${analysisThreshold}`, 'INFO');
try {
// 1. Analyser besoins de chaque type de couche
const needsAnalysis = await analyzeSelectiveNeeds(content, csvData);
logSh(` 📋 Analyse besoins: Tech=${needsAnalysis.technical.score.toFixed(2)} | Trans=${needsAnalysis.transitions.score.toFixed(2)} | Style=${needsAnalysis.style.score.toFixed(2)}`, 'DEBUG');
// 2. Déterminer couches à appliquer selon scores
const layersToApply = [];
if (needsAnalysis.technical.needed && needsAnalysis.technical.score > analysisThreshold) {
layersToApply.push({
type: 'technical',
llm: 'openai',
intensity: Math.min(maxIntensity, needsAnalysis.technical.score * 1.2),
priority: 1
});
}
if (needsAnalysis.transitions.needed && needsAnalysis.transitions.score > analysisThreshold) {
layersToApply.push({
type: 'transitions',
llm: 'gemini',
intensity: Math.min(maxIntensity, needsAnalysis.transitions.score * 1.1),
priority: 2
});
}
if (needsAnalysis.style.needed && needsAnalysis.style.score > analysisThreshold) {
layersToApply.push({
type: 'style',
llm: 'mistral',
intensity: Math.min(maxIntensity, needsAnalysis.style.score),
priority: 3
});
}
if (layersToApply.length === 0) {
logSh(`✅ COUCHES ADAPTATIVES: Aucune amélioration nécessaire`, 'INFO');
return {
content,
stats: {
adaptive: true,
layersApplied: 0,
analysisOnly: true,
duration: Date.now() - startTime
}
};
}
// 3. Appliquer couches par ordre de priorité
layersToApply.sort((a, b) => a.priority - b.priority);
logSh(` 🎯 Couches sélectionnées: ${layersToApply.map(l => `${l.type}(${l.intensity.toFixed(1)})`).join(' → ')}`, 'INFO');
let currentContent = content;
const adaptiveStats = {
layersAnalyzed: 3,
layersApplied: layersToApply.length,
layers: [],
totalModifications: 0,
adaptive: true
};
for (const layer of layersToApply) {
try {
logSh(` 🔧 Couche adaptative: ${layer.type} (intensité: ${layer.intensity.toFixed(1)})`, 'DEBUG');
const layerResult = await applySelectiveLayer(currentContent, {
...config,
layerType: layer.type,
llmProvider: layer.llm,
intensity: layer.intensity,
analysisMode: true
});
currentContent = layerResult.content;
adaptiveStats.layers.push({
type: layer.type,
llm: layer.llm,
intensity: layer.intensity,
elementsEnhanced: layerResult.stats.elementsEnhanced,
duration: layerResult.stats.duration,
success: !layerResult.stats.fallback
});
adaptiveStats.totalModifications += layerResult.stats.elementsEnhanced;
} catch (layerError) {
logSh(` ❌ Couche adaptative ${layer.type} échouée: ${layerError.message}`, 'ERROR');
adaptiveStats.layers.push({
type: layer.type,
error: layerError.message,
success: false
});
}
}
const duration = Date.now() - startTime;
const successfulLayers = adaptiveStats.layers.filter(l => l.success).length;
logSh(`✅ COUCHES ADAPTATIVES: ${successfulLayers}/${layersToApply.length} appliquées | ${adaptiveStats.totalModifications} modifications (${duration}ms)`, 'INFO');
await tracer.event('Couches adaptatives appliquées', { ...adaptiveStats, totalDuration: duration });
return {
content: currentContent,
stats: { ...adaptiveStats, totalDuration: duration },
original: content
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ COUCHES ADAPTATIVES ÉCHOUÉES après ${duration}ms: ${error.message}`, 'ERROR');
return {
content,
stats: { adaptive: true, error: error.message, duration },
original: content,
fallback: true
};
}
}, { content: Object.keys(content), config });
}
/**
* PIPELINE COUCHES PERSONNALISÉ
*/
async function applyLayerPipeline(content, layerSequence, config = {}) {
return await tracer.run('SelectiveLayers.applyLayerPipeline()', async () => {
if (!Array.isArray(layerSequence) || layerSequence.length === 0) {
throw new Error('Séquence de couches invalide ou vide');
}
await tracer.annotate({
selectiveLayerPipeline: true,
pipelineLength: layerSequence.length,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🔄 PIPELINE COUCHES SELECTIVE PERSONNALISÉ: ${layerSequence.length} étapes`, 'INFO');
try {
let currentContent = content;
const pipelineStats = {
pipelineLength: layerSequence.length,
steps: [],
totalModifications: 0,
success: true
};
for (let i = 0; i < layerSequence.length; i++) {
const step = layerSequence[i];
try {
logSh(` 📍 Étape ${i + 1}/${layerSequence.length}: ${step.type} (${step.llm || 'auto'})`, 'DEBUG');
const stepResult = await applySelectiveLayer(currentContent, {
...config,
...step
});
currentContent = stepResult.content;
pipelineStats.steps.push({
order: i + 1,
...step,
elementsEnhanced: stepResult.stats.elementsEnhanced,
duration: stepResult.stats.duration,
success: !stepResult.stats.fallback
});
pipelineStats.totalModifications += stepResult.stats.elementsEnhanced;
} catch (stepError) {
logSh(` ❌ Étape ${i + 1} échouée: ${stepError.message}`, 'ERROR');
pipelineStats.steps.push({
order: i + 1,
...step,
error: stepError.message,
success: false
});
}
}
const duration = Date.now() - startTime;
const successfulSteps = pipelineStats.steps.filter(s => s.success).length;
logSh(`✅ PIPELINE SELECTIVE: ${successfulSteps}/${layerSequence.length} étapes | ${pipelineStats.totalModifications} modifications (${duration}ms)`, 'INFO');
await tracer.event('Pipeline selective appliqué', { ...pipelineStats, totalDuration: duration });
return {
content: currentContent,
stats: { ...pipelineStats, totalDuration: duration },
original: content
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ PIPELINE SELECTIVE ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
return {
content,
stats: { error: error.message, duration, success: false },
original: content,
fallback: true
};
}
}, { content: Object.keys(content), layerSequence, config });
}
// ============= HELPER FUNCTIONS =============
/**
* Analyser besoins selective enhancement
*/
async function analyzeSelectiveNeeds(content, csvData) {
const analysis = {
technical: { needed: false, score: 0, elements: [] },
transitions: { needed: false, score: 0, elements: [] },
style: { needed: false, score: 0, elements: [] }
};
// Analyser chaque élément pour tous types de besoins
Object.entries(content).forEach(([tag, text]) => {
// Analyse technique (import depuis SelectiveCore logic)
const technicalNeed = assessTechnicalNeed(text, csvData);
if (technicalNeed.score > 0.3) {
analysis.technical.needed = true;
analysis.technical.score += technicalNeed.score;
analysis.technical.elements.push({ tag, score: technicalNeed.score });
}
// Analyse transitions
const transitionNeed = assessTransitionNeed(text);
if (transitionNeed.score > 0.3) {
analysis.transitions.needed = true;
analysis.transitions.score += transitionNeed.score;
analysis.transitions.elements.push({ tag, score: transitionNeed.score });
}
// Analyse style
const styleNeed = assessStyleNeed(text, csvData?.personality);
if (styleNeed.score > 0.3) {
analysis.style.needed = true;
analysis.style.score += styleNeed.score;
analysis.style.elements.push({ tag, score: styleNeed.score });
}
});
// Normaliser scores
const elementCount = Object.keys(content).length;
analysis.technical.score = analysis.technical.score / elementCount;
analysis.transitions.score = analysis.transitions.score / elementCount;
analysis.style.score = analysis.style.score / elementCount;
return analysis;
}
/**
* Évaluer besoin technique (simplifié de SelectiveCore)
*/
function assessTechnicalNeed(content, csvData) {
let score = 0;
// Manque de termes techniques spécifiques
if (csvData?.mc0) {
const technicalTerms = ['dibond', 'pmma', 'aluminium', 'fraisage', 'impression', 'gravure'];
const foundTerms = technicalTerms.filter(term => content.toLowerCase().includes(term));
if (foundTerms.length === 0 && content.length > 100) {
score += 0.4;
}
}
// Vocabulaire générique
const genericWords = ['produit', 'solution', 'service', 'qualité'];
const genericCount = genericWords.filter(word => content.toLowerCase().includes(word)).length;
if (genericCount > 2) score += 0.3;
return { score: Math.min(1, score) };
}
/**
* Évaluer besoin transitions (simplifié)
*/
function assessTransitionNeed(content) {
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) return { score: 0 };
let score = 0;
// Connecteurs répétitifs
const connectors = ['par ailleurs', 'en effet', 'de plus'];
let repetitions = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(connector, 'gi')) || []);
if (matches.length > 1) repetitions++;
});
if (repetitions > 1) score += 0.4;
return { score: Math.min(1, score) };
}
/**
* Évaluer besoin style (simplifié)
*/
function assessStyleNeed(content, personality) {
let score = 0;
if (!personality) {
score += 0.2;
return { score };
}
// Style générique
const personalityWords = (personality.vocabulairePref || '').toLowerCase().split(',');
const personalityFound = personalityWords.some(word =>
word.trim() && content.toLowerCase().includes(word.trim())
);
if (!personalityFound && content.length > 50) score += 0.4;
return { score: Math.min(1, score) };
}
/**
* Obtenir stacks disponibles
*/
function getAvailableStacks() {
return Object.values(PREDEFINED_STACKS);
}
module.exports = {
// Main functions
applyPredefinedStack,
applyAdaptiveLayers,
applyLayerPipeline,
// Utils
getAvailableStacks,
analyzeSelectiveNeeds,
// Constants
PREDEFINED_STACKS
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/main_modulaire.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// MAIN MODULAIRE - PIPELINE ARCHITECTURALE MODERNE
// Responsabilité: Orchestration workflow avec architecture modulaire complète
// Usage: node main_modulaire.js [rowNumber] [stackType]
// ========================================
const { logSh } = require('./ErrorReporting');
const { tracer } = require('./trace');
// Imports pipeline de base
const { readInstructionsData, selectPersonalityWithAI, getPersonalities } = require('./BrainConfig');
const { extractElements, buildSmartHierarchy } = require('./ElementExtraction');
const { generateMissingKeywords } = require('./MissingKeywords');
const { generateSimple } = require('./ContentGeneration');
const { injectGeneratedContent } = require('./ContentAssembly');
const { compileAndStoreArticle } = require('./ArticleStorage');
// Imports modules modulaires
const { applySelectiveLayer } = require('./selective-enhancement/SelectiveCore');
const {
applyPredefinedStack,
applyAdaptiveLayers,
getAvailableStacks
} = require('./selective-enhancement/SelectiveLayers');
const {
applyAdversarialLayer
} = require('./adversarial-generation/AdversarialCore');
const {
applyPredefinedStack: applyAdversarialStack
} = require('./adversarial-generation/AdversarialLayers');
/**
* WORKFLOW MODULAIRE PRINCIPAL
*/
async function handleModularWorkflow(config = {}) {
return await tracer.run('MainModulaire.handleModularWorkflow()', async () => {
const {
rowNumber = 2,
selectiveStack = 'standardEnhancement', // lightEnhancement, standardEnhancement, fullEnhancement, personalityFocus, fluidityFocus, adaptive
adversarialMode = 'light', // none, light, standard, heavy, adaptive
source = 'main_modulaire'
} = config;
await tracer.annotate({
modularWorkflow: true,
rowNumber,
selectiveStack,
adversarialMode,
source
});
const startTime = Date.now();
logSh(`🚀 WORKFLOW MODULAIRE DÉMARRÉ`, 'INFO');
logSh(` 📊 Ligne: ${rowNumber} | Selective: ${selectiveStack} | Adversarial: ${adversarialMode}`, 'INFO');
try {
// ========================================
// PHASE 1: PRÉPARATION DONNÉES
// ========================================
logSh(`📋 PHASE 1: Préparation données`, 'INFO');
const csvData = await readInstructionsData(rowNumber);
if (!csvData) {
throw new Error(`Impossible de lire les données ligne ${rowNumber}`);
}
const personalities = await getPersonalities();
const selectedPersonality = await selectPersonalityWithAI(
csvData.mc0,
csvData.t0,
personalities
);
csvData.personality = selectedPersonality;
logSh(` ✅ Données: ${csvData.mc0} | Personnalité: ${selectedPersonality.nom}`, 'DEBUG');
// ========================================
// PHASE 2: EXTRACTION ÉLÉMENTS
// ========================================
logSh(`📝 PHASE 2: Extraction éléments XML`, 'INFO');
const elements = await extractElements(csvData.xmlTemplate, csvData);
logSh(`${elements.length} éléments extraits`, 'DEBUG');
// ========================================
// PHASE 3: GÉNÉRATION MOTS-CLÉS MANQUANTS
// ========================================
logSh(`🔍 PHASE 3: Génération mots-clés manquants`, 'INFO');
const finalElements = await generateMissingKeywords(elements, csvData);
logSh(` ✅ Mots-clés complétés`, 'DEBUG');
// ========================================
// PHASE 4: CONSTRUCTION HIÉRARCHIE
// ========================================
logSh(`🏗️ PHASE 4: Construction hiérarchie`, 'INFO');
const hierarchy = await buildSmartHierarchy(finalElements);
logSh(`${Object.keys(hierarchy).length} sections hiérarchisées`, 'DEBUG');
// ========================================
// PHASE 5: GÉNÉRATION CONTENU DE BASE
// ========================================
logSh(`💫 PHASE 5: Génération contenu de base`, 'INFO');
const generatedContent = await generateSimple(hierarchy, csvData);
logSh(`${Object.keys(generatedContent).length} éléments générés`, 'DEBUG');
// ========================================
// PHASE 6: SELECTIVE ENHANCEMENT MODULAIRE
// ========================================
logSh(`🔧 PHASE 6: Selective Enhancement Modulaire (${selectiveStack})`, 'INFO');
let selectiveResult;
switch (selectiveStack) {
case 'adaptive':
selectiveResult = await applyAdaptiveLayers(generatedContent, {
maxIntensity: 1.1,
analysisThreshold: 0.3,
csvData
});
break;
case 'technical':
case 'transitions':
case 'style':
selectiveResult = await applySelectiveLayer(generatedContent, {
layerType: selectiveStack,
llmProvider: 'auto',
intensity: 1.0,
csvData
});
break;
default:
// Stack prédéfini
selectiveResult = await applyPredefinedStack(generatedContent, selectiveStack, {
csvData,
analysisMode: true
});
}
const enhancedContent = selectiveResult.content;
logSh(` ✅ Selective: ${selectiveResult.stats.elementsEnhanced || selectiveResult.stats.totalModifications || 0} améliorations`, 'INFO');
// ========================================
// PHASE 7: ADVERSARIAL ENHANCEMENT (OPTIONNEL)
// ========================================
let finalContent = enhancedContent;
let adversarialStats = null;
if (adversarialMode !== 'none') {
logSh(`🎯 PHASE 7: Adversarial Enhancement (${adversarialMode})`, 'INFO');
let adversarialResult;
switch (adversarialMode) {
case 'adaptive':
// Utiliser adversarial adaptatif
adversarialResult = await applyAdversarialLayer(enhancedContent, {
detectorTarget: 'general',
method: 'hybrid',
intensity: 0.8,
analysisMode: true
});
break;
case 'light':
case 'standard':
case 'heavy':
// Utiliser stack adversarial prédéfini
const stackMapping = {
light: 'lightDefense',
standard: 'standardDefense',
heavy: 'heavyDefense'
};
adversarialResult = await applyAdversarialStack(enhancedContent, stackMapping[adversarialMode], {
csvData
});
break;
}
if (adversarialResult && !adversarialResult.fallback) {
finalContent = adversarialResult.content;
adversarialStats = adversarialResult.stats;
logSh(` ✅ Adversarial: ${adversarialStats.elementsModified || adversarialStats.totalModifications || 0} modifications`, 'INFO');
} else {
logSh(` ⚠️ Adversarial fallback: contenu selective préservé`, 'WARNING');
}
}
// ========================================
// PHASE 8: ASSEMBLAGE ET STOCKAGE
// ========================================
logSh(`🔗 PHASE 8: Assemblage et stockage`, 'INFO');
const assembledContent = await injectGeneratedContent(finalContent, csvData.xmlTemplate);
const storageResult = await compileAndStoreArticle(assembledContent, {
...csvData,
source: `${source}_${selectiveStack}${adversarialMode !== 'none' ? `_${adversarialMode}` : ''}`
});
logSh(` ✅ Stocké: ${storageResult.compiledLength} caractères`, 'DEBUG');
// ========================================
// RÉSUMÉ FINAL
// ========================================
const totalDuration = Date.now() - startTime;
const finalStats = {
rowNumber,
selectiveStack,
adversarialMode,
totalDuration,
elementsGenerated: Object.keys(generatedContent).length,
selectiveEnhancements: selectiveResult.stats.elementsEnhanced || selectiveResult.stats.totalModifications || 0,
adversarialModifications: adversarialStats?.elementsModified || adversarialStats?.totalModifications || 0,
finalLength: storageResult.compiledLength,
personality: selectedPersonality.nom,
source
};
logSh(`✅ WORKFLOW MODULAIRE TERMINÉ (${totalDuration}ms)`, 'INFO');
logSh(` 📊 ${finalStats.elementsGenerated} générés | ${finalStats.selectiveEnhancements} selective | ${finalStats.adversarialModifications} adversarial`, 'INFO');
logSh(` 🎭 Personnalité: ${finalStats.personality} | Taille finale: ${finalStats.finalLength} chars`, 'INFO');
await tracer.event('Workflow modulaire terminé', finalStats);
return {
success: true,
stats: finalStats,
content: finalContent,
assembledContent,
storageResult,
selectiveResult,
adversarialResult: adversarialStats ? { stats: adversarialStats } : null
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ WORKFLOW MODULAIRE ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
logSh(`Stack trace: ${error.stack}`, 'ERROR');
await tracer.event('Workflow modulaire échoué', {
error: error.message,
duration,
rowNumber,
selectiveStack,
adversarialMode
});
throw error;
}
}, { config });
}
/**
* BENCHMARK COMPARATIF STACKS
*/
async function benchmarkStacks(rowNumber = 2) {
console.log('\n⚡ === BENCHMARK STACKS MODULAIRES ===\n');
const stacks = getAvailableStacks();
const adversarialModes = ['none', 'light', 'standard'];
const results = [];
for (const stack of stacks.slice(0, 3)) { // Tester 3 stacks principaux
for (const advMode of adversarialModes.slice(0, 2)) { // 2 modes adversarial
console.log(`🧪 Test: ${stack.name} + adversarial ${advMode}`);
try {
const startTime = Date.now();
const result = await handleModularWorkflow({
rowNumber,
selectiveStack: stack.name,
adversarialMode: advMode,
source: 'benchmark'
});
const duration = Date.now() - startTime;
results.push({
stack: stack.name,
adversarial: advMode,
duration,
success: true,
selectiveEnhancements: result.stats.selectiveEnhancements,
adversarialModifications: result.stats.adversarialModifications,
finalLength: result.stats.finalLength
});
console.log(`${duration}ms | ${result.stats.selectiveEnhancements} selective | ${result.stats.adversarialModifications} adversarial`);
} catch (error) {
results.push({
stack: stack.name,
adversarial: advMode,
success: false,
error: error.message
});
console.log(` ❌ Échoué: ${error.message}`);
}
}
}
// Résumé benchmark
console.log('\n📊 RÉSUMÉ BENCHMARK:');
const successful = results.filter(r => r.success);
if (successful.length > 0) {
const avgDuration = successful.reduce((sum, r) => sum + r.duration, 0) / successful.length;
const bestPerf = successful.reduce((best, r) => r.duration < best.duration ? r : best);
const mostEnhancements = successful.reduce((best, r) =>
(r.selectiveEnhancements + r.adversarialModifications) > (best.selectiveEnhancements + best.adversarialModifications) ? r : best
);
console.log(` ⚡ Durée moyenne: ${avgDuration.toFixed(0)}ms`);
console.log(` 🏆 Meilleure perf: ${bestPerf.stack} + ${bestPerf.adversarial} (${bestPerf.duration}ms)`);
console.log(` 🔥 Plus d'améliorations: ${mostEnhancements.stack} + ${mostEnhancements.adversarial} (${mostEnhancements.selectiveEnhancements + mostEnhancements.adversarialModifications})`);
}
return results;
}
/**
* INTERFACE LIGNE DE COMMANDE
*/
async function main() {
const args = process.argv.slice(2);
const command = args[0] || 'workflow';
try {
switch (command) {
case 'workflow':
const rowNumber = parseInt(args[1]) || 2;
const selectiveStack = args[2] || 'standardEnhancement';
const adversarialMode = args[3] || 'light';
console.log(`\n🚀 Exécution workflow modulaire:`);
console.log(` 📊 Ligne: ${rowNumber}`);
console.log(` 🔧 Stack selective: ${selectiveStack}`);
console.log(` 🎯 Mode adversarial: ${adversarialMode}`);
const result = await handleModularWorkflow({
rowNumber,
selectiveStack,
adversarialMode
});
console.log('\n✅ WORKFLOW MODULAIRE RÉUSSI');
console.log(`📈 Stats: ${JSON.stringify(result.stats, null, 2)}`);
break;
case 'benchmark':
const benchRowNumber = parseInt(args[1]) || 2;
console.log(`\n⚡ Benchmark stacks (ligne ${benchRowNumber})`);
const benchResults = await benchmarkStacks(benchRowNumber);
console.log('\n📊 Résultats complets:');
console.table(benchResults);
break;
case 'stacks':
console.log('\n📦 STACKS SELECTIVE DISPONIBLES:');
const availableStacks = getAvailableStacks();
availableStacks.forEach(stack => {
console.log(`\n 🔧 ${stack.name}:`);
console.log(` 📝 ${stack.description}`);
console.log(` 📊 ${stack.layersCount} couches`);
console.log(` 🎯 Couches: ${stack.layers ? stack.layers.map(l => `${l.type}(${l.llm})`).join(' → ') : 'N/A'}`);
});
console.log('\n🎯 MODES ADVERSARIAL DISPONIBLES:');
console.log(' - none: Pas d\'adversarial');
console.log(' - light: Défense légère');
console.log(' - standard: Défense standard');
console.log(' - heavy: Défense intensive');
console.log(' - adaptive: Adaptatif intelligent');
break;
case 'help':
default:
console.log('\n🔧 === MAIN MODULAIRE - USAGE ===');
console.log('\nCommandes disponibles:');
console.log(' workflow [ligne] [stack] [adversarial] - Exécuter workflow complet');
console.log(' benchmark [ligne] - Benchmark stacks');
console.log(' stacks - Lister stacks disponibles');
console.log(' help - Afficher cette aide');
console.log('\nExemples:');
console.log(' node main_modulaire.js workflow 2 fullEnhancement standard');
console.log(' node main_modulaire.js workflow 3 adaptive light');
console.log(' node main_modulaire.js benchmark 2');
console.log(' node main_modulaire.js stacks');
break;
}
} catch (error) {
console.error('\n❌ ERREUR MAIN MODULAIRE:', error.message);
console.error(error.stack);
process.exit(1);
}
}
// Export pour usage programmatique
module.exports = {
handleModularWorkflow,
benchmarkStacks
};
// Exécution CLI si appelé directement
if (require.main === module) {
main().catch(error => {
console.error('❌ ERREUR FATALE:', error.message);
process.exit(1);
});
}
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/ManualTrigger.js │
└────────────────────────────────────────────────────────────────────┘
*/
const { logSh } = require('./ErrorReporting'); // Using unified logSh from ErrorReporting
/**
* 🚀 TRIGGER MANUEL - Lit ligne 2 et lance le workflow
* Exécute cette fonction depuis l'éditeur Apps Script
*/
function runWorkflowLigne(numeroLigne = 2) {
cleanLogSheet(); // Nettoie les logs pour ce test
try {
logSh('🎬 >>> DÉMARRAGE WORKFLOW MANUEL <<<', 'INFO');
// 1. LIRE AUTOMATIQUEMENT LA LIGNE INDIQUÉ
const csvData = readCSVDataFromRow(numeroLigne);
logSh(`✅ Données lues - MC0: ${csvData.mc0}`, 'INFO');
logSh(`✅ Titre: ${csvData.t0}`, 'INFO');
logSh(`✅ Personnalité: ${csvData.personality.nom}`, 'INFO');
// 2. XML TEMPLATE SIMPLE POUR TEST (ou lit depuis Digital Ocean si configuré)
const xmlTemplate = getXMLTemplateForTest(csvData);
logSh(`✅ XML template: ${xmlTemplate.length} caractères`, 'INFO');
// 3. 🎯 LANCER LE WORKFLOW PRINCIPAL
const workflowData = {
csvData: csvData,
xmlTemplate: Utilities.base64Encode(xmlTemplate),
source: 'manuel_ligne2'
};
const result = handleFullWorkflow(workflowData);
logSh('🏆 === WORKFLOW MANUEL TERMINÉ ===', 'INFO');
// ← EXTRAIRE LES VRAIES DONNÉES
let actualData;
if (result && result.getContentText) {
// C'est un ContentService, extraire le JSON
actualData = JSON.parse(result.getContentText());
} else {
actualData = result;
}
logSh(`Type result: ${typeof result}`, 'DEBUG');
logSh(`Result keys: ${Object.keys(result || {})}`, 'DEBUG');
logSh(`ActualData keys: ${Object.keys(actualData || {})}`, 'DEBUG');
logSh(`ActualData: ${JSON.stringify(actualData)}`, 'DEBUG');
if (actualData && actualData.stats) {
logSh(`📊 Éléments générés: ${actualData.stats.contentPieces}`, 'INFO');
logSh(`📝 Nombre de mots: ${actualData.stats.wordCount}`, 'INFO');
} else {
logSh('⚠️ Format résultat inattendu', 'WARNING');
logSh('ActualData: ' + JSON.stringify(actualData, null, 2), 'DEBUG'); // Using logSh instead of console.log
}
return actualData;
} catch (error) {
logSh(`❌ ERREUR WORKFLOW MANUEL: ${error.toString()}`, 'ERROR');
logSh(`Stack: ${error.stack}`, 'ERROR');
throw error;
}
}
/**
* HELPER - Lire CSV depuis une ligne spécifique
*/
function readCSVDataFromRow(rowNumber) {
const sheetId = '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
const spreadsheet = SpreadsheetApp.openById(sheetId);
const articlesSheet = spreadsheet.getSheetByName('instructions');
// Lire la ligne complète (colonnes A à H)
const range = articlesSheet.getRange(rowNumber, 1, 1, 9);
const [slug, t0, mc0, tMinus1, lMinus1, mcPlus1, tPlus1, lPlus1, xmlFileName] = range.getValues()[0];
logSh(`📖 Lecture ligne ${rowNumber}: ${slug}`, 'DEBUG');
// Récupérer personnalités et sélectionner automatiquement
const personalitiesSheet = spreadsheet.getSheetByName('Personnalites');
const personalities = getPersonalities(personalitiesSheet);
const selectedPersonality = selectPersonalityWithAI(mc0, t0, personalities);
return {
rowNumber: rowNumber,
slug: slug || 'test-slug',
t0: t0 || 'Titre par défaut',
mc0: mc0 || 'mot-clé test',
tMinus1: tMinus1 || 'parent',
lMinus1: lMinus1 || '/parent',
mcPlus1: mcPlus1 || 'mot1,mot2,mot3,mot4',
tPlus1: tPlus1 || 'Titre1,Titre2,Titre3,Titre4',
lPlus1: lPlus1 || '/lien1,/lien2,/lien3,/lien4',
personality: selectedPersonality,
xmlFileName: xmlFileName ? xmlFileName.toString().trim() : null
};
}
/**
* HELPER - XML Template simple pour test (ou depuis Digital Ocean)
*/
function getXMLTemplateForTest(csvData) {
logSh("csvData.xmlFileName: " + csvData.xmlFileName, 'DEBUG'); // Using logSh instead of console.log
if (csvData.xmlFileName) {
logSh("Tentative Digital Ocean...", 'INFO'); // Using logSh instead of console.log
try {
return fetchXMLFromDigitalOceanSimple(csvData.xmlFileName);
} catch (error) {
// ← ENLÈVE LE CATCH SILENCIEUX
logSh("Erreur DO: " + error.toString(), 'WARNING'); // Using logSh instead of console.log
logSh(`❌ ERREUR DO DÉTAILLÉE: ${error.toString()}`, 'ERROR');
// Continue sans Digital Ocean
}
}
logSh("❌ FATAL: Aucun template XML disponible", 'ERROR');
throw new Error("FATAL: Template XML indisponible (Digital Ocean inaccessible + pas de fallback) - arrêt du workflow");
}
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/selective-enhancement/demo-modulaire.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// DÉMONSTRATION ARCHITECTURE MODULAIRE SELECTIVE
// Usage: node lib/selective-enhancement/demo-modulaire.js
// Objectif: Valider l'intégration modulaire selective enhancement
// ========================================
const { logSh } = require('../ErrorReporting');
// Import modules selective modulaires
const { applySelectiveLayer } = require('./SelectiveCore');
const {
applyPredefinedStack,
applyAdaptiveLayers,
getAvailableStacks
} = require('./SelectiveLayers');
const {
analyzeTechnicalQuality,
analyzeTransitionFluidity,
analyzeStyleConsistency,
generateImprovementReport
} = require('./SelectiveUtils');
/**
* EXEMPLE D'UTILISATION MODULAIRE SELECTIVE
*/
async function demoModularSelective() {
console.log('\n🔧 === DÉMONSTRATION SELECTIVE MODULAIRE ===\n');
// Contenu d'exemple avec problèmes de qualité
const exempleContenu = {
'|Titre_Principal_1|': 'Guide complet pour choisir votre plaque personnalisée',
'|Introduction_1|': 'La personnalisation d\'une plaque signalétique représente un enjeu important pour votre entreprise. Cette solution permet de créer une identité visuelle.',
'|Texte_1|': 'Il est important de noter que les matériaux utilisés sont de qualité. Par ailleurs, la qualité est bonne. En effet, nos solutions sont bonnes et robustes. Par ailleurs, cela fonctionne bien.',
'|FAQ_Question_1|': 'Quels sont les matériaux disponibles ?',
'|FAQ_Reponse_1|': 'Nos matériaux sont de qualité : ils conviennent parfaitement. Ces solutions garantissent une qualité et un rendu optimal.'
};
console.log('📊 CONTENU ORIGINAL:');
Object.entries(exempleContenu).forEach(([tag, content]) => {
console.log(` ${tag}: "${content}"`);
});
// Analyser qualité originale
const fullOriginal = Object.values(exempleContenu).join(' ');
const qualiteOriginale = {
technical: analyzeTechnicalQuality(fullOriginal, ['dibond', 'aluminium', 'pmma', 'impression']),
transitions: analyzeTransitionFluidity(fullOriginal),
style: analyzeStyleConsistency(fullOriginal)
};
console.log(`\n📈 QUALITÉ ORIGINALE:`);
console.log(` 🔧 Technique: ${qualiteOriginale.technical.score}/100`);
console.log(` 🔗 Transitions: ${qualiteOriginale.transitions.score}/100`);
console.log(` 🎨 Style: ${qualiteOriginale.style.score}/100`);
try {
// ========================================
// TEST 1: COUCHE TECHNIQUE SEULE
// ========================================
console.log('\n🔧 TEST 1: Application couche technique');
const result1 = await applySelectiveLayer(exempleContenu, {
layerType: 'technical',
llmProvider: 'gpt4',
intensity: 0.9,
csvData: {
personality: { nom: 'Marc', style: 'technique' },
mc0: 'plaque personnalisée'
}
});
console.log(`✅ Résultat: ${result1.stats.enhanced}/${result1.stats.processed} éléments améliorés`);
console.log(` ⏱️ Durée: ${result1.stats.duration}ms`);
// ========================================
// TEST 2: STACK PRÉDÉFINI
// ========================================
console.log('\n📦 TEST 2: Application stack prédéfini');
// Lister stacks disponibles
const stacks = getAvailableStacks();
console.log(' Stacks disponibles:');
stacks.forEach(stack => {
console.log(` - ${stack.name}: ${stack.description}`);
});
const result2 = await applyPredefinedStack(exempleContenu, 'standardEnhancement', {
csvData: {
personality: {
nom: 'Sophie',
style: 'professionnel',
vocabulairePref: 'signalétique,personnalisation,qualité,expertise',
niveauTechnique: 'standard'
},
mc0: 'plaque personnalisée'
}
});
console.log(`✅ Stack standard: ${result2.stats.totalModifications} modifications totales`);
console.log(` 📊 Couches: ${result2.stats.layers.filter(l => l.success).length}/${result2.stats.layers.length} réussies`);
// ========================================
// TEST 3: COUCHES ADAPTATIVES
// ========================================
console.log('\n🧠 TEST 3: Application couches adaptatives');
const result3 = await applyAdaptiveLayers(exempleContenu, {
maxIntensity: 1.2,
analysisThreshold: 0.3,
csvData: {
personality: {
nom: 'Laurent',
style: 'commercial',
vocabulairePref: 'expertise,solution,performance,innovation',
niveauTechnique: 'accessible'
},
mc0: 'signalétique personnalisée'
}
});
if (result3.stats.adaptive) {
console.log(`✅ Adaptatif: ${result3.stats.layersApplied} couches appliquées`);
console.log(` 📊 Modifications: ${result3.stats.totalModifications}`);
}
// ========================================
// COMPARAISON QUALITÉ FINALE
// ========================================
console.log('\n📊 ANALYSE QUALITÉ FINALE:');
const contenuFinal = result2.content; // Prendre résultat stack standard
const fullEnhanced = Object.values(contenuFinal).join(' ');
const qualiteFinale = {
technical: analyzeTechnicalQuality(fullEnhanced, ['dibond', 'aluminium', 'pmma', 'impression']),
transitions: analyzeTransitionFluidity(fullEnhanced),
style: analyzeStyleConsistency(fullEnhanced, result2.csvData?.personality)
};
console.log('\n📈 AMÉLIORATION QUALITÉ:');
console.log(` 🔧 Technique: ${qualiteOriginale.technical.score}${qualiteFinale.technical.score} (+${(qualiteFinale.technical.score - qualiteOriginale.technical.score).toFixed(1)})`);
console.log(` 🔗 Transitions: ${qualiteOriginale.transitions.score}${qualiteFinale.transitions.score} (+${(qualiteFinale.transitions.score - qualiteOriginale.transitions.score).toFixed(1)})`);
console.log(` 🎨 Style: ${qualiteOriginale.style.score}${qualiteFinale.style.score} (+${(qualiteFinale.style.score - qualiteOriginale.style.score).toFixed(1)})`);
// Rapport détaillé
const rapport = generateImprovementReport(exempleContenu, contenuFinal, 'selective');
console.log('\n📋 RAPPORT AMÉLIORATION:');
console.log(` 📈 Amélioration moyenne: ${rapport.summary.averageImprovement.toFixed(1)}%`);
console.log(` ✅ Éléments améliorés: ${rapport.summary.elementsImproved}/${rapport.summary.elementsProcessed}`);
if (rapport.details.recommendations.length > 0) {
console.log(` 💡 Recommandations: ${rapport.details.recommendations.join(', ')}`);
}
// ========================================
// EXEMPLES DE TRANSFORMATION
// ========================================
console.log('\n✨ EXEMPLES DE TRANSFORMATION:');
console.log('\n📝 INTRODUCTION:');
console.log('AVANT:', `"${exempleContenu['|Introduction_1|']}"`);
console.log('APRÈS:', `"${contenuFinal['|Introduction_1|']}"`);
console.log('\n📝 TEXTE PRINCIPAL:');
console.log('AVANT:', `"${exempleContenu['|Texte_1|']}"`);
console.log('APRÈS:', `"${contenuFinal['|Texte_1|']}"`);
console.log('\n✅ === DÉMONSTRATION SELECTIVE MODULAIRE TERMINÉE ===\n');
return {
success: true,
originalQuality: qualiteOriginale,
finalQuality: qualiteFinale,
improvementReport: rapport
};
} catch (error) {
console.error('\n❌ ERREUR DÉMONSTRATION:', error.message);
console.error(error.stack);
return { success: false, error: error.message };
}
}
/**
* EXEMPLE D'INTÉGRATION AVEC PIPELINE EXISTANTE
*/
async function demoIntegrationExistante() {
console.log('\n🔗 === DÉMONSTRATION INTÉGRATION PIPELINE ===\n');
// Simuler contenu venant de ContentGeneration.js (Level 1)
const contenuExistant = {
'|Titre_H1_1|': 'Solutions de plaques personnalisées professionnelles',
'|Meta_Description_1|': 'Découvrez notre gamme complète de plaques personnalisées pour tous vos besoins de signalétique professionnelle.',
'|Introduction_1|': 'Dans le domaine de la signalétique personnalisée, le choix des matériaux et des techniques de fabrication constitue un élément déterminant.',
'|Texte_Avantages_1|': 'Les avantages de nos solutions incluent la durabilité, la résistance aux intempéries et la possibilité de personnalisation complète.'
};
console.log('💼 SCÉNARIO: Application selective post-génération normale');
try {
console.log('\n🎯 Étape 1: Contenu généré par pipeline Level 1');
console.log(' ✅ Contenu de base: qualité préservée');
console.log('\n🎯 Étape 2: Application selective enhancement modulaire');
// Test avec couche technique puis style
let contenuEnhanced = contenuExistant;
// Amélioration technique
const resultTechnique = await applySelectiveLayer(contenuEnhanced, {
layerType: 'technical',
llmProvider: 'gpt4',
intensity: 1.0,
analysisMode: true,
csvData: {
personality: { nom: 'Marc', style: 'technique' },
mc0: 'plaque personnalisée'
}
});
contenuEnhanced = resultTechnique.content;
console.log(` ✅ Couche technique: ${resultTechnique.stats.enhanced} éléments améliorés`);
// Amélioration style
const resultStyle = await applySelectiveLayer(contenuEnhanced, {
layerType: 'style',
llmProvider: 'mistral',
intensity: 0.8,
analysisMode: true,
csvData: {
personality: {
nom: 'Sophie',
style: 'professionnel moderne',
vocabulairePref: 'innovation,expertise,personnalisation,qualité',
niveauTechnique: 'accessible'
}
}
});
contenuEnhanced = resultStyle.content;
console.log(` ✅ Couche style: ${resultStyle.stats.enhanced} éléments stylisés`);
console.log('\n📊 RÉSULTAT FINAL INTÉGRÉ:');
Object.entries(contenuEnhanced).forEach(([tag, content]) => {
console.log(`\n ${tag}:`);
console.log(` ORIGINAL: "${contenuExistant[tag]}"`);
console.log(` ENHANCED: "${content}"`);
});
return {
success: true,
techniqueResult: resultTechnique,
styleResult: resultStyle,
finalContent: contenuEnhanced
};
} catch (error) {
console.error('❌ ERREUR INTÉGRATION:', error.message);
return { success: false, error: error.message };
}
}
/**
* TEST PERFORMANCE ET BENCHMARKS
*/
async function benchmarkPerformance() {
console.log('\n⚡ === BENCHMARK PERFORMANCE ===\n');
// Contenu de test de taille variable
const contenuTest = {};
// Générer contenu test
for (let i = 1; i <= 10; i++) {
contenuTest[`|Element_${i}|`] = `Ceci est un contenu de test numéro ${i} pour valider les performances du système selective enhancement modulaire. ` +
`Il est important de noter que ce contenu contient du vocabulaire générique et des répétitions. Par ailleurs, les transitions sont basiques. ` +
`En effet, la qualité technique est faible et le style est générique. Par ailleurs, cela nécessite des améliorations.`.repeat(Math.floor(i/3) + 1);
}
console.log(`📊 Contenu test: ${Object.keys(contenuTest).length} éléments`);
try {
const benchmarks = [];
// Test 1: Couche technique seule
const start1 = Date.now();
const result1 = await applySelectiveLayer(contenuTest, {
layerType: 'technical',
intensity: 0.8
});
benchmarks.push({
test: 'Couche technique seule',
duration: Date.now() - start1,
enhanced: result1.stats.enhanced,
processed: result1.stats.processed
});
// Test 2: Stack complet
const start2 = Date.now();
const result2 = await applyPredefinedStack(contenuTest, 'fullEnhancement');
benchmarks.push({
test: 'Stack complet (3 couches)',
duration: Date.now() - start2,
totalModifications: result2.stats.totalModifications,
layers: result2.stats.layers.length
});
// Test 3: Adaptatif
const start3 = Date.now();
const result3 = await applyAdaptiveLayers(contenuTest, { maxIntensity: 1.0 });
benchmarks.push({
test: 'Couches adaptatives',
duration: Date.now() - start3,
layersApplied: result3.stats.layersApplied,
totalModifications: result3.stats.totalModifications
});
console.log('\n📈 RÉSULTATS BENCHMARK:');
benchmarks.forEach(bench => {
console.log(`\n ${bench.test}:`);
console.log(` ⏱️ Durée: ${bench.duration}ms`);
if (bench.enhanced) console.log(` ✅ Améliorés: ${bench.enhanced}/${bench.processed}`);
if (bench.totalModifications) console.log(` 🔄 Modifications: ${bench.totalModifications}`);
if (bench.layers) console.log(` 📦 Couches: ${bench.layers}`);
if (bench.layersApplied) console.log(` 🧠 Couches adaptées: ${bench.layersApplied}`);
});
return { success: true, benchmarks };
} catch (error) {
console.error('❌ ERREUR BENCHMARK:', error.message);
return { success: false, error: error.message };
}
}
// Exécuter démonstrations si fichier appelé directement
if (require.main === module) {
(async () => {
await demoModularSelective();
await demoIntegrationExistante();
await benchmarkPerformance();
})().catch(console.error);
}
module.exports = {
demoModularSelective,
demoIntegrationExistante,
benchmarkPerformance
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/SelectiveEnhancement.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: SelectiveEnhancement.js - Node.js Version
// Description: Enhancement par batch pour éviter timeouts
// ========================================
const { callLLM } = require('./LLMManager');
const { logSh } = require('./ErrorReporting');
const { tracer } = require('./trace.js');
const { selectMultiplePersonalitiesWithAI, getPersonalities } = require('./BrainConfig');
// Utilitaire pour les délais
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* NOUVELLE APPROCHE - Multi-Personnalités Batch Enhancement
* 4 personnalités différentes utilisées dans le pipeline pour maximum d'anti-détection
*/
async function generateWithBatchEnhancement(hierarchy, csvData) {
const totalElements = Object.keys(hierarchy).length;
// NOUVEAU: Sélection de 4 personnalités complémentaires
const personalities = await tracer.run('SelectiveEnhancement.selectMultiplePersonalities()', async () => {
const allPersonalities = await getPersonalities();
const selectedPersonalities = await selectMultiplePersonalitiesWithAI(csvData.mc0, csvData.t0, allPersonalities);
await tracer.event(`4 personnalités sélectionnées: ${selectedPersonalities.map(p => p.nom).join(', ')}`);
return selectedPersonalities;
}, { mc0: csvData.mc0, t0: csvData.t0 });
await tracer.annotate({
totalElements,
personalities: personalities.map(p => `${p.nom}(${p.style})`).join(', '),
mc0: csvData.mc0
});
// ÉTAPE 1 : Génération base avec IA configurée + Personnalité 1
const baseContents = await tracer.run('SelectiveEnhancement.generateAllContentBase()', async () => {
const csvDataWithPersonality1 = { ...csvData, personality: personalities[0] };
const aiProvider1 = personalities[0].aiEtape1Base;
const result = await generateAllContentBase(hierarchy, csvDataWithPersonality1, aiProvider1);
await tracer.event(`${Object.keys(result).length} éléments générés avec ${personalities[0].nom} via ${aiProvider1.toUpperCase()}`);
return result;
}, { hierarchyElements: Object.keys(hierarchy).length, personality1: personalities[0].nom, llmProvider: personalities[0].aiEtape1Base, mc0: csvData.mc0 });
// ÉTAPE 2 : Enhancement technique avec IA configurée + Personnalité 2
const technicalEnhanced = await tracer.run('SelectiveEnhancement.enhanceAllTechnicalTerms()', async () => {
const csvDataWithPersonality2 = { ...csvData, personality: personalities[1] };
const aiProvider2 = personalities[1].aiEtape2Technique;
const result = await enhanceAllTechnicalTerms(baseContents, csvDataWithPersonality2, aiProvider2);
const enhancedCount = Object.keys(result).filter(k => result[k] !== baseContents[k]).length;
await tracer.event(`${enhancedCount}/${Object.keys(result).length} éléments techniques améliorés avec ${personalities[1].nom} via ${aiProvider2.toUpperCase()}`);
return result;
}, { baseElements: Object.keys(baseContents).length, personality2: personalities[1].nom, llmProvider: personalities[1].aiEtape2Technique, mc0: csvData.mc0 });
// ÉTAPE 3 : Enhancement transitions avec IA configurée + Personnalité 3
const transitionsEnhanced = await tracer.run('SelectiveEnhancement.enhanceAllTransitions()', async () => {
const csvDataWithPersonality3 = { ...csvData, personality: personalities[2] };
const aiProvider3 = personalities[2].aiEtape3Transitions;
const result = await enhanceAllTransitions(technicalEnhanced, csvDataWithPersonality3, aiProvider3);
const enhancedCount = Object.keys(result).filter(k => result[k] !== technicalEnhanced[k]).length;
await tracer.event(`${enhancedCount}/${Object.keys(result).length} transitions fluidifiées avec ${personalities[2].nom} via ${aiProvider3.toUpperCase()}`);
return result;
}, { technicalElements: Object.keys(technicalEnhanced).length, personality3: personalities[2].nom, llmProvider: personalities[2].aiEtape3Transitions });
// ÉTAPE 4 : Enhancement style avec IA configurée + Personnalité 4
const finalContents = await tracer.run('SelectiveEnhancement.enhanceAllPersonalityStyle()', async () => {
const csvDataWithPersonality4 = { ...csvData, personality: personalities[3] };
const aiProvider4 = personalities[3].aiEtape4Style;
const result = await enhanceAllPersonalityStyle(transitionsEnhanced, csvDataWithPersonality4, aiProvider4);
const enhancedCount = Object.keys(result).filter(k => result[k] !== transitionsEnhanced[k]).length;
const avgWords = Math.round(Object.values(result).reduce((acc, content) => acc + content.split(' ').length, 0) / Object.keys(result).length);
await tracer.event(`${enhancedCount}/${Object.keys(result).length} éléments stylisés avec ${personalities[3].nom} via ${aiProvider4.toUpperCase()}`, { avgWordsPerElement: avgWords });
return result;
}, { transitionElements: Object.keys(transitionsEnhanced).length, personality4: personalities[3].nom, llmProvider: personalities[3].aiEtape4Style });
// Log final du DNA Mixing réussi avec IA configurables
const aiChain = personalities.map((p, i) => `${p.aiEtape1Base || p.aiEtape2Technique || p.aiEtape3Transitions || p.aiEtape4Style}`.toUpperCase()).join(' → ');
logSh(`✅ DNA MIXING MULTI-PERSONNALITÉS TERMINÉ:`, 'INFO');
logSh(` 🎭 4 personnalités utilisées: ${personalities.map(p => p.nom).join(' → ')}`, 'INFO');
logSh(` 🤖 IA configurées: ${personalities[0].aiEtape1Base.toUpperCase()}${personalities[1].aiEtape2Technique.toUpperCase()}${personalities[2].aiEtape3Transitions.toUpperCase()}${personalities[3].aiEtape4Style.toUpperCase()}`, 'INFO');
logSh(` 📝 ${Object.keys(finalContents).length} éléments avec style hybride généré`, 'INFO');
return finalContents;
}
/**
* ÉTAPE 1 - Génération base TOUS éléments avec IA configurable
*/
async function generateAllContentBase(hierarchy, csvData, aiProvider) {
logSh('🔍 === DEBUG GÉNÉRATION BASE ===', 'DEBUG');
// Debug: logger la hiérarchie complète
logSh(`🔍 Hiérarchie reçue: ${Object.keys(hierarchy).length} sections`, 'DEBUG');
Object.keys(hierarchy).forEach((path, i) => {
const section = hierarchy[path];
logSh(`🔍 Section ${i+1} [${path}]:`, 'DEBUG');
logSh(`🔍 - title: ${section.title ? section.title.originalElement?.originalTag : 'AUCUN'}`, 'DEBUG');
logSh(`🔍 - text: ${section.text ? section.text.originalElement?.originalTag : 'AUCUN'}`, 'DEBUG');
logSh(`🔍 - questions: ${section.questions?.length || 0}`, 'DEBUG');
});
const allElements = collectAllElements(hierarchy);
logSh(`🔍 Éléments collectés: ${allElements.length}`, 'DEBUG');
// Debug: logger tous les éléments collectés
allElements.forEach((element, i) => {
logSh(`🔍 Élément ${i+1}: tag="${element.tag}", type="${element.type}"`, 'DEBUG');
});
// NOUVELLE LOGIQUE : SÉPARER PAIRES FAQ ET AUTRES ÉLÉMENTS
const results = {};
logSh(`🔍 === GÉNÉRATION INTELLIGENTE DE ${allElements.length} ÉLÉMENTS ===`, 'DEBUG');
logSh(`🔍 Ordre respecté: ${allElements.map(el => el.tag.replace(/\|/g, '')).join(' → ')}`, 'DEBUG');
// 1. IDENTIFIER les paires FAQ
const { faqPairs, otherElements } = separateFAQPairsAndOthers(allElements);
logSh(`🔍 ${faqPairs.length} paires FAQ trouvées, ${otherElements.length} autres éléments`, 'INFO');
// 2. GÉNÉRER les autres éléments EN BATCH ORDONNÉ (titres d'abord, puis textes avec contexte)
const groupedElements = groupElementsByType(otherElements);
// ORDRE DE GÉNÉRATION : TITRES → TEXTES → INTRO → AUTRES
const orderedTypes = ['titre', 'texte', 'intro'];
for (const type of orderedTypes) {
const elements = groupedElements[type];
if (!elements || elements.length === 0) continue;
// DÉCOUPER EN CHUNKS DE MAX 4 ÉLÉMENTS POUR ÉVITER TIMEOUTS
const chunks = chunkArray(elements, 4);
logSh(`🚀 BATCH ${type.toUpperCase()}: ${elements.length} éléments en ${chunks.length} chunks`, 'INFO');
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
logSh(` Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
try {
// Passer les résultats déjà générés pour contexte (titres → textes)
const batchPrompt = createBatchBasePrompt(chunk, type, csvData, results);
const batchResponse = await callLLM(aiProvider, batchPrompt, {
temperature: 0.7,
maxTokens: 2000 * chunk.length
}, csvData.personality);
const batchResults = parseBatchResponse(batchResponse, chunk);
Object.assign(results, batchResults);
logSh(`✅ Chunk ${chunkIndex + 1}: ${Object.keys(batchResults).length}/${chunk.length} éléments générés`, 'INFO');
} catch (error) {
logSh(`❌ FATAL: Chunk ${chunkIndex + 1} de ${type} échoué: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Génération chunk ${chunkIndex + 1} de ${type} échouée - arrêt du workflow: ${error.message}`);
}
// Délai entre chunks pour éviter rate limiting
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
}
logSh(`✅ BATCH ${type.toUpperCase()} COMPLET: ${elements.length} éléments générés en ${chunks.length} chunks`, 'INFO');
}
// TRAITER les types restants (autres que titre/texte/intro)
for (const [type, elements] of Object.entries(groupedElements)) {
if (orderedTypes.includes(type) || elements.length === 0) continue;
// DÉCOUPER EN CHUNKS DE MAX 4 ÉLÉMENTS POUR ÉVITER TIMEOUTS
const chunks = chunkArray(elements, 4);
logSh(`🚀 BATCH ${type.toUpperCase()}: ${elements.length} éléments en ${chunks.length} chunks`, 'INFO');
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
logSh(` Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
try {
const batchPrompt = createBatchBasePrompt(chunk, type, csvData, results);
const batchResponse = await callLLM(aiProvider, batchPrompt, {
temperature: 0.7,
maxTokens: 2000 * chunk.length
}, csvData.personality);
const batchResults = parseBatchResponse(batchResponse, chunk);
Object.assign(results, batchResults);
logSh(`✅ Chunk ${chunkIndex + 1}: ${Object.keys(batchResults).length}/${chunk.length} éléments générés`, 'INFO');
} catch (error) {
logSh(`❌ FATAL: Chunk ${chunkIndex + 1} de ${type} échoué: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Génération chunk ${chunkIndex + 1} de ${type} échouée - arrêt du workflow: ${error.message}`);
}
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
}
logSh(`✅ BATCH ${type.toUpperCase()} COMPLET: ${elements.length} éléments générés en ${chunks.length} chunks`, 'INFO');
}
// 3. GÉNÉRER les paires FAQ ensemble (RESTAURÉ depuis .gs)
if (faqPairs.length > 0) {
logSh(`🔍 === GÉNÉRATION PAIRES FAQ (${faqPairs.length} paires) ===`, 'INFO');
const faqResults = await generateFAQPairsRestored(faqPairs, csvData, aiProvider);
Object.assign(results, faqResults);
}
logSh(`🔍 === RÉSULTATS FINAUX GÉNÉRATION BASE ===`, 'DEBUG');
logSh(`🔍 Total généré: ${Object.keys(results).length} éléments`, 'DEBUG');
Object.keys(results).forEach(tag => {
logSh(`🔍 [${tag}]: "${results[tag]}"`, 'DEBUG');
});
return results;
}
/**
* ÉTAPE 2 - Enhancement technique ÉLÉMENT PAR ÉLÉMENT avec IA configurable
* NOUVEAU : Traitement individuel pour fiabilité maximale et debug précis
*/
async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) {
logSh('🔧 === DÉBUT ENHANCEMENT TECHNIQUE ===', 'INFO');
logSh('Enhancement technique BATCH TOTAL...', 'DEBUG');
const allElements = Object.keys(baseContents);
if (allElements.length === 0) {
logSh('⚠️ Aucun élément à analyser techniquement', 'WARNING');
return baseContents;
}
const analysisStart = Date.now();
logSh(`📊 Analyse démarrée: ${allElements.length} éléments à examiner`, 'INFO');
try {
// ÉTAPE 1 : Extraction batch TOUS les termes techniques (1 seul appel)
logSh(`🔍 Analyse technique batch: ${allElements.length} éléments`, 'INFO');
const technicalAnalysis = await extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider);
const analysisEnd = Date.now();
// ÉTAPE 2 : Enhancement batch TOUS les éléments qui en ont besoin (1 seul appel)
const elementsNeedingEnhancement = technicalAnalysis.filter(item => item.needsEnhancement);
logSh(`📋 Analyse terminée (${analysisEnd - analysisStart}ms):`, 'INFO');
logSh(`${elementsNeedingEnhancement.length}/${allElements.length} éléments nécessitent enhancement`, 'INFO');
if (elementsNeedingEnhancement.length === 0) {
logSh('✅ Aucun élément ne nécessite enhancement technique - contenu déjà optimal', 'INFO');
return baseContents;
}
// Log détaillé des éléments à améliorer
elementsNeedingEnhancement.forEach((item, i) => {
logSh(` ${i+1}. [${item.tag}]: ${item.technicalTerms.join(', ')}`, 'DEBUG');
});
const enhancementStart = Date.now();
logSh(`🔧 Enhancement technique: ${elementsNeedingEnhancement.length}/${allElements.length} éléments`, 'INFO');
const enhancedContents = await enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider);
const enhancementEnd = Date.now();
// ÉTAPE 3 : Merger résultats
const results = { ...baseContents };
let actuallyEnhanced = 0;
Object.keys(enhancedContents).forEach(tag => {
if (enhancedContents[tag] !== baseContents[tag]) {
results[tag] = enhancedContents[tag];
actuallyEnhanced++;
}
});
logSh(`⚡ Enhancement terminé (${enhancementEnd - enhancementStart}ms):`, 'INFO');
logSh(`${actuallyEnhanced} éléments réellement améliorés`, 'INFO');
logSh(` • Termes intégrés: dibond, impression UV, fraisage, etc.`, 'DEBUG');
logSh(`✅ Enhancement technique terminé avec succès`, 'INFO');
return results;
} catch (error) {
const analysisTotal = Date.now() - analysisStart;
logSh(`❌ FATAL: Enhancement technique échoué après ${analysisTotal}ms`, 'ERROR');
logSh(`❌ Message: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Enhancement technique impossible - arrêt du workflow: ${error.message}`);
}
}
/**
* Analyser un seul élément pour détecter les termes techniques
*/
async function analyzeSingleElementTechnicalTerms(tag, content, csvData, aiProvider) {
const prompt = `MISSION: Analyser ce contenu et déterminer s'il contient des termes techniques.
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTENU À ANALYSER:
TAG: ${tag}
CONTENU: "${content}"
CONSIGNES:
- Cherche UNIQUEMENT des vrais termes techniques métier/industrie
- Évite mots génériques (qualité, service, pratique, personnalisé, etc.)
- Focus: matériaux, procédés, normes, dimensions, technologies spécifiques
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé, anodisation
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique, haute performance
RÉPONSE REQUISE:
- Si termes techniques trouvés: "OUI - termes: [liste des termes séparés par virgules]"
- Si aucun terme technique: "NON"
EXEMPLE:
OUI - termes: aluminium composite, impression numérique, gravure laser`;
try {
const response = await callLLM(aiProvider, prompt, { temperature: 0.3 });
if (response.toUpperCase().startsWith('OUI')) {
// Extraire les termes de la réponse
const termsMatch = response.match(/termes:\s*(.+)/i);
const terms = termsMatch ? termsMatch[1].trim() : '';
logSh(`✅ [${tag}] Termes techniques détectés: ${terms}`, 'DEBUG');
return true;
} else {
logSh(`⏭️ [${tag}] Pas de termes techniques`, 'DEBUG');
return false;
}
} catch (error) {
logSh(`❌ ERREUR analyse ${tag}: ${error.message}`, 'ERROR');
return false; // En cas d'erreur, on skip l'enhancement
}
}
/**
* Enhancer un seul élément techniquement
*/
async function enhanceSingleElementTechnical(tag, content, csvData, aiProvider) {
const prompt = `MISSION: Améliore ce contenu en intégrant des termes techniques précis.
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTENU À AMÉLIORER:
TAG: ${tag}
CONTENU: "${content}"
OBJECTIFS:
- Remplace les termes génériques par des termes techniques précis
- Ajoute des spécifications techniques réalistes
- Maintient le même style et longueur
- Intègre naturellement: matériaux (dibond, aluminium composite), procédés (impression UV, gravure laser), dimensions, normes
EXEMPLE DE TRANSFORMATION:
"matériaux haute performance" → "dibond 3mm ou aluminium composite"
"impression moderne" → "impression UV haute définition"
"fixation solide" → "fixation par chevilles inox Ø6mm"
CONTRAINTES:
- GARDE la même structure
- MÊME longueur approximative
- Style cohérent avec l'original
- RÉPONDS DIRECTEMENT par le contenu amélioré, sans préfixe`;
try {
const enhancedContent = await callLLM(aiProvider, prompt, { temperature: 0.7 });
return enhancedContent.trim();
} catch (error) {
logSh(`❌ ERREUR enhancement ${tag}: ${error.message}`, 'ERROR');
return content; // En cas d'erreur, on retourne le contenu original
}
}
// ANCIENNES FONCTIONS BATCH SUPPRIMÉES - REMPLACÉES PAR TRAITEMENT INDIVIDUEL
/**
* NOUVELLE FONCTION : Enhancement batch TOUS les éléments
*/
// FONCTION SUPPRIMÉE : enhanceAllElementsTechnicalBatch() - Remplacée par traitement individuel
/**
* ÉTAPE 3 - Enhancement transitions BATCH avec IA configurable
*/
async function enhanceAllTransitions(baseContents, csvData, aiProvider) {
logSh('🔗 === DÉBUT ENHANCEMENT TRANSITIONS ===', 'INFO');
logSh('Enhancement transitions batch...', 'DEBUG');
const transitionStart = Date.now();
const allElements = Object.keys(baseContents);
logSh(`📊 Analyse transitions: ${allElements.length} éléments à examiner`, 'INFO');
// Sélectionner éléments longs qui bénéficient d'amélioration transitions
const transitionElements = [];
let analyzedCount = 0;
Object.keys(baseContents).forEach(tag => {
const content = baseContents[tag];
analyzedCount++;
if (content.length > 150) {
const needsTransitions = analyzeTransitionNeed(content);
logSh(` [${tag}]: ${content.length}c, transitions=${needsTransitions ? '✅' : '❌'}`, 'DEBUG');
if (needsTransitions) {
transitionElements.push({
tag: tag,
content: content
});
}
} else {
logSh(` [${tag}]: ${content.length}c - trop court, ignoré`, 'DEBUG');
}
});
logSh(`📋 Analyse transitions terminée:`, 'INFO');
logSh(`${analyzedCount} éléments analysés`, 'INFO');
logSh(`${transitionElements.length} nécessitent amélioration`, 'INFO');
if (transitionElements.length === 0) {
logSh('✅ Pas d\'éléments nécessitant enhancement transitions - fluidité déjà optimale', 'INFO');
return baseContents;
}
logSh(`${transitionElements.length} éléments à améliorer (transitions)`, 'INFO');
const chunks = chunkArray(transitionElements, 6); // Plus petit pour Gemini
const results = { ...baseContents };
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(`Chunk transitions ${chunkIndex + 1}/${chunks.length} (${chunk.length} éléments)`, 'DEBUG');
const batchTransitionsPrompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.
CONTEXTE: Article SEO professionnel pour site web commercial
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} adapté web)
CONNECTEURS PRÉFÉRÉS: ${csvData.personality?.connecteursPref}
CONTENUS:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
"${item.content}"`).join('\n\n')}
OBJECTIFS:
- Connecteurs plus naturels et variés issus de: ${csvData.personality?.connecteursPref}
- Transitions fluides entre idées
- ÉVITE répétitions excessives ("franchement", "du coup", "vraiment", "par ailleurs")
- Style cohérent ${csvData.personality?.style}
CONTRAINTES STRICTES:
- NE CHANGE PAS le fond du message
- GARDE la même structure et longueur approximative
- Améliore SEULEMENT la fluidité des transitions
- RESPECTE le style ${csvData.personality?.nom}
- RÉPONDS DIRECTEMENT PAR LE CONTENU AMÉLIORÉ, sans préfixe ni tag XML
FORMAT DE RÉPONSE:
[1] Contenu avec transitions améliorées selon ${csvData.personality?.nom}
[2] Contenu avec transitions améliorées selon ${csvData.personality?.nom}
etc...`;
const improved = await callLLM(aiProvider, batchTransitionsPrompt, {
temperature: 0.6,
maxTokens: 2500
}, csvData.personality);
const parsedImprovements = parseTransitionsBatchResponse(improved, chunk);
Object.keys(parsedImprovements).forEach(tag => {
results[tag] = parsedImprovements[tag];
});
} catch (error) {
logSh(`❌ Erreur chunk transitions ${chunkIndex + 1}: ${error.message}`, 'ERROR');
}
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
}
return results;
}
/**
* ÉTAPE 4 - Enhancement style personnalité BATCH avec IA configurable
*/
async function enhanceAllPersonalityStyle(baseContents, csvData, aiProvider) {
const personality = csvData.personality;
if (!personality) {
logSh('Pas de personnalité, skip enhancement style', 'DEBUG');
return baseContents;
}
logSh(`Enhancement style ${personality.nom} batch...`, 'DEBUG');
// Tous les éléments bénéficient de l'adaptation personnalité
const styleElements = Object.keys(baseContents).map(tag => ({
tag: tag,
content: baseContents[tag]
}));
const chunks = chunkArray(styleElements, 8);
const results = { ...baseContents };
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(`Chunk style ${chunkIndex + 1}/${chunks.length} (${chunk.length} éléments)`, 'DEBUG');
const batchStylePrompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}.
CONTEXTE: Finalisation article SEO pour site e-commerce professionnel
PERSONNALITÉ: ${personality.nom}
DESCRIPTION: ${personality.description}
STYLE CIBLE: ${personality.style} adapté au web professionnel
VOCABULAIRE: ${personality.vocabulairePref}
CONNECTEURS: ${personality.connecteursPref}
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
LONGUEUR PHRASES: ${personality.longueurPhrases}
CONTENUS À STYLISER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
"${item.content}"`).join('\n\n')}
CONSIGNES STRICTES:
- GARDE le même contenu informatif et technique
- Adapte SEULEMENT le ton, les expressions et le vocabulaire selon ${personality.nom}
- RESPECTE la longueur approximative (même nombre de mots ±20%)
- ÉVITE les répétitions excessives ("franchement", "du coup", "vraiment")
- VARIE les expressions et connecteurs selon: ${personality.connecteursPref}
- Style ${personality.nom} reconnaissable mais NATUREL
- RÉPONDS DIRECTEMENT PAR LE CONTENU STYLISÉ, sans préfixe ni tag XML
- PAS de messages d'excuse ou d'incapacité
FORMAT DE RÉPONSE:
[1] Contenu stylisé selon ${personality.nom} (${personality.style})
[2] Contenu stylisé selon ${personality.nom} (${personality.style})
etc...`;
const styled = await callLLM(aiProvider, batchStylePrompt, {
temperature: 0.8,
maxTokens: 3000
}, personality);
const parsedStyles = parseStyleBatchResponse(styled, chunk);
Object.keys(parsedStyles).forEach(tag => {
results[tag] = parsedStyles[tag];
});
} catch (error) {
logSh(`❌ Erreur chunk style ${chunkIndex + 1}: ${error.message}`, 'ERROR');
}
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
}
return results;
}
// ============= HELPER FUNCTIONS =============
/**
* Sleep function replacement for Utilities.sleep
*/
// FONCTION SUPPRIMÉE : sleep() dupliquée - déjà définie ligne 12
/**
* RESTAURÉ DEPUIS .GS : Génération des paires FAQ cohérentes
*/
async function generateFAQPairsRestored(faqPairs, csvData, aiProvider) {
logSh(`🔍 === GÉNÉRATION PAIRES FAQ (logique .gs restaurée) ===`, 'INFO');
if (faqPairs.length === 0) return {};
const batchPrompt = createBatchFAQPairsPrompt(faqPairs, csvData);
logSh(`🔍 Prompt FAQ paires (${batchPrompt.length} chars): "${batchPrompt.substring(0, 300)}..."`, 'DEBUG');
try {
const batchResponse = await callLLM(aiProvider, batchPrompt, {
temperature: 0.8,
maxTokens: 3000 // Plus large pour les paires
}, csvData.personality);
logSh(`🔍 Réponse FAQ paires reçue: ${batchResponse.length} caractères`, 'DEBUG');
logSh(`🔍 Début réponse: "${batchResponse.substring(0, 200)}..."`, 'DEBUG');
return parseFAQPairsResponse(batchResponse, faqPairs);
} catch (error) {
logSh(`❌ FATAL: Erreur génération paires FAQ: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Génération paires FAQ échouée - arrêt du workflow: ${error.message}`);
}
}
/**
* RESTAURÉ DEPUIS .GS : Prompt pour paires FAQ cohérentes
*/
function createBatchFAQPairsPrompt(faqPairs, csvData) {
const personality = csvData.personality;
let prompt = `=== 1. CONTEXTE ===
Entreprise: Autocollant.fr - signalétique personnalisée
Sujet: ${csvData.mc0}
Section: FAQ pour article SEO commercial
=== 2. PERSONNALITÉ ===
Rédacteur: ${personality.nom}
Style: ${personality.style}
Ton: ${personality.description || 'professionnel'}
=== 3. RÈGLES GÉNÉRALES ===
- Questions naturelles de clients
- Réponses expertes et rassurantes
- Langage professionnel mais accessible
- Textes rédigés humainement et de façon authentique
- Couvrir: prix, livraison, personnalisation, installation, durabilité
- IMPÉRATIF: Respecter strictement les contraintes XML
=== 4. PAIRES FAQ À GÉNÉRER ===
`;
faqPairs.forEach((pair, index) => {
const questionTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
const answerTag = pair.answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
prompt += `${index + 1}. [${questionTag}] + [${answerTag}] - Paire FAQ naturelle
`;
});
prompt += `
FORMAT DE RÉPONSE:
PAIRE 1:
[${faqPairs[0].question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}]
Question client directe et naturelle sur ${csvData.mc0} ?
[${faqPairs[0].answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}]
Réponse utile et rassurante selon le style ${personality.style} de ${personality.nom}.
`;
if (faqPairs.length > 1) {
prompt += `PAIRE 2:
etc...
`;
}
return prompt;
}
/**
* RESTAURÉ DEPUIS .GS : Parser réponse paires FAQ
*/
function parseFAQPairsResponse(response, faqPairs) {
const results = {};
logSh(`🔍 Parsing FAQ paires: "${response.substring(0, 300)}..."`, 'DEBUG');
// Parser avec regex [TAG] contenu
const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
let content = match[2].trim().replace(/\n\s*\n/g, '\n').replace(/^\n+|\n+$/g, '');
// NOUVEAU: Appliquer le nettoyage XML pour FAQ aussi
content = cleanXMLTagsFromContent(content);
if (content && content.length > 0) {
parsedItems[tag] = content;
logSh(`🔍 Parsé [${tag}]: "${content.substring(0, 100)}..."`, 'DEBUG');
}
}
// Mapper aux vrais tags FAQ avec |
let pairesCompletes = 0;
faqPairs.forEach(pair => {
const questionCleanTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
const answerCleanTag = pair.answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
const questionContent = parsedItems[questionCleanTag];
const answerContent = parsedItems[answerCleanTag];
if (questionContent && answerContent) {
results[pair.question.tag] = questionContent;
results[pair.answer.tag] = answerContent;
pairesCompletes++;
logSh(`✅ Paire FAQ ${pair.number} complète: Q="${questionContent}" R="${answerContent.substring(0, 50)}..."`, 'INFO');
} else {
logSh(`⚠️ Paire FAQ ${pair.number} incomplète: Q=${!!questionContent} R=${!!answerContent}`, 'WARNING');
if (questionContent) results[pair.question.tag] = questionContent;
if (answerContent) results[pair.answer.tag] = answerContent;
}
});
logSh(`📊 FAQ parsing: ${pairesCompletes}/${faqPairs.length} paires complètes`, 'INFO');
// FATAL si aucune paire complète (comme dans le .gs)
if (pairesCompletes === 0 && faqPairs.length > 0) {
logSh(`❌ FATAL: Aucune paire FAQ générée correctement`, 'ERROR');
throw new Error(`FATAL: Génération FAQ incomplète (0/${faqPairs.length} paires complètes) - arrêt du workflow`);
}
return results;
}
/**
* RESTAURÉ DEPUIS .GS : Nettoyer instructions FAQ
*/
function cleanFAQInstructions(instructions, csvData) {
if (!instructions) return '';
let cleanInstructions = instructions;
// Remplacer variables
cleanInstructions = cleanInstructions.replace(/\{\{T0\}\}/g, csvData.t0 || '');
cleanInstructions = cleanInstructions.replace(/\{\{MC0\}\}/g, csvData.mc0 || '');
cleanInstructions = cleanInstructions.replace(/\{\{T-1\}\}/g, csvData.tMinus1 || '');
cleanInstructions = cleanInstructions.replace(/\{\{L-1\}\}/g, csvData.lMinus1 || '');
// Variables multiples MC+1_X, T+1_X, L+1_X
if (csvData.mcPlus1) {
const mcPlus1 = csvData.mcPlus1.split(',').map(s => s.trim());
for (let i = 1; i <= 6; i++) {
const mcValue = mcPlus1[i-1] || `[MC+1_${i} non défini]`;
cleanInstructions = cleanInstructions.replace(new RegExp(`\\{\\{MC\\+1_${i}\\}\\}`, 'g'), mcValue);
}
}
if (csvData.tPlus1) {
const tPlus1 = csvData.tPlus1.split(',').map(s => s.trim());
for (let i = 1; i <= 6; i++) {
const tValue = tPlus1[i-1] || `[T+1_${i} non défini]`;
cleanInstructions = cleanInstructions.replace(new RegExp(`\\{\\{T\\+1_${i}\\}\\}`, 'g'), tValue);
}
}
// Nettoyer HTML
cleanInstructions = cleanInstructions.replace(/<\/?[^>]+>/g, '');
cleanInstructions = cleanInstructions.replace(/\s+/g, ' ').trim();
return cleanInstructions;
}
/**
* Collecter tous les éléments dans l'ordre XML original
* CORRECTION: Suit l'ordre séquentiel XML au lieu de grouper par section
*/
function collectAllElements(hierarchy) {
const allElements = [];
const tagToElementMap = {};
// 1. Créer un mapping de tous les éléments disponibles
Object.keys(hierarchy).forEach(path => {
const section = hierarchy[path];
if (section.title) {
tagToElementMap[section.title.originalElement.originalTag] = {
tag: section.title.originalElement.originalTag,
element: section.title.originalElement,
type: 'titre'
};
}
if (section.text) {
tagToElementMap[section.text.originalElement.originalTag] = {
tag: section.text.originalElement.originalTag,
element: section.text.originalElement,
type: 'texte'
};
}
section.questions.forEach(q => {
tagToElementMap[q.originalElement.originalTag] = {
tag: q.originalElement.originalTag,
element: q.originalElement,
type: q.originalElement.type
};
});
});
// 2. Récupérer l'ordre XML original depuis le template global
logSh(`🔍 Global XML Template disponible: ${!!global.currentXmlTemplate}`, 'DEBUG');
if (global.currentXmlTemplate && global.currentXmlTemplate.length > 0) {
logSh(`🔍 Template XML: ${global.currentXmlTemplate.substring(0, 200)}...`, 'DEBUG');
const regex = /\|([^|]+)\|/g;
let match;
// Parcourir le XML dans l'ordre d'apparition
while ((match = regex.exec(global.currentXmlTemplate)) !== null) {
const fullMatch = match[1];
// Extraire le nom du tag (sans variables)
const nameMatch = fullMatch.match(/^([^{]+)/);
const tagName = nameMatch ? nameMatch[1].trim() : fullMatch.split('{')[0];
const pureTag = `|${tagName}|`;
// Si cet élément existe dans notre mapping, l'ajouter dans l'ordre
if (tagToElementMap[pureTag]) {
allElements.push(tagToElementMap[pureTag]);
logSh(`🔍 Ajouté dans l'ordre: ${pureTag}`, 'DEBUG');
delete tagToElementMap[pureTag]; // Éviter les doublons
} else {
logSh(`🔍 Tag XML non trouvé dans mapping: ${pureTag}`, 'DEBUG');
}
}
}
// 3. Ajouter les éléments restants (sécurité)
const remainingElements = Object.values(tagToElementMap);
if (remainingElements.length > 0) {
logSh(`🔍 Éléments restants ajoutés: ${remainingElements.map(el => el.tag).join(', ')}`, 'DEBUG');
remainingElements.forEach(element => {
allElements.push(element);
});
}
logSh(`🔍 ORDRE FINAL: ${allElements.map(el => el.tag.replace(/\|/g, '')).join(' → ')}`, 'INFO');
return allElements;
}
/**
* RESTAURÉ DEPUIS .GS : Séparer les paires FAQ des autres éléments
*/
function separateFAQPairsAndOthers(allElements) {
const faqPairs = [];
const otherElements = [];
const faqQuestions = {};
const faqAnswers = {};
// 1. Collecter toutes les questions et réponses FAQ
allElements.forEach(element => {
if (element.type === 'faq_question') {
// Extraire le numéro : |Faq_q_1| → 1
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqQuestions[faqNumber] = element;
logSh(`🔍 Question FAQ ${faqNumber} trouvée: ${element.tag}`, 'DEBUG');
} else if (element.type === 'faq_reponse') {
// Extraire le numéro : |Faq_a_1| → 1
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqAnswers[faqNumber] = element;
logSh(`🔍 Réponse FAQ ${faqNumber} trouvée: ${element.tag}`, 'DEBUG');
} else {
// Élément normal (titre, texte, intro, etc.)
otherElements.push(element);
}
});
// 2. Créer les paires FAQ cohérentes
Object.keys(faqQuestions).forEach(number => {
const question = faqQuestions[number];
const answer = faqAnswers[number];
if (question && answer) {
faqPairs.push({
number: number,
question: question,
answer: answer
});
logSh(`✅ Paire FAQ ${number} créée: ${question.tag} + ${answer.tag}`, 'INFO');
} else if (question) {
logSh(`⚠️ Question FAQ ${number} sans réponse correspondante`, 'WARNING');
otherElements.push(question); // Traiter comme élément individuel
} else if (answer) {
logSh(`⚠️ Réponse FAQ ${number} sans question correspondante`, 'WARNING');
otherElements.push(answer); // Traiter comme élément individuel
}
});
logSh(`🔍 Séparation terminée: ${faqPairs.length} paires FAQ, ${otherElements.length} autres éléments`, 'INFO');
return { faqPairs, otherElements };
}
/**
* Grouper éléments par type
*/
function groupElementsByType(elements) {
const groups = {};
elements.forEach(element => {
const type = element.type;
if (!groups[type]) {
groups[type] = [];
}
groups[type].push(element);
});
return groups;
}
/**
* Diviser array en chunks
*/
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
/**
* Trouver le titre associé à un élément texte
*/
function findAssociatedTitle(textElement, existingResults) {
const textName = textElement.element.name || textElement.tag;
// STRATÉGIE 1: Correspondance directe (Txt_H2_1 → Titre_H2_1)
const directMatch = textName.replace(/Txt_/, 'Titre_').replace(/Text_/, 'Titre_');
const directTitle = existingResults[`|${directMatch}|`] || existingResults[directMatch];
if (directTitle) return directTitle;
// STRATÉGIE 2: Même niveau hiérarchique (H2, H3)
const levelMatch = textName.match(/(H\d)_(\d+)/);
if (levelMatch) {
const [, level, number] = levelMatch;
const titleTag = `Titre_${level}_${number}`;
const levelTitle = existingResults[`|${titleTag}|`] || existingResults[titleTag];
if (levelTitle) return levelTitle;
}
// STRATÉGIE 3: Proximité dans l'ordre (texte suivant un titre)
const allTitles = Object.entries(existingResults)
.filter(([tag]) => tag.includes('Titre'))
.sort(([a], [b]) => a.localeCompare(b));
if (allTitles.length > 0) {
// Retourner le premier titre disponible comme contexte général
return allTitles[0][1];
}
return null;
}
/**
* Créer prompt batch de base
*/
function createBatchBasePrompt(elements, type, csvData, existingResults = {}) {
const personality = csvData.personality;
let prompt = `=== 1. CONTEXTE ===
Entreprise: Autocollant.fr - signalétique personnalisée
Sujet: ${csvData.mc0}
Type d'article: SEO professionnel pour site commercial
=== 2. PERSONNALITÉ ===
Rédacteur: ${personality.nom}
Style: ${personality.style}
Ton: ${personality.description || 'professionnel'}
=== 3. RÈGLES GÉNÉRALES ===
- Contenu SEO optimisé
- Langage naturel et fluide
- Éviter répétitions
- Pas de références techniques dans le contenu
- Textes rédigés humainement et de façon authentique
- IMPÉRATIF: Respecter strictement les contraintes XML (nombre de mots, etc.)
=== 4. ÉLÉMENTS À GÉNÉRER ===
`;
// AJOUTER CONTEXTE DES TITRES POUR LES TEXTES
if (type === 'texte' && Object.keys(existingResults).length > 0) {
const generatedTitles = Object.entries(existingResults)
.filter(([tag]) => tag.includes('Titre'))
.map(([tag, title]) => `${tag.replace(/\|/g, '')}: "${title}"`)
.slice(0, 5); // Limiter à 5 titres pour éviter surcharge
if (generatedTitles.length > 0) {
prompt += `
Titres existants pour contexte:
${generatedTitles.join('\n')}
`;
}
}
elements.forEach((elementInfo, index) => {
const cleanTag = elementInfo.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
prompt += `${index + 1}. [${cleanTag}] `;
// INSTRUCTIONS PROPRES PAR ÉLÉMENT
if (type === 'titre') {
if (elementInfo.element.type === 'titre_h1') {
prompt += `Titre principal accrocheur\n`;
} else if (elementInfo.element.type === 'titre_h2') {
prompt += `Titre de section engageant\n`;
} else if (elementInfo.element.type === 'titre_h3') {
prompt += `Sous-titre spécialisé\n`;
} else {
prompt += `Titre pertinent\n`;
}
} else if (type === 'texte') {
prompt += `Paragraphe informatif\n`;
// ASSOCIER LE TITRE CORRESPONDANT AUTOMATIQUEMENT
const associatedTitle = findAssociatedTitle(elementInfo, existingResults);
if (associatedTitle) {
prompt += ` Contexte: "${associatedTitle}"\n`;
}
if (elementInfo.element.resolvedContent) {
prompt += ` Angle: "${elementInfo.element.resolvedContent}"\n`;
}
} else if (type === 'intro') {
prompt += `Introduction engageante\n`;
} else {
prompt += `Contenu pertinent\n`;
}
});
prompt += `\nSTYLE ${personality.nom.toUpperCase()} - ${personality.style}:
- Vocabulaire: ${personality.vocabulairePref}
- Connecteurs: ${personality.connecteursPref}
- Phrases: ${personality.longueurPhrases}
- Niveau technique: ${personality.niveauTechnique}
CONSIGNES STRICTES POUR ARTICLE SEO:
- CONTEXTE: Article professionnel pour site e-commerce, destiné aux clients potentiels
- STYLE: ${personality.style} de ${personality.nom} mais ADAPTÉ au web professionnel
- INTERDICTION ABSOLUE: expressions trop familières répétées ("du coup", "bon", "franchement", "nickel", "tip-top")
- VOCABULAIRE: Mélange expertise technique + accessibilité client
- SEO: Utilise naturellement "${csvData.mc0}" et termes associés
- POUR LES TITRES: Titre SEO attractif UNIQUEMENT, JAMAIS "Titre_H1_1" ou "Titre_H2_7"
- EXEMPLE TITRE: "Plaques personnalisées résistantes : guide complet 2024"
- CONTENU: Informatif, rassurant, incite à l'achat SANS être trop commercial
- RÉPONDS DIRECTEMENT par le contenu web demandé, SANS préfixe
FORMAT DE RÉPONSE ${type === 'titre' ? '(TITRES UNIQUEMENT)' : ''}:
[${elements[0].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}]
${type === 'titre' ? 'Titre réel et attractif (PAS "Titre_H1_1")' : 'Contenu rédigé selon le style ' + personality.nom}
[${elements[1] ? elements[1].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '') : 'element2'}]
${type === 'titre' ? 'Titre réel et attractif (PAS "Titre_H2_1")' : 'Contenu rédigé selon le style ' + personality.nom}
etc...`;
return prompt;
}
/**
* Parser réponse batch générique avec nettoyage des tags XML
*/
function parseBatchResponse(response, elements) {
const results = {};
// Parser avec regex [TAG] contenu
const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
let content = match[2].trim();
// NOUVEAU: Nettoyer les tags XML qui peuvent apparaître dans le contenu
content = cleanXMLTagsFromContent(content);
parsedItems[tag] = content;
}
// Mapper aux vrais tags avec |
elements.forEach(element => {
const cleanTag = element.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
if (parsedItems[cleanTag] && parsedItems[cleanTag].length > 10) {
results[element.tag] = parsedItems[cleanTag];
logSh(`✅ Parsé [${cleanTag}]: "${parsedItems[cleanTag].substring(0, 100)}..."`, 'DEBUG');
} else {
// Fallback si parsing échoue ou contenu trop court
results[element.tag] = `Contenu professionnel pour ${element.element.name}`;
logSh(`⚠️ Fallback [${cleanTag}]: parsing échoué ou contenu invalide`, 'WARNING');
}
});
return results;
}
/**
* NOUVELLE FONCTION: Nettoyer les tags XML du contenu généré
*/
function cleanXMLTagsFromContent(content) {
if (!content) return content;
// Supprimer les tags XML avec **
content = content.replace(/\*\*[^*]+\*\*/g, '');
// Supprimer les préfixes de titres indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?(voici\s+le\s+topo\s+pour\s+)?Titre_[HU]\d+_\d+[.,\s]*/gi, '');
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?pour\s+Titre_[HU]\d+_\d+[.,\s]*/gi, '');
content = content.replace(/^(Bon,?\s*)?(donc,?\s*)?Titre_[HU]\d+_\d+[.,\s]*/gi, '');
// Supprimer les messages d'excuse
content = content.replace(/Oh là là,?\s*je\s*(suis\s*)?(\w+\s*)?désolée?\s*,?\s*mais\s*je\s*n'ai\s*pas\s*l'information.*?(?=\.|$)/gi, '');
content = content.replace(/Bon,?\s*passons\s*au\s*suivant.*?(?=\.|$)/gi, '');
content = content.replace(/je\s*ne\s*sais\s*pas\s*quoi\s*vous\s*dire.*?(?=\.|$)/gi, '');
content = content.replace(/encore\s*un\s*point\s*où\s*je\s*n'ai\s*pas\s*l'information.*?(?=\.|$)/gi, '');
// Réduire les répétitions excessives d'expressions familières
content = content.replace(/(du coup[,\s]+){3,}/gi, 'du coup ');
content = content.replace(/(bon[,\s]+){3,}/gi, 'bon ');
content = content.replace(/(franchement[,\s]+){3,}/gi, 'franchement ');
content = content.replace(/(alors[,\s]+){3,}/gi, 'alors ');
content = content.replace(/(nickel[,\s]+){2,}/gi, 'nickel ');
content = content.replace(/(tip-top[,\s]+){2,}/gi, 'tip-top ');
content = content.replace(/(costaud[,\s]+){2,}/gi, 'costaud ');
// Nettoyer espaces multiples et retours ligne
content = content.replace(/\s{2,}/g, ' ');
content = content.replace(/\n{2,}/g, '\n');
content = content.trim();
return content;
}
// ============= PARSING FUNCTIONS =============
// FONCTION SUPPRIMÉE : parseAllTechnicalTermsResponse() - Parser batch défaillant remplacé par traitement individuel
// FONCTIONS SUPPRIMÉES : parseTechnicalEnhancementBatchResponse() et parseTechnicalBatchResponse() - Remplacées par traitement individuel
// Placeholder pour les fonctions de parsing conservées qui suivent
function parseTransitionsBatchResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let content = match[2].trim();
// Appliquer le nettoyage XML
content = cleanXMLTagsFromContent(content);
if (content && content.length > 10) {
results[chunk[index].tag] = content;
} else {
// Fallback si contenu invalide
results[chunk[index].tag] = chunk[index].content; // Garder contenu original
}
index++;
}
return results;
}
function parseStyleBatchResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let content = match[2].trim();
// Appliquer le nettoyage XML
content = cleanXMLTagsFromContent(content);
if (content && content.length > 10) {
results[chunk[index].tag] = content;
} else {
// Fallback si contenu invalide
results[chunk[index].tag] = chunk[index].content; // Garder contenu original
}
index++;
}
return results;
}
// ============= ANALYSIS FUNCTIONS =============
/**
* Analyser besoin d'amélioration transitions
*/
function analyzeTransitionNeed(content) {
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
// Critères multiples d'analyse
const metrics = {
repetitiveConnectors: analyzeRepetitiveConnectors(content),
abruptTransitions: analyzeAbruptTransitions(sentences),
sentenceVariety: analyzeSentenceVariety(sentences),
formalityLevel: analyzeFormalityLevel(content),
overallLength: content.length
};
// Score de besoin (0-1)
let needScore = 0;
needScore += metrics.repetitiveConnectors * 0.3;
needScore += metrics.abruptTransitions * 0.4;
needScore += (1 - metrics.sentenceVariety) * 0.2;
needScore += metrics.formalityLevel * 0.1;
// Seuil ajustable selon longueur
const threshold = metrics.overallLength > 300 ? 0.4 : 0.6;
logSh(`🔍 Analyse transitions: score=${needScore.toFixed(2)}, seuil=${threshold}`, 'DEBUG');
return needScore > threshold;
}
function analyzeRepetitiveConnectors(content) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc'];
let totalConnectors = 0;
let repetitions = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
totalConnectors += matches.length;
if (matches.length > 1) repetitions += matches.length - 1;
});
return totalConnectors > 0 ? repetitions / totalConnectors : 0;
}
function analyzeAbruptTransitions(sentences) {
if (sentences.length < 2) return 0;
let abruptCount = 0;
for (let i = 1; i < sentences.length; i++) {
const current = sentences[i].trim();
const previous = sentences[i-1].trim();
const hasConnector = hasTransitionWord(current);
const topicContinuity = calculateTopicContinuity(previous, current);
// Transition abrupte = pas de connecteur + faible continuité thématique
if (!hasConnector && topicContinuity < 0.3) {
abruptCount++;
}
}
return abruptCount / (sentences.length - 1);
}
function analyzeSentenceVariety(sentences) {
if (sentences.length < 2) return 1;
const lengths = sentences.map(s => s.trim().length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
// Calculer variance des longueurs
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
// Score de variété (0-1) - plus la variance est élevée, plus c'est varié
return Math.min(1, stdDev / avgLength);
}
function analyzeFormalityLevel(content) {
const formalIndicators = [
'il convient de', 'par conséquent', 'néanmoins', 'toutefois',
'de surcroît', 'en définitive', 'il s\'avère que', 'force est de constater'
];
let formalCount = 0;
formalIndicators.forEach(indicator => {
if (content.toLowerCase().includes(indicator)) formalCount++;
});
const sentences = content.split(/[.!?]+/).length;
return sentences > 0 ? formalCount / sentences : 0;
}
function calculateTopicContinuity(sentence1, sentence2) {
const stopWords = ['les', 'des', 'une', 'sont', 'avec', 'pour', 'dans', 'cette', 'vous', 'peut', 'tout'];
const words1 = extractSignificantWords(sentence1, stopWords);
const words2 = extractSignificantWords(sentence2, stopWords);
if (words1.length === 0 || words2.length === 0) return 0;
const commonWords = words1.filter(word => words2.includes(word));
const semanticSimilarity = commonWords.length / Math.min(words1.length, words2.length);
const technicalWords = ['plaque', 'dibond', 'aluminium', 'impression', 'signalétique'];
const commonTechnical = commonWords.filter(word => technicalWords.includes(word));
const technicalBonus = commonTechnical.length * 0.2;
return Math.min(1, semanticSimilarity + technicalBonus);
}
function extractSignificantWords(sentence, stopWords) {
return sentence.toLowerCase()
.match(/\b[a-zàâäéèêëïîôùûüÿç]{4,}\b/g) // Mots 4+ lettres avec accents
?.filter(word => !stopWords.includes(word)) || [];
}
function hasTransitionWord(sentence) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite', 'puis', 'également', 'aussi', 'toutefois', 'néanmoins', 'alors', 'enfin'];
return connectors.some(connector => sentence.toLowerCase().includes(connector));
}
/**
* Instructions de style dynamiques
*/
function getPersonalityStyleInstructions(personality) {
// CORRECTION: Utilisation des VRAIS champs Google Sheets au lieu du hardcodé
if (!personality) return "Style professionnel standard";
const instructions = `STYLE ${personality.nom.toUpperCase()} (${personality.style}):
- Description: ${personality.description}
- Vocabulaire préféré: ${personality.vocabulairePref || 'professionnel, qualité'}
- Connecteurs préférés: ${personality.connecteursPref || 'par ailleurs, en effet'}
- Mots-clés secteurs: ${personality.motsClesSecteurs || 'technique, qualité'}
- Longueur phrases: ${personality.longueurPhrases || 'Moyennes (15-25 mots)'}
- Niveau technique: ${personality.niveauTechnique || 'Accessible'}
- Style CTA: ${personality.ctaStyle || 'Professionnel'}
- Défauts simulés: ${personality.defautsSimules || 'Aucun'}
- Erreurs typiques à éviter: ${personality.erreursTypiques || 'Répétitions, généralités'}`;
return instructions;
}
/**
* Créer prompt pour élément (fonction de base nécessaire)
*/
function createPromptForElement(element, csvData) {
const personality = csvData.personality;
const styleContext = `Rédige dans le style ${personality.style} de ${personality.nom} (${personality.description}).`;
switch (element.type) {
case 'titre_h1':
return `${styleContext}
MISSION: Crée un titre H1 accrocheur pour: ${csvData.mc0}
Référence: ${csvData.t0}
CONSIGNES: 10 mots maximum, direct et impactant, optimisé SEO.
RÉPONDS UNIQUEMENT PAR LE TITRE, sans introduction.`;
case 'titre_h2':
return `${styleContext}
MISSION: Crée un titre H2 optimisé SEO pour: ${csvData.mc0}
CONSIGNES: Intègre naturellement le mot-clé, 8 mots maximum.
RÉPONDS UNIQUEMENT PAR LE TITRE, sans introduction.`;
case 'intro':
if (element.instructions) {
return `${styleContext}
MISSION: ${element.instructions}
Données contextuelles:
- MC0: ${csvData.mc0}
- T-1: ${csvData.tMinus1}
- L-1: ${csvData.lMinus1}
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
}
return `${styleContext}
MISSION: Rédige une introduction de 100 mots pour ${csvData.mc0}.
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
case 'texte':
if (element.instructions) {
return `${styleContext}
MISSION: ${element.instructions}
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
}
return `${styleContext}
MISSION: Rédige un paragraphe de 150 mots sur ${csvData.mc0}.
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
case 'faq_question':
if (element.instructions) {
return `${styleContext}
MISSION: ${element.instructions}
CONTEXTE: ${csvData.mc0} - ${csvData.t0}
STYLE: Question ${csvData.personality?.style} de ${csvData.personality?.nom}
CONSIGNES:
- Vraie question que se poserait un client intéressé par ${csvData.mc0}
- Commence par "Comment", "Quel", "Pourquoi", "Où", "Quand" ou "Est-ce que"
- Maximum 15 mots, pratique et concrète
- Vocabulaire: ${csvData.personality?.vocabulairePref || 'accessible'}
RÉPONDS UNIQUEMENT PAR LA QUESTION, sans guillemets ni introduction.`;
}
return `${styleContext}
MISSION: Génère une vraie question FAQ client sur ${csvData.mc0}.
CONSIGNES:
- Question pratique et concrète qu'un client se poserait
- Commence par "Comment", "Quel", "Pourquoi", "Combien", "Où" ou "Est-ce que"
- Maximum 15 mots, style ${csvData.personality?.style}
- Vocabulaire: ${csvData.personality?.vocabulairePref || 'accessible'}
RÉPONDS UNIQUEMENT PAR LA QUESTION, sans guillemets ni introduction.`;
case 'faq_reponse':
if (element.instructions) {
return `${styleContext}
MISSION: ${element.instructions}
CONTEXTE: ${csvData.mc0} - ${csvData.t0}
STYLE: Réponse ${csvData.personality?.style} de ${csvData.personality?.nom}
CONSIGNES:
- Réponse utile et rassurante
- 50-80 mots, ton ${csvData.personality?.style}
- Vocabulaire: ${csvData.personality?.vocabulairePref}
- Connecteurs: ${csvData.personality?.connecteursPref}
RÉPONDS UNIQUEMENT PAR LA RÉPONSE, sans introduction.`;
}
return `${styleContext}
MISSION: Réponds à une question client sur ${csvData.mc0}.
CONSIGNES:
- Réponse utile, claire et rassurante
- 50-80 mots, ton ${csvData.personality?.style} de ${csvData.personality?.nom}
- Vocabulaire: ${csvData.personality?.vocabulairePref || 'professionnel'}
- Connecteurs: ${csvData.personality?.connecteursPref || 'par ailleurs'}
RÉPONDS UNIQUEMENT PAR LA RÉPONSE, sans introduction.`;
default:
return `${styleContext}
MISSION: Génère du contenu pertinent pour ${csvData.mc0}.
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
}
}
/**
* NOUVELLE FONCTION : Extraction batch TOUS les termes techniques
*/
async function extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider) {
const contentEntries = Object.keys(baseContents);
const batchAnalysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTENUS À ANALYSER:
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
CONTENU: "${baseContents[tag]}"`).join('\n\n')}
CONSIGNES:
- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
- Évite mots génériques (qualité, service, pratique, personnalisé, etc.)
- Focus: matériaux, procédés, normes, dimensions, technologies
- Si aucun terme technique → "AUCUN"
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique
FORMAT RÉPONSE EXACT:
[1] dibond, impression UV, 3mm OU AUCUN
[2] aluminium, fraisage CNC OU AUCUN
[3] AUCUN
etc... (${contentEntries.length} lignes total)`;
try {
const analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, {
temperature: 0.3,
maxTokens: 2000
}, csvData.personality);
return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries);
} catch (error) {
logSh(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`);
}
}
/**
* NOUVELLE FONCTION : Enhancement batch TOUS les éléments
*/
async function enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider) {
if (elementsNeedingEnhancement.length === 0) return {};
const batchEnhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces ${elementsNeedingEnhancement.length} contenus.
CONTEXTE: Article SEO pour site e-commerce de signalétique
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} web professionnel)
SUJET: ${csvData.mc0} - Secteur: Signalétique/impression
VOCABULAIRE PRÉFÉRÉ: ${csvData.personality?.vocabulairePref}
CONTENUS + TERMES À AMÉLIORER:
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag}
CONTENU ACTUEL: "${item.content}"
TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')}
CONSIGNES STRICTES:
- Améliore UNIQUEMENT la précision technique, garde le style ${csvData.personality?.nom}
- GARDE la même longueur, structure et ton
- Intègre naturellement les termes techniques listés
- NE CHANGE PAS le fond du message ni le style personnel
- Utilise un vocabulaire expert mais accessible
- ÉVITE les répétitions excessives
- RESPECTE le niveau technique: ${csvData.personality?.niveauTechnique}
- Termes techniques secteur: dibond, aluminium, impression UV, fraisage, épaisseur, PMMA
FORMAT RÉPONSE:
[1] Contenu avec amélioration technique selon ${csvData.personality?.nom}
[2] Contenu avec amélioration technique selon ${csvData.personality?.nom}
etc... (${elementsNeedingEnhancement.length} éléments total)`;
try {
const enhanced = await callLLM(aiProvider, batchEnhancementPrompt, {
temperature: 0.4,
maxTokens: 5000 // Plus large pour batch total
}, csvData.personality);
return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement);
} catch (error) {
logSh(`❌ FATAL: Enhancement technique batch échoué: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Enhancement technique batch impossible - arrêt du workflow: ${error.message}`);
}
}
/**
* Parser réponse extraction termes
*/
function parseAllTechnicalTermsResponse(response, baseContents, contentEntries) {
const results = [];
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
const parsedItems = {};
// Parser la réponse
while ((match = regex.exec(response)) !== null) {
const index = parseInt(match[1]) - 1; // Convertir en 0-indexé
const termsText = match[2].trim();
parsedItems[index] = termsText;
}
// Mapper aux éléments
contentEntries.forEach((tag, index) => {
const termsText = parsedItems[index] || 'AUCUN';
const hasTerms = !termsText.toUpperCase().includes('AUCUN');
const technicalTerms = hasTerms ?
termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) :
[];
results.push({
tag: tag,
content: baseContents[tag],
technicalTerms: technicalTerms,
needsEnhancement: hasTerms && technicalTerms.length > 0
});
logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'pas de termes techniques'}`, 'DEBUG');
});
const enhancementCount = results.filter(r => r.needsEnhancement).length;
logSh(`📊 Analyse terminée: ${enhancementCount}/${contentEntries.length} éléments ont besoin d'enhancement`, 'INFO');
return results;
}
/**
* Parser réponse enhancement technique
*/
function parseTechnicalEnhancementBatchResponse(response, elementsNeedingEnhancement) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < elementsNeedingEnhancement.length) {
let content = match[2].trim();
const element = elementsNeedingEnhancement[index];
// NOUVEAU: Appliquer le nettoyage XML
content = cleanXMLTagsFromContent(content);
if (content && content.length > 10) {
results[element.tag] = content;
logSh(`✅ Enhanced [${element.tag}]: "${content.substring(0, 100)}..."`, 'DEBUG');
} else {
// Fallback si contenu invalide après nettoyage
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: contenu invalide après nettoyage`, 'WARNING');
}
index++;
}
// Vérifier si on a bien tout parsé
if (Object.keys(results).length < elementsNeedingEnhancement.length) {
logSh(`⚠️ Parsing partiel: ${Object.keys(results).length}/${elementsNeedingEnhancement.length}`, 'WARNING');
// Compléter avec contenu original pour les manquants
elementsNeedingEnhancement.forEach(element => {
if (!results[element.tag]) {
results[element.tag] = element.content;
}
});
}
return results;
}
// ============= EXPORTS =============
module.exports = {
generateWithBatchEnhancement,
generateAllContentBase,
enhanceAllTechnicalTerms,
enhanceAllTransitions,
enhanceAllPersonalityStyle,
collectAllElements,
groupElementsByType,
chunkArray,
createBatchBasePrompt,
parseBatchResponse,
cleanXMLTagsFromContent,
analyzeTransitionNeed,
getPersonalityStyleInstructions,
createPromptForElement,
sleep,
separateFAQPairsAndOthers,
generateFAQPairsRestored,
createBatchFAQPairsPrompt,
parseFAQPairsResponse,
cleanFAQInstructions,
extractAllTechnicalTermsBatch,
enhanceAllElementsTechnicalBatch,
parseAllTechnicalTermsResponse,
parseTechnicalEnhancementBatchResponse
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/trace-wrap.js │
└────────────────────────────────────────────────────────────────────┘
*/
// lib/trace-wrap.js
const { tracer } = require('./trace.js');
const traced = (name, fn, attrs) => (...args) =>
tracer.run(name, () => fn(...args), attrs);
module.exports = {
traced
};
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/Utils.js │
└────────────────────────────────────────────────────────────────────┘
*/
// ========================================
// FICHIER: utils.js - Conversion Node.js
// Description: Utilitaires génériques pour le workflow
// ========================================
// Import du système de logging (assumant que logSh est disponible globalement)
// const { logSh } = require('./logging'); // À décommenter si logSh est dans un module séparé
/**
* Créer une réponse de succès standardisée
* @param {Object} data - Données à retourner
* @returns {Object} Réponse formatée pour Express/HTTP
*/
function createSuccessResponse(data) {
return {
success: true,
data: data,
timestamp: new Date().toISOString()
};
}
/**
* Créer une réponse d'erreur standardisée
* @param {string|Error} error - Message d'erreur ou objet Error
* @returns {Object} Réponse d'erreur formatée
*/
function createErrorResponse(error) {
const errorMessage = error instanceof Error ? error.message : error.toString();
return {
success: false,
error: errorMessage,
timestamp: new Date().toISOString(),
stack: process.env.NODE_ENV === 'development' && error instanceof Error ? error.stack : undefined
};
}
/**
* Middleware Express pour envoyer des réponses standardisées
* Usage: res.success(data) ou res.error(error)
*/
function responseMiddleware(req, res, next) {
// Méthode pour réponse de succès
res.success = (data, statusCode = 200) => {
res.status(statusCode).json(createSuccessResponse(data));
};
// Méthode pour réponse d'erreur
res.error = (error, statusCode = 500) => {
res.status(statusCode).json(createErrorResponse(error));
};
next();
}
/**
* HELPER : Nettoyer les instructions FAQ
* Remplace les variables et nettoie le HTML
* @param {string} instructions - Instructions à nettoyer
* @param {Object} csvData - Données CSV pour remplacement variables
* @returns {string} Instructions nettoyées
*/
function cleanFAQInstructions(instructions, csvData) {
if (!instructions || !csvData) {
return instructions || '';
}
let clean = instructions.toString();
try {
// Remplacer variables simples
clean = clean.replace(/\{\{MC0\}\}/g, csvData.mc0 || '');
clean = clean.replace(/\{\{T0\}\}/g, csvData.t0 || '');
// Variables multiples si nécessaire
if (csvData.mcPlus1) {
const mcPlus1 = csvData.mcPlus1.split(',').map(s => s.trim());
for (let i = 1; i <= 6; i++) {
const mcValue = mcPlus1[i-1] || `[MC+1_${i} non défini]`;
clean = clean.replace(new RegExp(`\\{\\{MC\\+1_${i}\\}\\}`, 'g'), mcValue);
}
}
// Variables T+1 et L+1 si disponibles
if (csvData.tPlus1) {
const tPlus1 = csvData.tPlus1.split(',').map(s => s.trim());
for (let i = 1; i <= 6; i++) {
const tValue = tPlus1[i-1] || `[T+1_${i} non défini]`;
clean = clean.replace(new RegExp(`\\{\\{T\\+1_${i}\\}\\}`, 'g'), tValue);
}
}
if (csvData.lPlus1) {
const lPlus1 = csvData.lPlus1.split(',').map(s => s.trim());
for (let i = 1; i <= 6; i++) {
const lValue = lPlus1[i-1] || `[L+1_${i} non défini]`;
clean = clean.replace(new RegExp(`\\{\\{L\\+1_${i}\\}\\}`, 'g'), lValue);
}
}
// Nettoyer HTML
clean = clean.replace(/<\/?[^>]+>/g, '');
// Nettoyer espaces en trop
clean = clean.replace(/\s+/g, ' ').trim();
} catch (error) {
if (typeof logSh === 'function') {
logSh(`⚠️ Erreur nettoyage instructions FAQ: ${error.toString()}`, 'WARNING');
}
// Retourner au moins la version partiellement nettoyée
}
return clean;
}
/**
* Utilitaire pour attendre un délai (remplace Utilities.sleep de Google Apps Script)
* @param {number} ms - Millisecondes à attendre
* @returns {Promise} Promise qui se résout après le délai
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Utilitaire pour encoder en base64
* @param {string} text - Texte à encoder
* @returns {string} Texte encodé en base64
*/
function base64Encode(text) {
return Buffer.from(text, 'utf8').toString('base64');
}
/**
* Utilitaire pour décoder du base64
* @param {string} base64Text - Texte base64 à décoder
* @returns {string} Texte décodé
*/
function base64Decode(base64Text) {
return Buffer.from(base64Text, 'base64').toString('utf8');
}
/**
* Valider et nettoyer un slug/filename
* @param {string} slug - Slug à nettoyer
* @returns {string} Slug nettoyé
*/
function cleanSlug(slug) {
if (!slug) return '';
return slug
.toString()
.toLowerCase()
.replace(/[^a-z0-9\-_]/g, '-') // Remplacer caractères spéciaux par -
.replace(/-+/g, '-') // Éviter doubles tirets
.replace(/^-+|-+$/g, ''); // Enlever tirets début/fin
}
/**
* Compter les mots dans un texte
* @param {string} text - Texte à analyser
* @returns {number} Nombre de mots
*/
function countWords(text) {
if (!text || typeof text !== 'string') return 0;
return text
.trim()
.replace(/\s+/g, ' ') // Normaliser espaces
.split(' ')
.filter(word => word.length > 0)
.length;
}
/**
* Formater une durée en millisecondes en format lisible
* @param {number} ms - Durée en millisecondes
* @returns {string} Durée formatée (ex: "2.3s" ou "450ms")
*/
function formatDuration(ms) {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60000) {
return `${(ms / 1000).toFixed(1)}s`;
} else {
const minutes = Math.floor(ms / 60000);
const seconds = ((ms % 60000) / 1000).toFixed(1);
return `${minutes}m ${seconds}s`;
}
}
/**
* Utilitaire pour retry automatique d'une fonction
* @param {Function} fn - Fonction à exécuter avec retry
* @param {number} maxRetries - Nombre maximum de tentatives
* @param {number} delay - Délai entre tentatives (ms)
* @returns {Promise} Résultat de la fonction ou erreur finale
*/
async function withRetry(fn, maxRetries = 3, delay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (typeof logSh === 'function') {
logSh(`⚠️ Tentative ${attempt}/${maxRetries} échouée: ${error.toString()}`, 'WARNING');
}
if (attempt < maxRetries) {
await sleep(delay * attempt); // Exponential backoff
}
}
}
throw lastError;
}
/**
* Validation basique d'email
* @param {string} email - Email à valider
* @returns {boolean} True si email valide
*/
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* Générer un ID unique simple
* @returns {string} ID unique basé sur timestamp + random
*/
function generateId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Truncate un texte à une longueur donnée
* @param {string} text - Texte à tronquer
* @param {number} maxLength - Longueur maximale
* @param {string} suffix - Suffixe à ajouter si tronqué (défaut: '...')
* @returns {string} Texte tronqué
*/
function truncate(text, maxLength, suffix = '...') {
if (!text || text.length <= maxLength) {
return text;
}
return text.substring(0, maxLength - suffix.length) + suffix;
}
// ============= EXPORTS =============
module.exports = {
createSuccessResponse,
createErrorResponse,
responseMiddleware,
cleanFAQInstructions,
sleep,
base64Encode,
base64Decode,
cleanSlug,
countWords,
formatDuration,
withRetry,
isValidEmail,
generateId,
truncate
};