seo-generator-server/lib/selective-smart-touch/SmartAnalysisLayer.js
StillHammer 0244521f5c feat(selective-smart-touch): Add intelligent analysis-driven enhancement system + validation spec
## 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>
2025-10-13 15:01:02 +08:00

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 };