seo-generator-server/lib/selective-smart-touch/SmartTouchCore.js
StillHammer 9a2ef7da2b feat(human-simulation): Système d'erreurs graduées procédurales + anti-répétition complet
## 🎯 Nouveau système d'erreurs graduées (architecture SmartTouch)

### Architecture procédurale intelligente :
- **3 niveaux de gravité** : Légère (50%) → Moyenne (30%) → Grave (10%)
- **14 types d'erreurs** réalistes et subtiles
- **Sélection procédurale** selon contexte (longueur, technique, heure)
- **Distribution contrôlée** : max 1 grave, 2 moyennes, 3 légères par article

### 1. Erreurs GRAVES (10% articles max) :
- Accord sujet-verbe : "ils sont" → "ils est"
- Mot manquant : "pour garantir la qualité" → "pour garantir qualité"
- Double mot : "pour garantir" → "pour pour garantir"
- Négation oubliée : "n'est pas" → "est pas"

### 2. Erreurs MOYENNES (30% articles) :
- Accord pluriel : "plaques résistantes" → "plaques résistant"
- Virgule manquante : "Ainsi, il" → "Ainsi il"
- Registre inapproprié : "Par conséquent" → "Du coup"
- Préposition incorrecte : "résistant aux" → "résistant des"
- Connecteur illogique : "cependant" → "donc"

### 3. Erreurs LÉGÈRES (50% articles) :
- Double espace : "de votre" → "de  votre"
- Trait d'union : "c'est-à-dire" → "c'est à dire"
- Espace ponctuation : "qualité ?" → "qualité?"
- Majuscule : "Toutenplaque" → "toutenplaque"
- Apostrophe droite : "l'article" → "l'article"

##  Système anti-répétition complet :

### Corrections critiques :
- **HumanSimulationTracker.js** : Tracker centralisé global
- **Word boundaries (\b)** sur TOUS les regex → FIX "maison" → "néanmoinson"
- **Protection 30+ expressions idiomatiques** françaises
- **Anti-répétition** : max 2× même mot, jamais 2× même développement
- **Diversification** : 48 variantes (hésitations, développements, connecteurs)

### Nouvelle structure (comme SmartTouch) :
```
lib/human-simulation/
├── error-profiles/                (NOUVEAU)
│   ├── ErrorProfiles.js          (définitions + probabilités)
│   ├── ErrorGrave.js             (10% articles)
│   ├── ErrorMoyenne.js           (30% articles)
│   ├── ErrorLegere.js            (50% articles)
│   └── ErrorSelector.js          (sélection procédurale)
├── HumanSimulationCore.js         (orchestrateur)
├── HumanSimulationTracker.js      (anti-répétition)
└── [autres modules]
```

## 🔄 Remplace ancien système :
-  SpellingErrors.js (basique, répétitif, "et" → "." × 8)
-  error-profiles/ (gradué, procédural, intelligent, diversifié)

## 🎲 Fonctionnalités procédurales :
- Analyse contexte : longueur texte, complexité technique, heure rédaction
- Multiplicateurs adaptatifs selon contexte
- Conditions application intelligentes
- Tracking global par batch (respecte limites 10%/30%/50%)

## 📊 Résultats validation :
Sur 100 articles → ~40-50 avec erreurs subtiles et diverses (plus de spam répétitif)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 01:06:28 +08:00

440 lines
16 KiB
JavaScript

// ========================================
// SMART TOUCH CORE - Orchestrateur SelectiveSmartTouch
// Responsabilité: Orchestration complète Analyse → Améliorations ciblées
// Architecture: Analyse intelligente PUIS améliorations précises (contrôle total)
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { SmartAnalysisLayer } = require('./SmartAnalysisLayer');
const { SmartTechnicalLayer } = require('./SmartTechnicalLayer');
const { SmartStyleLayer } = require('./SmartStyleLayer');
const { SmartReadabilityLayer } = require('./SmartReadabilityLayer');
const { GlobalBudgetManager } = require('./GlobalBudgetManager'); // ✅ NOUVEAU: Budget global
/**
* SMART TOUCH CORE
* Orchestrateur principal: Analyse → Technical → Style → Readability (ciblé)
*/
class SmartTouchCore {
constructor() {
this.name = 'SelectiveSmartTouch';
// Instancier les layers
this.analysisLayer = new SmartAnalysisLayer();
this.technicalLayer = new SmartTechnicalLayer();
this.styleLayer = new SmartStyleLayer();
this.readabilityLayer = new SmartReadabilityLayer();
}
/**
* APPLIQUER SMART TOUCH COMPLET
* @param {object} content - Map {tag: texte}
* @param {object} config - Configuration
* @returns {object} - Résultat avec stats détaillées
*/
async apply(content, config = {}) {
return await tracer.run('SmartTouchCore.apply()', async () => {
const {
mode = 'full', // 'analysis_only', 'technical_only', 'style_only', 'readability_only', 'full'
intensity = 1.0,
csvData = null,
llmProvider = 'gpt-4o-mini', // ✅ LLM à utiliser (extrait du pipeline config)
skipAnalysis = false, // Si true, applique sans analyser (mode legacy)
layersOrder = ['technical', 'style', 'readability'], // Ordre d'application personnalisable
charsPerExpression = 4000 // ✅ NOUVEAU: Caractères par expression familière (configurable)
} = config;
await tracer.annotate({
selectiveSmartTouch: true,
mode,
intensity,
elementsCount: Object.keys(content).length,
personality: csvData?.personality?.nom
});
const startTime = Date.now();
logSh(`🧠 SELECTIVE SMART TOUCH START: ${Object.keys(content).length} éléments | Mode: ${mode}`, 'INFO');
try {
let currentContent = { ...content };
const stats = {
mode,
analysisResults: {},
layersApplied: [],
totalModifications: 0,
elementsProcessed: Object.keys(content).length,
elementsImproved: 0,
duration: 0
};
// === ✅ FIX: INITIALISER BUDGET GLOBAL AVANT TOUT (scope global) ===
const budgetConfig = {
charsPerExpression: config.charsPerExpression || 4000 // Configurable via pipeline
};
const budgetManager = new GlobalBudgetManager(csvData?.personality, currentContent, budgetConfig);
const allTags = Object.keys(currentContent);
budgetManager.distributeRandomly(allTags);
logSh(`💰 Budget global initialisé: ${JSON.stringify(budgetManager.budget)}`, 'INFO');
logSh(`🎲 Budget distribué sur ${allTags.length} tags`, 'INFO');
// ========================================
// PHASE 1: ANALYSE INTELLIGENTE
// ========================================
if (!skipAnalysis) {
logSh(`\n📊 === PHASE 1: ANALYSE INTELLIGENTE ===`, 'INFO');
const analysisResults = await this.analysisLayer.analyzeBatch(currentContent, {
mc0: csvData?.mc0,
personality: csvData?.personality,
llmProvider // ✅ Passer LLM à l'analyse batch
});
stats.analysisResults = analysisResults;
// Résumer analyse
const summary = this.analysisLayer.summarizeBatchAnalysis(analysisResults);
logSh(` 📋 Résumé analyse: ${summary.needsImprovement}/${summary.totalElements} éléments nécessitent amélioration`, 'INFO');
logSh(` 📊 Score moyen: ${summary.averageScore.toFixed(2)} | Améliorations totales: ${summary.totalImprovements}`, 'INFO');
logSh(` 🎯 Besoins: Technical=${summary.commonIssues.technical} | Style=${summary.commonIssues.style} | Readability=${summary.commonIssues.readability}`, 'INFO');
// Si mode analysis_only, retourner ici
if (mode === 'analysis_only') {
const duration = Date.now() - startTime;
logSh(`✅ SELECTIVE SMART TOUCH (ANALYSIS ONLY) terminé: ${duration}ms`, 'INFO');
return {
content: currentContent,
stats: {
...stats,
duration,
analysisOnly: true
},
modifications: 0,
analysisResults
};
}
// ========================================
// PHASE 2: AMÉLIORATIONS CIBLÉES
// ========================================
logSh(`\n🔧 === PHASE 2: AMÉLIORATIONS CIBLÉES ===`, 'INFO');
// Déterminer quelles couches appliquer
const layersToApply = this.determineLayersToApply(mode, layersOrder);
// === DÉTECTION CONTEXTE GLOBALE (1 seule fois) ===
const contentContext = this.analysisLayer.detectContentContext(
Object.values(currentContent).join(' '),
csvData?.personality
);
// === ✅ FIX: SÉLECTIONNER 10% SEGMENTS UNE SEULE FOIS (GLOBAL) ===
logSh(`\n🎯 === SÉLECTION 10% SEGMENTS GLOBAUX ===`, 'INFO');
const percentageToImprove = intensity * 0.1;
const segmentsByTag = {};
// Pré-calculer segments et sélection pour chaque tag UNE SEULE FOIS
for (const [tag, text] of Object.entries(currentContent)) {
const analysis = analysisResults[tag];
if (!analysis) continue;
// Analyser par segments
const segments = this.analysisLayer.analyzeBySegments(text, {
mc0: csvData?.mc0,
personality: csvData?.personality
});
// Sélectionner les X% segments les plus faibles
const weakestSegments = this.analysisLayer.selectWeakestSegments(
segments,
percentageToImprove
);
segmentsByTag[tag] = {
segments,
weakestSegments,
analysis
};
logSh(` 📊 [${tag}] ${segments.length} segments, ${weakestSegments.length} sélectionnés (${(percentageToImprove * 100).toFixed(0)}%)`, 'INFO');
}
// === APPLIQUER TOUTES LES LAYERS SUR LES MÊMES SEGMENTS SÉLECTIONNÉS ===
const improvedTags = new Set(); // ✅ FIX: Tracker les tags améliorés (pas de duplicata)
for (const layerName of layersToApply) {
const layerStartTime = Date.now();
logSh(`\n 🎯 Couche: ${layerName}`, 'INFO');
let layerModifications = 0;
const layerResults = {};
// Appliquer la couche sur chaque élément (en réutilisant les MÊMES segments sélectionnés)
for (const [tag, text] of Object.entries(currentContent)) {
const tagData = segmentsByTag[tag];
if (!tagData) continue;
try {
// ✅ Utiliser les segments PRÉ-SÉLECTIONNÉS (pas de nouvelle sélection)
const { segments, weakestSegments, analysis } = tagData;
// Appliquer amélioration UNIQUEMENT sur segments déjà sélectionnés
const result = await this.applyLayerToSegments(
layerName,
segments,
weakestSegments,
analysis,
{
mc0: csvData?.mc0,
personality: csvData?.personality,
intensity,
contentContext, // Passer contexte aux layers
llmProvider, // ✅ Passer LLM choisi dans pipeline
budgetManager, // ✅ NOUVEAU: Passer budget manager
currentTag: tag // ✅ NOUVEAU: Tag actuel pour tracking budget
}
);
if (!result.skipped && result.content !== text) {
currentContent[tag] = result.content;
layerModifications += result.modifications || 0;
improvedTags.add(tag); // ✅ FIX: Ajouter au Set (pas de duplicata)
}
layerResults[tag] = result;
} catch (error) {
logSh(` ❌ [${tag}] Échec ${layerName}: ${error.message}`, 'ERROR');
}
}
const layerDuration = Date.now() - layerStartTime;
stats.layersApplied.push({
name: layerName,
modifications: layerModifications,
duration: layerDuration
});
stats.totalModifications += layerModifications;
logSh(`${layerName} terminé: ${layerModifications} modifications (${layerDuration}ms)`, 'INFO');
}
// ✅ FIX: Mettre à jour le compteur d'éléments améliorés APRÈS toutes les layers
stats.elementsImproved = improvedTags.size;
} else {
// Mode skipAnalysis: appliquer sans analyse (legacy fallback)
logSh(`⚠️ Mode skipAnalysis activé: application directe sans analyse préalable`, 'WARNING');
// TODO: Implémenter mode legacy si nécessaire
logSh(`❌ Mode skipAnalysis non implémenté pour SmartTouch (requiert analyse)`, 'ERROR');
}
// ========================================
// RÉSULTATS FINAUX
// ========================================
const duration = Date.now() - startTime;
stats.duration = duration;
// === ✅ NOUVEAU: Rapport budget final ===
const budgetReport = budgetManager?.getReport();
if (budgetReport) {
stats.budgetReport = budgetReport;
logSh(`\n💰 === RAPPORT BUDGET EXPRESSIONS ===`, 'INFO');
logSh(` 📊 Budget total: ${budgetReport.budget.total}`, 'INFO');
logSh(` ✅ Consommé: ${budgetReport.totalConsumed} (${budgetReport.percentageUsed}%)`, 'INFO');
logSh(` 💸 Restant: ${budgetReport.remaining}`, 'INFO');
if (budgetReport.overBudget) {
logSh(` ⚠️ DÉPASSEMENT BUDGET !`, 'WARN');
}
}
logSh(`\n✅ === SELECTIVE SMART TOUCH TERMINÉ ===`, 'INFO');
logSh(` 📊 ${stats.elementsImproved}/${stats.elementsProcessed} éléments améliorés`, 'INFO');
logSh(` 🔄 ${stats.totalModifications} modifications totales`, 'INFO');
logSh(` ⏱️ Durée: ${duration}ms`, 'INFO');
logSh(` 🎯 Couches appliquées: ${stats.layersApplied.map(l => l.name).join(' → ')}`, 'INFO');
await tracer.event('SelectiveSmartTouch terminé', stats);
return {
content: currentContent,
stats,
modifications: stats.totalModifications,
analysisResults: stats.analysisResults,
budgetReport // ✅ NOUVEAU: Inclure rapport budget
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ SELECTIVE SMART TOUCH ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
return {
content, // Fallback: contenu original
stats: {
error: error.message,
duration,
fallback: true
},
modifications: 0,
fallback: true
};
}
}, { content: Object.keys(content), config });
}
/**
* DÉTERMINER COUCHES À APPLIQUER
*/
determineLayersToApply(mode, layersOrder) {
switch (mode) {
case 'technical_only':
return ['technical'];
case 'style_only':
return ['style'];
case 'readability_only':
return ['readability'];
case 'full':
default:
return layersOrder; // Ordre personnalisable
}
}
/**
* APPLIQUER UNE COUCHE SPÉCIFIQUE
*/
async applyLayer(layerName, content, analysis, context) {
switch (layerName) {
case 'technical':
return await this.technicalLayer.applyTargeted(content, analysis, context);
case 'style':
return await this.styleLayer.applyTargeted(content, analysis, context);
case 'readability':
return await this.readabilityLayer.applyTargeted(content, analysis, context);
default:
throw new Error(`Couche inconnue: ${layerName}`);
}
}
/**
* APPLIQUER COUCHE SUR SEGMENTS SÉLECTIONNÉS UNIQUEMENT (10% système)
* @param {string} layerName - Nom de la couche
* @param {array} allSegments - Tous les segments du texte
* @param {array} weakestSegments - Segments sélectionnés à améliorer
* @param {object} analysis - Analyse globale
* @param {object} context - Contexte
* @returns {object} - { content: texte réassemblé, modifications, ... }
*/
async applyLayerToSegments(layerName, allSegments, weakestSegments, analysis, context) {
// Si aucun segment à améliorer, retourner texte original
if (weakestSegments.length === 0) {
const originalContent = allSegments.map(s => s.content).join(' ');
return {
content: originalContent,
modifications: 0,
skipped: true,
reason: 'No weak segments identified'
};
}
// Créer Map des indices des segments à améliorer pour lookup rapide
const weakIndices = new Set(weakestSegments.map(s => s.index));
// === AMÉLIORER UNIQUEMENT LES SEGMENTS FAIBLES ===
const improvedSegments = [];
let totalModifications = 0;
for (const segment of allSegments) {
if (weakIndices.has(segment.index)) {
// AMÉLIORER ce segment
try {
const result = await this.applyLayer(layerName, segment.content, analysis, context);
improvedSegments.push({
...segment,
content: result.skipped ? segment.content : result.content,
improved: !result.skipped
});
totalModifications += result.modifications || 0;
} catch (error) {
logSh(` ⚠️ Échec amélioration segment ${segment.index}: ${error.message}`, 'WARN');
// Fallback: garder segment original
improvedSegments.push({
...segment,
content: segment.content, // ✅ FIX: Copier content original
improved: false
});
}
} else {
// GARDER segment intact
improvedSegments.push({
...segment,
content: segment.content, // ✅ FIX: Copier content original
improved: false
});
}
}
// === RÉASSEMBLER TEXTE COMPLET ===
const reassembledContent = improvedSegments.map(s => s.content).join(' ');
// Nettoyer espaces multiples
const cleanedContent = reassembledContent.replace(/\s{2,}/g, ' ').trim();
const improvedCount = improvedSegments.filter(s => s.improved).length;
logSh(`${improvedCount}/${allSegments.length} segments améliorés (${totalModifications} modifs)`, 'DEBUG');
return {
content: cleanedContent,
modifications: totalModifications,
segmentsImproved: improvedCount,
segmentsTotal: allSegments.length,
skipped: false
};
}
/**
* MODES DISPONIBLES
*/
static getAvailableModes() {
return [
{
name: 'analysis_only',
description: 'Analyse uniquement (sans amélioration)',
layers: []
},
{
name: 'technical_only',
description: 'Améliorations techniques ciblées uniquement',
layers: ['technical']
},
{
name: 'style_only',
description: 'Améliorations style ciblées uniquement',
layers: ['style']
},
{
name: 'readability_only',
description: 'Améliorations lisibilité ciblées uniquement',
layers: ['readability']
},
{
name: 'full',
description: 'Analyse + toutes améliorations ciblées (recommandé)',
layers: ['technical', 'style', 'readability']
}
];
}
}
module.exports = { SmartTouchCore };