## SelectiveSmartTouch (NEW) - Architecture révolutionnaire: Analyse intelligente → Améliorations ciblées précises - 5 modules: SmartAnalysisLayer, SmartTechnicalLayer, SmartStyleLayer, SmartReadabilityLayer, SmartTouchCore - Système 10% segments: amélioration uniquement des segments les plus faibles (intensity-based) - Détection contexte globale pour prompts adaptatifs multi-secteurs - Intégration complète dans PipelineExecutor et PipelineDefinition ## Pipeline Validator Spec (NEW) - Spécification complète système validation qualité par LLM - 5 critères universels: Qualité, Verbosité, SEO, Répétitions, Naturalité - Échantillonnage intelligent par filtrage balises (pas XML) - Évaluation multi-versions avec justifications détaillées - Coût estimé: ~$1/validation (260 appels LLM) ## Optimizations - Réduction intensités fullEnhancement (technical 1.0→0.7, style 0.8→0.5) - Ajout gardes-fous anti-familiarité excessive dans StyleLayer - Sauvegarde étapes intermédiaires activée par défaut (pipeline-runner) ## Fixes - Fix typo critique SmartTouchCore.js:110 (determineLayers ToApply → determineLayersToApply) - Prompts généralisés multi-secteurs (e-commerce, SaaS, services, informatif) 🚀 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
480 lines
16 KiB
JavaScript
480 lines
16 KiB
JavaScript
// ========================================
|
|
// SMART ANALYSIS LAYER - Analyse intelligente avant amélioration
|
|
// Responsabilité: Analyser contenu et identifier améliorations précises nécessaires
|
|
// LLM: GPT-4o-mini (objectivité, température basse)
|
|
// Architecture: Phase 1 de SelectiveSmartTouch (Analyse → Amélioration ciblée)
|
|
// ========================================
|
|
|
|
const { callLLM } = require('../LLMManager');
|
|
const { logSh } = require('../ErrorReporting');
|
|
const { tracer } = require('../trace');
|
|
|
|
/**
|
|
* SMART ANALYSIS LAYER
|
|
* Analyse objective du contenu pour identifier améliorations précises
|
|
*/
|
|
class SmartAnalysisLayer {
|
|
constructor() {
|
|
this.name = 'SmartAnalysis';
|
|
this.defaultLLM = 'gpt-4o-mini';
|
|
}
|
|
|
|
/**
|
|
* ANALYSE COMPLÈTE D'UN ÉLÉMENT
|
|
* @param {string} content - Contenu à analyser
|
|
* @param {object} context - Contexte (mc0, personality, llmProvider, etc.)
|
|
* @returns {object} - Analyse JSON structurée
|
|
*/
|
|
async analyzeElement(content, context = {}) {
|
|
return await tracer.run('SmartAnalysis.analyzeElement()', async () => {
|
|
const { mc0, personality, llmProvider } = context;
|
|
|
|
// ✅ Utiliser LLM fourni dans context, sinon fallback sur defaultLLM
|
|
const llmToUse = llmProvider || this.defaultLLM;
|
|
|
|
await tracer.annotate({
|
|
smartAnalysis: true,
|
|
contentLength: content.length,
|
|
hasMc0: !!mc0,
|
|
hasPersonality: !!personality,
|
|
llmProvider: llmToUse
|
|
});
|
|
|
|
const startTime = Date.now();
|
|
logSh(`🔍 SMART ANALYSIS: Analyse d'un élément (${content.length} chars) avec ${llmToUse}`, 'DEBUG');
|
|
|
|
try {
|
|
const prompt = this.createAnalysisPrompt(content, context);
|
|
|
|
const response = await callLLM(llmToUse, prompt, {
|
|
temperature: 0.2, // Basse température = objectivité
|
|
maxTokens: 1500
|
|
});
|
|
|
|
// Parser JSON de l'analyse
|
|
const analysis = this.parseAnalysisResponse(response);
|
|
|
|
const duration = Date.now() - startTime;
|
|
logSh(`✅ Analyse terminée: ${analysis.improvements.length} améliorations identifiées (${duration}ms)`, 'DEBUG');
|
|
|
|
await tracer.event('Smart Analysis terminée', {
|
|
duration,
|
|
improvementsCount: analysis.improvements.length,
|
|
needsImprovement: analysis.overallScore < 0.7
|
|
});
|
|
|
|
return analysis;
|
|
|
|
} catch (error) {
|
|
const duration = Date.now() - startTime;
|
|
logSh(`❌ SMART ANALYSIS ÉCHOUÉE (${duration}ms): ${error.message}`, 'ERROR');
|
|
|
|
// Fallback: analyse basique algorithmique
|
|
return this.fallbackAnalysis(content, context);
|
|
}
|
|
}, { contentLength: content.length, context });
|
|
}
|
|
|
|
/**
|
|
* ANALYSE BATCH DE PLUSIEURS ÉLÉMENTS
|
|
*/
|
|
async analyzeBatch(contentMap, context = {}) {
|
|
return await tracer.run('SmartAnalysis.analyzeBatch()', async () => {
|
|
const startTime = Date.now();
|
|
const results = {};
|
|
|
|
logSh(`🔍 SMART ANALYSIS BATCH: ${Object.keys(contentMap).length} éléments`, 'INFO');
|
|
|
|
for (const [tag, content] of Object.entries(contentMap)) {
|
|
try {
|
|
results[tag] = await this.analyzeElement(content, context);
|
|
logSh(` ✅ [${tag}]: ${results[tag].improvements.length} améliorations`, 'DEBUG');
|
|
} catch (error) {
|
|
logSh(` ❌ [${tag}]: Analyse échouée - ${error.message}`, 'ERROR');
|
|
results[tag] = this.fallbackAnalysis(content, context);
|
|
}
|
|
|
|
// Petit délai pour éviter rate limiting
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
logSh(`✅ SMART ANALYSIS BATCH terminé: ${Object.keys(results).length} éléments analysés (${duration}ms)`, 'INFO');
|
|
|
|
return results;
|
|
}, { elementsCount: Object.keys(contentMap).length });
|
|
}
|
|
|
|
/**
|
|
* DÉTECTION CONTEXTUELLE (B2C vs B2B, niveau technique)
|
|
* Permet d'adapter les améliorations au public cible
|
|
*/
|
|
detectContentContext(content, personality = null) {
|
|
logSh('🔍 Détection contexte contenu...', 'DEBUG');
|
|
|
|
const context = {
|
|
audience: 'unknown', // B2C, B2B, mixed
|
|
techLevel: 'medium', // low, medium, high, too_high
|
|
contentType: 'unknown', // ecommerce, service, informative, technical
|
|
sector: 'general'
|
|
};
|
|
|
|
// === DÉTECTION AUDIENCE ===
|
|
const b2cSignals = [
|
|
'acheter', 'votre maison', 'chez vous', 'personnalisé', 'facile',
|
|
'simple', 'idéal pour vous', 'particulier', 'famille', 'clients'
|
|
];
|
|
const b2bSignals = [
|
|
'entreprise', 'professionnel', 'solution industrielle',
|
|
'cahier des charges', 'conformité', 'optimisation des processus'
|
|
];
|
|
|
|
const b2cCount = b2cSignals.filter(s => content.toLowerCase().includes(s)).length;
|
|
const b2bCount = b2bSignals.filter(s => content.toLowerCase().includes(s)).length;
|
|
|
|
if (b2cCount > b2bCount && b2cCount > 2) context.audience = 'B2C';
|
|
else if (b2bCount > b2cCount && b2bCount > 2) context.audience = 'B2B';
|
|
else if (b2cCount > 0 && b2bCount > 0) context.audience = 'mixed';
|
|
|
|
// === DÉTECTION NIVEAU TECHNIQUE ===
|
|
const jargonWords = [
|
|
'norme', 'coefficient', 'résistance', 'ISO', 'ASTM', 'certifié',
|
|
'EN ', 'DIN', 'conforme', 'spécification', 'standard', 'référence'
|
|
];
|
|
const technicalSpecs = (content.match(/\d+\s*(mm|cm|kg|°C|%|watt|lumen|J\/cm²|K⁻¹)/g) || []).length;
|
|
const jargonCount = jargonWords.filter(w => content.includes(w)).length;
|
|
|
|
if (jargonCount > 5 || technicalSpecs > 8) {
|
|
context.techLevel = 'too_high';
|
|
} else if (jargonCount > 2 || technicalSpecs > 4) {
|
|
context.techLevel = 'high';
|
|
} else if (jargonCount === 0 && technicalSpecs < 2) {
|
|
context.techLevel = 'low';
|
|
}
|
|
|
|
// === DÉTECTION TYPE CONTENU ===
|
|
const ecommerceKeywords = ['prix', 'acheter', 'commander', 'livraison', 'stock', 'produit'];
|
|
const serviceKeywords = ['prestation', 'accompagnement', 'conseil', 'expertise', 'service'];
|
|
|
|
if (ecommerceKeywords.filter(k => content.toLowerCase().includes(k)).length > 2) {
|
|
context.contentType = 'ecommerce';
|
|
} else if (serviceKeywords.filter(k => content.toLowerCase().includes(k)).length > 2) {
|
|
context.contentType = 'service';
|
|
} else if (jargonCount > 3) {
|
|
context.contentType = 'technical';
|
|
} else {
|
|
context.contentType = 'informative';
|
|
}
|
|
|
|
// === INTÉGRATION DONNÉES PERSONALITY ===
|
|
if (personality) {
|
|
if (personality.targetAudience?.toLowerCase().includes('grand public')) {
|
|
context.audience = 'B2C';
|
|
}
|
|
if (personality.secteur) {
|
|
context.sector = personality.secteur;
|
|
}
|
|
if (personality.tone?.includes('accessible') || personality.tone?.includes('simple')) {
|
|
context.preferSimple = true;
|
|
}
|
|
}
|
|
|
|
logSh(`✅ Contexte détecté: audience=${context.audience}, techLevel=${context.techLevel}, type=${context.contentType}`, 'DEBUG');
|
|
|
|
return context;
|
|
}
|
|
|
|
/**
|
|
* ANALYSE PAR SEGMENTS (découpe + score individuel)
|
|
* Permet de sélectionner seulement les segments les plus faibles
|
|
*/
|
|
analyzeBySegments(content, context = {}) {
|
|
logSh('🔍 Analyse par segments...', 'DEBUG');
|
|
|
|
// Découper en phrases (simpliste mais efficace)
|
|
const sentences = content.split(/(?<=[.!?])\s+/).filter(s => s.trim().length > 10);
|
|
|
|
const segments = sentences.map((sentence, index) => {
|
|
// Score algorithmique rapide de chaque phrase
|
|
const wordCount = sentence.split(/\s+/).length;
|
|
const hasNumbers = /\d+/.test(sentence);
|
|
const genericWords = ['nos solutions', 'notre expertise', 'qualité', 'service'].filter(w => sentence.toLowerCase().includes(w)).length;
|
|
const jargonWords = ['norme', 'coefficient', 'ISO', 'certifié'].filter(w => sentence.includes(w)).length;
|
|
|
|
let score = 0.5; // Score de base
|
|
|
|
// Pénalités
|
|
if (wordCount > 30) score -= 0.2; // Trop longue
|
|
if (genericWords > 0) score -= 0.15 * genericWords; // Vocabulaire générique
|
|
if (jargonWords > 1) score -= 0.1 * jargonWords; // Trop de jargon
|
|
|
|
// Bonus
|
|
if (hasNumbers && wordCount < 20) score += 0.1; // Concise avec données
|
|
|
|
score = Math.max(0.0, Math.min(1.0, score)); // Clamp entre 0 et 1
|
|
|
|
return {
|
|
index,
|
|
content: sentence,
|
|
score,
|
|
wordCount,
|
|
issues: [
|
|
wordCount > 30 ? 'trop_longue' : null,
|
|
genericWords > 0 ? 'vocabulaire_générique' : null,
|
|
jargonWords > 1 ? 'trop_technique' : null
|
|
].filter(Boolean)
|
|
};
|
|
});
|
|
|
|
logSh(`✅ ${segments.length} segments analysés`, 'DEBUG');
|
|
|
|
return segments;
|
|
}
|
|
|
|
/**
|
|
* SÉLECTION DES X% SEGMENTS LES PLUS FAIBLES
|
|
* Retourne indices des segments à améliorer
|
|
*/
|
|
selectWeakestSegments(segments, percentage = 0.1) {
|
|
// Trier par score croissant (plus faibles d'abord)
|
|
const sortedSegments = [...segments].sort((a, b) => a.score - b.score);
|
|
|
|
// Calculer combien de segments à prendre
|
|
const countToSelect = Math.max(1, Math.ceil(segments.length * percentage));
|
|
|
|
// Prendre les N segments les plus faibles
|
|
const selectedSegments = sortedSegments.slice(0, countToSelect);
|
|
|
|
logSh(`📊 Sélection: ${selectedSegments.length}/${segments.length} segments les plus faibles (${(percentage * 100).toFixed(0)}%)`, 'INFO');
|
|
|
|
// Retourner dans l'ordre original (par index)
|
|
return selectedSegments.sort((a, b) => a.index - b.index);
|
|
}
|
|
|
|
/**
|
|
* CRÉER PROMPT D'ANALYSE (générique, multi-secteur)
|
|
*/
|
|
createAnalysisPrompt(content, context) {
|
|
const { mc0, personality } = context;
|
|
|
|
return `MISSION: Analyse OBJECTIVE de ce contenu et identifie les améliorations précises nécessaires.
|
|
|
|
CONTENU À ANALYSER:
|
|
"${content}"
|
|
|
|
${mc0 ? `CONTEXTE SUJET: ${mc0}` : 'CONTEXTE: Générique'}
|
|
${personality ? `PERSONNALITÉ CIBLE: ${personality.nom} (${personality.style})` : ''}
|
|
|
|
ANALYSE CES DIMENSIONS:
|
|
|
|
1. DIMENSION TECHNIQUE:
|
|
- Manque-t-il des informations factuelles concrètes ?
|
|
- Le contenu est-il trop générique ou vague ?
|
|
- Y a-t-il besoin de données chiffrées, dimensions, spécifications ?
|
|
|
|
2. DIMENSION STYLE:
|
|
- Le ton est-il cohérent ?
|
|
${personality ? `- Le style correspond-il à "${personality.style}" ?` : '- Le style est-il professionnel ?'}
|
|
- Y a-t-il des expressions trop génériques ("nos solutions", "notre expertise") ?
|
|
|
|
3. DIMENSION LISIBILITÉ:
|
|
- Les phrases sont-elles trop longues ou complexes ?
|
|
- Les connecteurs sont-ils répétitifs ?
|
|
- La structure est-elle fluide ?
|
|
|
|
4. DIMENSION VOCABULAIRE:
|
|
- Mots génériques à remplacer par termes spécifiques ?
|
|
- Vocabulaire adapté au sujet ?
|
|
|
|
IMPORTANT: Sois OBJECTIF et SÉLECTIF. Ne liste QUE les améliorations réellement nécessaires.
|
|
Si le contenu est déjà bon sur une dimension, indique "needed: false" pour cette dimension.
|
|
|
|
RETOURNE UN JSON (et UNIQUEMENT du JSON valide):
|
|
{
|
|
"technical": {
|
|
"needed": true/false,
|
|
"score": 0.0-1.0,
|
|
"missing": ["élément précis manquant 1", "élément précis manquant 2"],
|
|
"issues": ["problème identifié"]
|
|
},
|
|
"style": {
|
|
"needed": true/false,
|
|
"score": 0.0-1.0,
|
|
"toneIssues": ["problème de ton"],
|
|
"genericPhrases": ["expression générique à personnaliser"]
|
|
},
|
|
"readability": {
|
|
"needed": true/false,
|
|
"score": 0.0-1.0,
|
|
"complexSentences": [numéro_ligne],
|
|
"repetitiveConnectors": ["connecteur répété"]
|
|
},
|
|
"vocabulary": {
|
|
"needed": true/false,
|
|
"score": 0.0-1.0,
|
|
"genericWords": ["mot générique"],
|
|
"suggestions": ["terme spécifique suggéré"]
|
|
},
|
|
"improvements": [
|
|
"Amélioration précise 1",
|
|
"Amélioration précise 2"
|
|
],
|
|
"overallScore": 0.0-1.0
|
|
}`;
|
|
}
|
|
|
|
/**
|
|
* PARSER RÉPONSE JSON DE L'ANALYSE
|
|
*/
|
|
parseAnalysisResponse(response) {
|
|
try {
|
|
// Nettoyer la réponse (supprimer markdown, etc.)
|
|
let cleanResponse = response.trim();
|
|
|
|
// Supprimer balises markdown JSON
|
|
cleanResponse = cleanResponse.replace(/```json\s*/gi, '');
|
|
cleanResponse = cleanResponse.replace(/```\s*/g, '');
|
|
|
|
// Parser JSON
|
|
const analysis = JSON.parse(cleanResponse);
|
|
|
|
// Valider structure
|
|
if (!analysis.technical || !analysis.style || !analysis.readability || !analysis.vocabulary) {
|
|
throw new Error('Structure JSON incomplète');
|
|
}
|
|
|
|
// Valider que improvements est un array
|
|
if (!Array.isArray(analysis.improvements)) {
|
|
analysis.improvements = [];
|
|
}
|
|
|
|
return analysis;
|
|
|
|
} catch (error) {
|
|
logSh(`⚠️ Parsing JSON échoué: ${error.message}, tentative de récupération...`, 'WARNING');
|
|
|
|
// Tentative d'extraction partielle
|
|
try {
|
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
if (jsonMatch) {
|
|
return JSON.parse(jsonMatch[0]);
|
|
}
|
|
} catch (e) {
|
|
// Ignore
|
|
}
|
|
|
|
throw new Error(`Impossible de parser l'analyse JSON: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ANALYSE FALLBACK ALGORITHMIQUE (si LLM échoue)
|
|
*/
|
|
fallbackAnalysis(content, context) {
|
|
logSh(`🔄 Fallback: Analyse algorithmique de base`, 'DEBUG');
|
|
|
|
const wordCount = content.split(/\s+/).length;
|
|
const sentenceCount = content.split(/[.!?]+/).filter(s => s.trim().length > 5).length;
|
|
const avgSentenceLength = wordCount / Math.max(sentenceCount, 1);
|
|
|
|
// Analyse basique
|
|
const genericWords = ['nos solutions', 'notre expertise', 'qualité', 'service', 'professionnel'];
|
|
const foundGeneric = genericWords.filter(word => content.toLowerCase().includes(word));
|
|
|
|
const improvements = [];
|
|
let technicalScore = 0.5;
|
|
let styleScore = 0.5;
|
|
let readabilityScore = 0.5;
|
|
|
|
// Détection phrases trop longues
|
|
if (avgSentenceLength > 25) {
|
|
improvements.push('Raccourcir les phrases trop longues (> 25 mots)');
|
|
readabilityScore = 0.4;
|
|
}
|
|
|
|
// Détection vocabulaire générique
|
|
if (foundGeneric.length > 2) {
|
|
improvements.push(`Remplacer expressions génériques: ${foundGeneric.join(', ')}`);
|
|
styleScore = 0.4;
|
|
}
|
|
|
|
// Détection manque de données concrètes
|
|
const hasNumbers = /\d+/.test(content);
|
|
if (!hasNumbers && wordCount > 50) {
|
|
improvements.push('Ajouter données concrètes (chiffres, dimensions, pourcentages)');
|
|
technicalScore = 0.4;
|
|
}
|
|
|
|
return {
|
|
technical: {
|
|
needed: technicalScore < 0.6,
|
|
score: technicalScore,
|
|
missing: hasNumbers ? [] : ['données chiffrées'],
|
|
issues: []
|
|
},
|
|
style: {
|
|
needed: styleScore < 0.6,
|
|
score: styleScore,
|
|
toneIssues: [],
|
|
genericPhrases: foundGeneric
|
|
},
|
|
readability: {
|
|
needed: readabilityScore < 0.6,
|
|
score: readabilityScore,
|
|
complexSentences: avgSentenceLength > 25 ? [1] : [],
|
|
repetitiveConnectors: []
|
|
},
|
|
vocabulary: {
|
|
needed: foundGeneric.length > 0,
|
|
score: foundGeneric.length > 2 ? 0.3 : 0.6,
|
|
genericWords: foundGeneric,
|
|
suggestions: []
|
|
},
|
|
improvements,
|
|
overallScore: (technicalScore + styleScore + readabilityScore) / 3,
|
|
fallbackUsed: true
|
|
};
|
|
}
|
|
|
|
/**
|
|
* RÉSUMER ANALYSES BATCH
|
|
*/
|
|
summarizeBatchAnalysis(analysisResults) {
|
|
const summary = {
|
|
totalElements: Object.keys(analysisResults).length,
|
|
needsImprovement: 0,
|
|
averageScore: 0,
|
|
commonIssues: {
|
|
technical: 0,
|
|
style: 0,
|
|
readability: 0,
|
|
vocabulary: 0
|
|
},
|
|
totalImprovements: 0
|
|
};
|
|
|
|
let totalScore = 0;
|
|
|
|
Object.values(analysisResults).forEach(analysis => {
|
|
totalScore += analysis.overallScore;
|
|
|
|
if (analysis.overallScore < 0.7) {
|
|
summary.needsImprovement++;
|
|
}
|
|
|
|
if (analysis.technical.needed) summary.commonIssues.technical++;
|
|
if (analysis.style.needed) summary.commonIssues.style++;
|
|
if (analysis.readability.needed) summary.commonIssues.readability++;
|
|
if (analysis.vocabulary.needed) summary.commonIssues.vocabulary++;
|
|
|
|
summary.totalImprovements += analysis.improvements.length;
|
|
});
|
|
|
|
summary.averageScore = totalScore / summary.totalElements;
|
|
|
|
return summary;
|
|
}
|
|
}
|
|
|
|
module.exports = { SmartAnalysisLayer };
|