- New modular pipeline architecture allowing custom workflow combinations
- Per-step LLM provider configuration (Claude, OpenAI, Gemini, Deepseek, Moonshot, Mistral)
- Visual pipeline builder and runner interfaces with drag-and-drop
- 10 predefined pipeline templates (minimal-test to originality-bypass)
- Pipeline CRUD operations via ConfigManager and REST API
- Fix variable resolution in instructions (HTML tags were breaking {{variables}})
- Fix hardcoded LLM providers in AdversarialCore
- Add TESTS_LLM_PROVIDER.md documentation with validation results
- Update dashboard to disable legacy config editor
API Endpoints:
- POST /api/pipeline/save, execute, validate, estimate
- GET /api/pipeline/list, modules, templates
Backward compatible with legacy modular workflow system.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
752 lines
24 KiB
JavaScript
752 lines
24 KiB
JavaScript
// ========================================
|
|
// SELECTIVE UTILS - UTILITAIRES MODULAIRES
|
|
// Responsabilité: Fonctions utilitaires partagées par tous les modules selective
|
|
// Architecture: Helper functions réutilisables et composables
|
|
// ========================================
|
|
|
|
const { logSh } = require('../ErrorReporting');
|
|
|
|
/**
|
|
* ANALYSEURS DE CONTENU SELECTIVE
|
|
*/
|
|
|
|
/**
|
|
* Analyser qualité technique d'un contenu
|
|
*/
|
|
function analyzeTechnicalQuality(content, contextualTerms = []) {
|
|
if (!content || typeof content !== 'string') return { score: 0, details: {} };
|
|
|
|
const analysis = {
|
|
score: 0,
|
|
details: {
|
|
technicalTermsFound: 0,
|
|
technicalTermsExpected: contextualTerms.length,
|
|
genericWordsCount: 0,
|
|
hasSpecifications: false,
|
|
hasDimensions: false,
|
|
contextIntegration: 0
|
|
}
|
|
};
|
|
|
|
const lowerContent = content.toLowerCase();
|
|
|
|
// 1. Compter termes techniques présents
|
|
contextualTerms.forEach(term => {
|
|
if (lowerContent.includes(term.toLowerCase())) {
|
|
analysis.details.technicalTermsFound++;
|
|
}
|
|
});
|
|
|
|
// 2. Détecter mots génériques
|
|
const genericWords = ['produit', 'solution', 'service', 'offre', 'article', 'élément'];
|
|
analysis.details.genericWordsCount = genericWords.filter(word =>
|
|
lowerContent.includes(word)
|
|
).length;
|
|
|
|
// 3. Vérifier spécifications techniques
|
|
analysis.details.hasSpecifications = /\b(norme|iso|din|ce)\b/i.test(content);
|
|
|
|
// 4. Vérifier dimensions/données techniques
|
|
analysis.details.hasDimensions = /\d+\s*(mm|cm|m|%|°|kg|g)\b/i.test(content);
|
|
|
|
// 5. Calculer score global (0-100)
|
|
const termRatio = contextualTerms.length > 0 ?
|
|
(analysis.details.technicalTermsFound / contextualTerms.length) * 40 : 20;
|
|
const genericPenalty = Math.min(20, analysis.details.genericWordsCount * 5);
|
|
const specificationBonus = analysis.details.hasSpecifications ? 15 : 0;
|
|
const dimensionBonus = analysis.details.hasDimensions ? 15 : 0;
|
|
const lengthBonus = content.length > 100 ? 10 : 0;
|
|
|
|
analysis.score = Math.max(0, Math.min(100,
|
|
termRatio + specificationBonus + dimensionBonus + lengthBonus - genericPenalty
|
|
));
|
|
|
|
return analysis;
|
|
}
|
|
|
|
/**
|
|
* Analyser fluidité des transitions
|
|
*/
|
|
function analyzeTransitionFluidity(content) {
|
|
if (!content || typeof content !== 'string') return { score: 0, details: {} };
|
|
|
|
const sentences = content.split(/[.!?]+/)
|
|
.map(s => s.trim())
|
|
.filter(s => s.length > 5);
|
|
|
|
if (sentences.length < 2) {
|
|
return { score: 100, details: { reason: 'Contenu trop court pour analyse transitions' } };
|
|
}
|
|
|
|
const analysis = {
|
|
score: 0,
|
|
details: {
|
|
sentencesCount: sentences.length,
|
|
connectorsFound: 0,
|
|
repetitiveConnectors: 0,
|
|
abruptTransitions: 0,
|
|
averageSentenceLength: 0,
|
|
lengthVariation: 0
|
|
}
|
|
};
|
|
|
|
// 1. Analyser connecteurs
|
|
const commonConnectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite'];
|
|
const connectorCounts = {};
|
|
|
|
commonConnectors.forEach(connector => {
|
|
const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
|
|
connectorCounts[connector] = matches.length;
|
|
analysis.details.connectorsFound += matches.length;
|
|
if (matches.length > 1) analysis.details.repetitiveConnectors++;
|
|
});
|
|
|
|
// 2. Détecter transitions abruptes
|
|
for (let i = 1; i < sentences.length; i++) {
|
|
const sentence = sentences[i].toLowerCase().trim();
|
|
const hasConnector = commonConnectors.some(connector =>
|
|
sentence.startsWith(connector) || sentence.includes(` ${connector} `)
|
|
);
|
|
|
|
if (!hasConnector && sentence.length > 20) {
|
|
analysis.details.abruptTransitions++;
|
|
}
|
|
}
|
|
|
|
// 3. Analyser variation de longueur
|
|
const lengths = sentences.map(s => s.split(/\s+/).length);
|
|
analysis.details.averageSentenceLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
|
|
|
|
const variance = lengths.reduce((acc, len) =>
|
|
acc + Math.pow(len - analysis.details.averageSentenceLength, 2), 0
|
|
) / lengths.length;
|
|
analysis.details.lengthVariation = Math.sqrt(variance);
|
|
|
|
// 4. Calculer score fluidité (0-100)
|
|
const connectorScore = Math.min(30, (analysis.details.connectorsFound / sentences.length) * 100);
|
|
const repetitionPenalty = Math.min(20, analysis.details.repetitiveConnectors * 5);
|
|
const abruptPenalty = Math.min(30, (analysis.details.abruptTransitions / sentences.length) * 50);
|
|
const variationScore = Math.min(20, analysis.details.lengthVariation * 2);
|
|
|
|
analysis.score = Math.max(0, Math.min(100,
|
|
connectorScore + variationScore - repetitionPenalty - abruptPenalty + 50
|
|
));
|
|
|
|
return analysis;
|
|
}
|
|
|
|
/**
|
|
* Analyser cohérence de style
|
|
*/
|
|
function analyzeStyleConsistency(content, expectedPersonality = null) {
|
|
if (!content || typeof content !== 'string') return { score: 0, details: {} };
|
|
|
|
const analysis = {
|
|
score: 0,
|
|
details: {
|
|
personalityAlignment: 0,
|
|
toneConsistency: 0,
|
|
vocabularyLevel: 'standard',
|
|
formalityScore: 0,
|
|
personalityWordsFound: 0
|
|
}
|
|
};
|
|
|
|
// 1. Analyser alignement personnalité
|
|
if (expectedPersonality && expectedPersonality.vocabulairePref) {
|
|
const personalityWords = expectedPersonality.vocabulairePref.toLowerCase().split(',');
|
|
const contentLower = content.toLowerCase();
|
|
|
|
personalityWords.forEach(word => {
|
|
if (word.trim() && contentLower.includes(word.trim())) {
|
|
analysis.details.personalityWordsFound++;
|
|
}
|
|
});
|
|
|
|
analysis.details.personalityAlignment = personalityWords.length > 0 ?
|
|
(analysis.details.personalityWordsFound / personalityWords.length) * 100 : 0;
|
|
}
|
|
|
|
// 2. Analyser niveau vocabulaire
|
|
const technicalWords = content.match(/\b\w{8,}\b/g) || [];
|
|
const totalWords = content.split(/\s+/).length;
|
|
const techRatio = technicalWords.length / totalWords;
|
|
|
|
if (techRatio > 0.15) analysis.details.vocabularyLevel = 'expert';
|
|
else if (techRatio < 0.05) analysis.details.vocabularyLevel = 'accessible';
|
|
else analysis.details.vocabularyLevel = 'standard';
|
|
|
|
// 3. Analyser formalité
|
|
const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois'];
|
|
const casualIndicators = ['du coup', 'sympa', 'cool', 'nickel'];
|
|
|
|
let formalCount = formalIndicators.filter(indicator =>
|
|
content.toLowerCase().includes(indicator)
|
|
).length;
|
|
|
|
let casualCount = casualIndicators.filter(indicator =>
|
|
content.toLowerCase().includes(indicator)
|
|
).length;
|
|
|
|
analysis.details.formalityScore = formalCount - casualCount; // Positif = formel, négatif = casual
|
|
|
|
// 4. Calculer score cohérence (0-100)
|
|
let baseScore = 50;
|
|
|
|
if (expectedPersonality) {
|
|
baseScore += analysis.details.personalityAlignment * 0.3;
|
|
|
|
// Ajustements selon niveau technique attendu
|
|
const expectedLevel = expectedPersonality.niveauTechnique || 'standard';
|
|
if (expectedLevel === analysis.details.vocabularyLevel) {
|
|
baseScore += 20;
|
|
} else {
|
|
baseScore -= 10;
|
|
}
|
|
}
|
|
|
|
// Bonus cohérence tonale
|
|
const sentences = content.split(/[.!?]+/).filter(s => s.length > 10);
|
|
if (sentences.length > 1) {
|
|
baseScore += Math.min(20, analysis.details.lengthVariation || 10);
|
|
}
|
|
|
|
analysis.score = Math.max(0, Math.min(100, baseScore));
|
|
|
|
return analysis;
|
|
}
|
|
|
|
/**
|
|
* COMPARATEURS ET MÉTRIQUES
|
|
*/
|
|
|
|
/**
|
|
* Comparer deux contenus et calculer taux amélioration
|
|
*/
|
|
function compareContentImprovement(original, enhanced, analysisType = 'general') {
|
|
if (!original || !enhanced) return { improvementRate: 0, details: {} };
|
|
|
|
const comparison = {
|
|
improvementRate: 0,
|
|
details: {
|
|
lengthChange: ((enhanced.length - original.length) / original.length) * 100,
|
|
wordCountChange: 0,
|
|
structuralChanges: 0,
|
|
contentPreserved: true
|
|
}
|
|
};
|
|
|
|
// 1. Analyser changements structurels
|
|
const originalSentences = original.split(/[.!?]+/).length;
|
|
const enhancedSentences = enhanced.split(/[.!?]+/).length;
|
|
comparison.details.structuralChanges = Math.abs(enhancedSentences - originalSentences);
|
|
|
|
// 2. Analyser changements de mots
|
|
const originalWords = original.toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
const enhancedWords = enhanced.toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
comparison.details.wordCountChange = enhancedWords.length - originalWords.length;
|
|
|
|
// 3. Vérifier préservation du contenu principal
|
|
const originalKeyWords = originalWords.filter(w => w.length > 4);
|
|
const preservedWords = originalKeyWords.filter(w => enhanced.toLowerCase().includes(w));
|
|
comparison.details.contentPreserved = (preservedWords.length / originalKeyWords.length) > 0.7;
|
|
|
|
// 4. Calculer taux amélioration selon type d'analyse
|
|
switch (analysisType) {
|
|
case 'technical':
|
|
const originalTech = analyzeTechnicalQuality(original);
|
|
const enhancedTech = analyzeTechnicalQuality(enhanced);
|
|
comparison.improvementRate = enhancedTech.score - originalTech.score;
|
|
break;
|
|
|
|
case 'transitions':
|
|
const originalFluid = analyzeTransitionFluidity(original);
|
|
const enhancedFluid = analyzeTransitionFluidity(enhanced);
|
|
comparison.improvementRate = enhancedFluid.score - originalFluid.score;
|
|
break;
|
|
|
|
case 'style':
|
|
const originalStyle = analyzeStyleConsistency(original);
|
|
const enhancedStyle = analyzeStyleConsistency(enhanced);
|
|
comparison.improvementRate = enhancedStyle.score - originalStyle.score;
|
|
break;
|
|
|
|
default:
|
|
// Amélioration générale (moyenne pondérée)
|
|
comparison.improvementRate = Math.min(50, Math.abs(comparison.details.lengthChange) * 0.1 +
|
|
(comparison.details.contentPreserved ? 20 : -20) +
|
|
Math.min(15, Math.abs(comparison.details.wordCountChange)));
|
|
}
|
|
|
|
return comparison;
|
|
}
|
|
|
|
/**
|
|
* UTILITAIRES DE CONTENU
|
|
*/
|
|
|
|
/**
|
|
* Nettoyer contenu généré par LLM
|
|
*/
|
|
function cleanGeneratedContent(content, cleaningLevel = 'standard') {
|
|
if (!content || typeof content !== 'string') return content;
|
|
|
|
let cleaned = content.trim();
|
|
|
|
// Nettoyage de base
|
|
cleaned = cleaned.replace(/^(voici\s+)?le\s+contenu\s+(amélioré|modifié|réécrit)[:\s]*/gi, '');
|
|
cleaned = cleaned.replace(/^(bon,?\s*)?(alors,?\s*)?(voici\s+)?/gi, '');
|
|
cleaned = cleaned.replace(/^(avec\s+les?\s+)?améliorations?\s*[:\s]*/gi, '');
|
|
|
|
// Nettoyage formatage
|
|
cleaned = cleaned.replace(/\*\*([^*]+)\*\*/g, '$1'); // Gras markdown → texte normal
|
|
cleaned = cleaned.replace(/\s{2,}/g, ' '); // Espaces multiples
|
|
cleaned = cleaned.replace(/([.!?])\s*([.!?])/g, '$1 '); // Double ponctuation
|
|
|
|
if (cleaningLevel === 'intensive') {
|
|
// Nettoyage intensif
|
|
cleaned = cleaned.replace(/^\s*[-*+]\s*/gm, ''); // Puces en début de ligne
|
|
cleaned = cleaned.replace(/^(pour\s+)?(ce\s+)?(contenu\s*)?[,:]?\s*/gi, '');
|
|
cleaned = cleaned.replace(/\([^)]*\)/g, ''); // Parenthèses et contenu
|
|
}
|
|
|
|
// Nettoyage final
|
|
cleaned = cleaned.replace(/^[,.\s]+/, ''); // Début
|
|
cleaned = cleaned.replace(/[,\s]+$/, ''); // Fin
|
|
cleaned = cleaned.trim();
|
|
|
|
return cleaned;
|
|
}
|
|
|
|
/**
|
|
* Valider contenu selective
|
|
*/
|
|
function validateSelectiveContent(content, originalContent, criteria = {}) {
|
|
const validation = {
|
|
isValid: true,
|
|
score: 0,
|
|
issues: [],
|
|
suggestions: []
|
|
};
|
|
|
|
const {
|
|
minLength = 20,
|
|
maxLengthChange = 50, // % de changement maximum
|
|
preserveContent = true,
|
|
checkTechnicalTerms = true
|
|
} = criteria;
|
|
|
|
// 1. Vérifier longueur
|
|
if (!content || content.length < minLength) {
|
|
validation.isValid = false;
|
|
validation.issues.push('Contenu trop court');
|
|
validation.suggestions.push('Augmenter la longueur du contenu généré');
|
|
} else {
|
|
validation.score += 25;
|
|
}
|
|
|
|
// 2. Vérifier changements de longueur
|
|
if (originalContent) {
|
|
const lengthChange = Math.abs((content.length - originalContent.length) / originalContent.length) * 100;
|
|
|
|
if (lengthChange > maxLengthChange) {
|
|
validation.issues.push('Changement de longueur excessif');
|
|
validation.suggestions.push('Réduire l\'intensité d\'amélioration');
|
|
} else {
|
|
validation.score += 25;
|
|
}
|
|
|
|
// 3. Vérifier préservation du contenu
|
|
if (preserveContent) {
|
|
const preservation = compareContentImprovement(originalContent, content);
|
|
|
|
if (!preservation.details.contentPreserved) {
|
|
validation.isValid = false;
|
|
validation.issues.push('Contenu original non préservé');
|
|
validation.suggestions.push('Améliorer conservation du sens original');
|
|
} else {
|
|
validation.score += 25;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Vérifications spécifiques
|
|
if (checkTechnicalTerms) {
|
|
const technicalQuality = analyzeTechnicalQuality(content);
|
|
|
|
if (technicalQuality.score > 60) {
|
|
validation.score += 25;
|
|
} else if (technicalQuality.score < 30) {
|
|
validation.issues.push('Qualité technique insuffisante');
|
|
validation.suggestions.push('Ajouter plus de termes techniques spécialisés');
|
|
}
|
|
}
|
|
|
|
// Score final et validation
|
|
validation.score = Math.min(100, validation.score);
|
|
validation.isValid = validation.isValid && validation.score >= 60;
|
|
|
|
return validation;
|
|
}
|
|
|
|
/**
|
|
* UTILITAIRES TECHNIQUES
|
|
*/
|
|
|
|
/**
|
|
* Chunk array avec gestion intelligente
|
|
*/
|
|
function chunkArray(array, chunkSize, smartChunking = false) {
|
|
if (!Array.isArray(array)) return [];
|
|
if (array.length <= chunkSize) return [array];
|
|
|
|
const chunks = [];
|
|
|
|
if (smartChunking) {
|
|
// Chunking intelligent : éviter de séparer éléments liés
|
|
let currentChunk = [];
|
|
|
|
for (let i = 0; i < array.length; i++) {
|
|
currentChunk.push(array[i]);
|
|
|
|
// Conditions de fin de chunk intelligente
|
|
const isChunkFull = currentChunk.length >= chunkSize;
|
|
const isLastElement = i === array.length - 1;
|
|
const nextElementRelated = i < array.length - 1 &&
|
|
array[i].tag && array[i + 1].tag &&
|
|
array[i].tag.includes('FAQ') && array[i + 1].tag.includes('FAQ');
|
|
|
|
if ((isChunkFull && !nextElementRelated) || isLastElement) {
|
|
chunks.push([...currentChunk]);
|
|
currentChunk = [];
|
|
}
|
|
}
|
|
|
|
// Ajouter chunk restant si non vide
|
|
if (currentChunk.length > 0) {
|
|
if (chunks.length > 0 && chunks[chunks.length - 1].length + currentChunk.length <= chunkSize * 1.2) {
|
|
// Merger avec dernier chunk si pas trop gros
|
|
chunks[chunks.length - 1].push(...currentChunk);
|
|
} else {
|
|
chunks.push(currentChunk);
|
|
}
|
|
}
|
|
} else {
|
|
// Chunking standard
|
|
for (let i = 0; i < array.length; i += chunkSize) {
|
|
chunks.push(array.slice(i, i + chunkSize));
|
|
}
|
|
}
|
|
|
|
return chunks;
|
|
}
|
|
|
|
/**
|
|
* Sleep avec logging optionnel
|
|
*/
|
|
async function sleep(ms, logMessage = null) {
|
|
if (logMessage) {
|
|
logSh(`⏳ ${logMessage} (${ms}ms)`, 'DEBUG');
|
|
}
|
|
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
/**
|
|
* Mesurer performance d'opération
|
|
*/
|
|
function measurePerformance(operationName, startTime = Date.now()) {
|
|
const endTime = Date.now();
|
|
const duration = endTime - startTime;
|
|
|
|
const performance = {
|
|
operationName,
|
|
startTime,
|
|
endTime,
|
|
duration,
|
|
durationFormatted: formatDuration(duration)
|
|
};
|
|
|
|
return performance;
|
|
}
|
|
|
|
/**
|
|
* Formater durée en format lisible
|
|
*/
|
|
function formatDuration(ms) {
|
|
if (ms < 1000) return `${ms}ms`;
|
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
|
|
}
|
|
|
|
/**
|
|
* GÉNÉRATION SIMPLE (REMPLACE CONTENTGENERATION.JS)
|
|
*/
|
|
|
|
/**
|
|
* Génération simple avec LLM configurable (compatible avec l'ancien système)
|
|
*/
|
|
async function generateSimple(hierarchy, csvData, options = {}) {
|
|
const LLMManager = require('../LLMManager');
|
|
|
|
const llmProvider = options.llmProvider || 'claude';
|
|
|
|
logSh(`🔥 Génération simple avec ${llmProvider.toUpperCase()}`, 'INFO');
|
|
|
|
if (!hierarchy || Object.keys(hierarchy).length === 0) {
|
|
throw new Error('Hiérarchie vide ou invalide');
|
|
}
|
|
|
|
const result = {
|
|
content: {},
|
|
stats: {
|
|
processed: 0,
|
|
enhanced: 0,
|
|
duration: 0,
|
|
llmProvider: llmProvider
|
|
}
|
|
};
|
|
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// Générer chaque élément avec Claude
|
|
for (const [tag, item] of Object.entries(hierarchy)) {
|
|
try {
|
|
logSh(`🎯 Génération: ${tag}`, 'DEBUG');
|
|
|
|
// Extraire l'instruction correctement selon la structure
|
|
let instruction = '';
|
|
if (typeof item === 'string') {
|
|
instruction = item;
|
|
} else if (item.instructions) {
|
|
instruction = item.instructions;
|
|
} else if (item.title && item.title.instructions) {
|
|
instruction = item.title.instructions;
|
|
} else if (item.text && item.text.instructions) {
|
|
instruction = item.text.instructions;
|
|
} else {
|
|
logSh(`⚠️ Pas d'instruction trouvée pour ${tag}, structure: ${JSON.stringify(Object.keys(item))}`, 'WARNING');
|
|
continue; // Skip cet élément
|
|
}
|
|
|
|
// Fonction pour résoudre les variables dans les instructions
|
|
const resolveVariables = (text, csvData) => {
|
|
return text.replace(/\{\{?([^}]+)\}?\}/g, (match, variable) => {
|
|
const cleanVar = variable.trim();
|
|
|
|
// Variables simples
|
|
if (cleanVar === 'MC0') return csvData.mc0 || '';
|
|
if (cleanVar === 'T0') return csvData.t0 || '';
|
|
if (cleanVar === 'T-1') return csvData.tMinus1 || '';
|
|
if (cleanVar === 'L-1') return csvData.lMinus1 || '';
|
|
|
|
// Variables avec index MC+1_X
|
|
if (cleanVar.startsWith('MC+1_')) {
|
|
const index = parseInt(cleanVar.split('_')[1]) - 1;
|
|
const mcPlus1 = (csvData.mcPlus1 || '').split(',').map(s => s.trim());
|
|
const resolved = mcPlus1[index] || csvData.mc0 || '';
|
|
logSh(` 🔍 Variable ${cleanVar} → "${resolved}" (index ${index}, mcPlus1: ${mcPlus1.length} items)`, 'DEBUG');
|
|
return resolved;
|
|
}
|
|
|
|
// Variables avec index T+1_X
|
|
if (cleanVar.startsWith('T+1_')) {
|
|
const index = parseInt(cleanVar.split('_')[1]) - 1;
|
|
const tPlus1 = (csvData.tPlus1 || '').split(',').map(s => s.trim());
|
|
const resolved = tPlus1[index] || csvData.t0 || '';
|
|
logSh(` 🔍 Variable ${cleanVar} → "${resolved}" (index ${index}, tPlus1: ${tPlus1.length} items)`, 'DEBUG');
|
|
return resolved;
|
|
}
|
|
|
|
// Variables avec index L+1_X
|
|
if (cleanVar.startsWith('L+1_')) {
|
|
const index = parseInt(cleanVar.split('_')[1]) - 1;
|
|
const lPlus1 = (csvData.lPlus1 || '').split(',').map(s => s.trim());
|
|
const resolved = lPlus1[index] || '';
|
|
logSh(` 🔍 Variable ${cleanVar} → "${resolved}" (index ${index}, lPlus1: ${lPlus1.length} items)`, 'DEBUG');
|
|
return resolved;
|
|
}
|
|
|
|
// Variable inconnue
|
|
logSh(` ⚠️ Variable inconnue: "${cleanVar}" (match: "${match}")`, 'WARNING');
|
|
return csvData.mc0 || '';
|
|
});
|
|
};
|
|
|
|
// Nettoyer l'instruction des balises HTML et résoudre les variables
|
|
const originalInstruction = instruction;
|
|
|
|
// NE PLUS nettoyer le HTML ici - c'est fait dans ElementExtraction.js
|
|
instruction = instruction.trim();
|
|
|
|
logSh(` 📝 Instruction avant résolution (${tag}): ${instruction.substring(0, 100)}...`, 'DEBUG');
|
|
instruction = resolveVariables(instruction, csvData);
|
|
logSh(` ✅ Instruction après résolution (${tag}): ${instruction.substring(0, 100)}...`, 'DEBUG');
|
|
|
|
// Nettoyer les accolades mal formées restantes
|
|
instruction = instruction
|
|
.replace(/\{[^}]*/g, '') // Supprimer accolades non fermées
|
|
.replace(/[{}]/g, '') // Supprimer accolades isolées
|
|
.trim();
|
|
|
|
// Vérifier que l'instruction n'est pas vide ou invalide
|
|
if (!instruction || instruction.length < 10) {
|
|
logSh(`⚠️ Instruction trop courte ou vide pour ${tag}, skip`, 'WARNING');
|
|
continue;
|
|
}
|
|
|
|
const prompt = `Tu es un expert en rédaction SEO. Tu dois générer du contenu professionnel et naturel.
|
|
|
|
CONTEXTE:
|
|
- Mot-clé principal: ${csvData.mc0}
|
|
- Titre principal: ${csvData.t0}
|
|
- Personnalité: ${csvData.personality?.nom} (${csvData.personality?.style})
|
|
|
|
INSTRUCTION SPÉCIFIQUE:
|
|
${instruction}
|
|
|
|
CONSIGNES:
|
|
- Contenu naturel et engageant
|
|
- Intégration naturelle du mot-clé "${csvData.mc0}"
|
|
- Style ${csvData.personality?.style || 'professionnel'}
|
|
- Pas de formatage markdown
|
|
- Réponse directe sans préambule
|
|
|
|
RÉPONSE:`;
|
|
|
|
const response = await LLMManager.callLLM(llmProvider, prompt, {
|
|
temperature: 0.9,
|
|
maxTokens: 300,
|
|
timeout: 30000
|
|
}, csvData.personality);
|
|
|
|
if (response && response.trim()) {
|
|
result.content[tag] = cleanGeneratedContent(response.trim());
|
|
result.stats.processed++;
|
|
result.stats.enhanced++;
|
|
} else {
|
|
logSh(`⚠️ Réponse vide pour ${tag}`, 'WARNING');
|
|
result.content[tag] = `Contenu ${tag} généré automatiquement`;
|
|
}
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur génération ${tag}: ${error.message}`, 'ERROR');
|
|
result.content[tag] = `Contenu ${tag} - Erreur de génération`;
|
|
}
|
|
}
|
|
|
|
result.stats.duration = Date.now() - startTime;
|
|
|
|
logSh(`✅ Génération simple terminée: ${result.stats.enhanced}/${result.stats.processed} éléments (${result.stats.duration}ms)`, 'INFO');
|
|
|
|
return result;
|
|
|
|
} catch (error) {
|
|
result.stats.duration = Date.now() - startTime;
|
|
logSh(`❌ Échec génération simple: ${error.message}`, 'ERROR');
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* STATISTIQUES ET RAPPORTS
|
|
*/
|
|
|
|
/**
|
|
* Générer rapport amélioration
|
|
*/
|
|
function generateImprovementReport(originalContent, enhancedContent, layerType = 'general') {
|
|
const report = {
|
|
layerType,
|
|
timestamp: new Date().toISOString(),
|
|
summary: {
|
|
elementsProcessed: 0,
|
|
elementsImproved: 0,
|
|
averageImprovement: 0,
|
|
totalExecutionTime: 0
|
|
},
|
|
details: {
|
|
byElement: [],
|
|
qualityMetrics: {},
|
|
recommendations: []
|
|
}
|
|
};
|
|
|
|
// Analyser chaque élément
|
|
Object.keys(originalContent).forEach(tag => {
|
|
const original = originalContent[tag];
|
|
const enhanced = enhancedContent[tag];
|
|
|
|
if (original && enhanced) {
|
|
report.summary.elementsProcessed++;
|
|
|
|
const improvement = compareContentImprovement(original, enhanced, layerType);
|
|
|
|
if (improvement.improvementRate > 0) {
|
|
report.summary.elementsImproved++;
|
|
}
|
|
|
|
report.summary.averageImprovement += improvement.improvementRate;
|
|
|
|
report.details.byElement.push({
|
|
tag,
|
|
improvementRate: improvement.improvementRate,
|
|
lengthChange: improvement.details.lengthChange,
|
|
contentPreserved: improvement.details.contentPreserved
|
|
});
|
|
}
|
|
});
|
|
|
|
// Calculer moyennes
|
|
if (report.summary.elementsProcessed > 0) {
|
|
report.summary.averageImprovement = report.summary.averageImprovement / report.summary.elementsProcessed;
|
|
}
|
|
|
|
// Métriques qualité globales
|
|
const fullOriginal = Object.values(originalContent).join(' ');
|
|
const fullEnhanced = Object.values(enhancedContent).join(' ');
|
|
|
|
report.details.qualityMetrics = {
|
|
technical: analyzeTechnicalQuality(fullEnhanced),
|
|
transitions: analyzeTransitionFluidity(fullEnhanced),
|
|
style: analyzeStyleConsistency(fullEnhanced)
|
|
};
|
|
|
|
// Recommandations
|
|
if (report.summary.averageImprovement < 10) {
|
|
report.details.recommendations.push('Augmenter l\'intensité d\'amélioration');
|
|
}
|
|
|
|
if (report.details.byElement.some(e => !e.contentPreserved)) {
|
|
report.details.recommendations.push('Améliorer préservation du contenu original');
|
|
}
|
|
|
|
return report;
|
|
}
|
|
|
|
module.exports = {
|
|
// Analyseurs
|
|
analyzeTechnicalQuality,
|
|
analyzeTransitionFluidity,
|
|
analyzeStyleConsistency,
|
|
|
|
// Comparateurs
|
|
compareContentImprovement,
|
|
|
|
// Utilitaires contenu
|
|
cleanGeneratedContent,
|
|
validateSelectiveContent,
|
|
|
|
// Utilitaires techniques
|
|
chunkArray,
|
|
sleep,
|
|
measurePerformance,
|
|
formatDuration,
|
|
|
|
// Génération simple (remplace ContentGeneration.js)
|
|
generateSimple,
|
|
|
|
// Rapports
|
|
generateImprovementReport
|
|
}; |