## 🎯 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>
440 lines
16 KiB
JavaScript
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 };
|