seo-generator-server/lib/selective-smart-touch/SmartStyleLayer.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

274 lines
9.8 KiB
JavaScript

// ========================================
// SMART STYLE LAYER - Améliorations style CIBLÉES
// Responsabilité: Appliquer UNIQUEMENT les améliorations style identifiées par analyse
// LLM: Mistral (excellence style et personnalité)
// Architecture: Phase 2 de SelectiveSmartTouch (post-analyse)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* SMART STYLE LAYER
* Applique améliorations style précises identifiées par SmartAnalysisLayer
*/
class SmartStyleLayer {
constructor() {
this.name = 'SmartStyle';
this.defaultLLM = 'mistral-small';
}
/**
* APPLIQUER AMÉLIORATIONS STYLE CIBLÉES
*/
async applyTargeted(content, analysis, context = {}) {
return await tracer.run('SmartStyle.applyTargeted()', async () => {
const { mc0, personality, intensity = 1.0 } = context;
// Si aucune amélioration style nécessaire, skip
if (!analysis.style.needed) {
logSh(`⏭️ SMART STYLE: Aucune amélioration nécessaire (score: ${analysis.style.score.toFixed(2)})`, 'DEBUG');
return {
content,
modifications: 0,
skipped: true,
reason: 'No style improvements needed'
};
}
// === GARDE-FOU QUANTITATIF: Compter expressions familières existantes ===
const familiarExpressions = this.countFamiliarExpressions(content);
const totalFamiliar = Object.values(familiarExpressions).reduce((sum, count) => sum + count, 0);
logSh(`🔍 Expressions familières détectées: ${totalFamiliar} (${JSON.stringify(familiarExpressions)})`, 'DEBUG');
// Si déjà trop d'expressions familières, SKIP ou WARN
if (totalFamiliar > 15) {
logSh(`🛡️ GARDE-FOU: ${totalFamiliar} expressions familières déjà présentes (> seuil 15), SKIP amélioration style`, 'WARN');
return {
content,
modifications: 0,
skipped: true,
reason: `Too many familiar expressions already (${totalFamiliar} > 15 threshold)`
};
}
await tracer.annotate({
smartStyle: true,
contentLength: content.length,
hasPersonality: !!personality,
intensity,
familiarExpressionsCount: totalFamiliar
});
// ✅ Utiliser LLM fourni dans context, sinon fallback sur defaultLLM
const llmToUse = context.llmProvider || this.defaultLLM;
const startTime = Date.now();
logSh(`🎨 SMART STYLE: Application améliorations style ciblées avec ${llmToUse}`, 'DEBUG');
try {
const prompt = this.createTargetedPrompt(content, analysis, context);
const response = await callLLM(llmToUse, prompt, {
temperature: 0.7, // Créativité modérée pour style
maxTokens: 2500
}, personality);
const improvedContent = this.cleanResponse(response);
const modifications = this.countModifications(content, improvedContent);
const duration = Date.now() - startTime;
logSh(`✅ SMART STYLE terminé: ${modifications} modifications appliquées (${duration}ms)`, 'DEBUG');
await tracer.event('Smart Style appliqué', {
duration,
modifications,
personalityApplied: personality?.nom || 'generic'
});
return {
content: improvedContent,
modifications,
duration,
personalityApplied: personality?.nom
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ SMART STYLE ÉCHOUÉ (${duration}ms): ${error.message}`, 'ERROR');
return {
content,
modifications: 0,
error: error.message,
fallback: true
};
}
}, { contentLength: content.length, analysis });
}
/**
* CRÉER PROMPT CIBLÉ
*/
createTargetedPrompt(content, analysis, context) {
const { mc0, personality, intensity = 1.0, budgetManager, currentTag } = context;
// Extraire améliorations style
const styleImprovements = analysis.improvements.filter(imp =>
imp.toLowerCase().includes('style') ||
imp.toLowerCase().includes('ton') ||
imp.toLowerCase().includes('personnalis') ||
imp.toLowerCase().includes('expression') ||
imp.toLowerCase().includes('vocabulaire')
);
// === ✅ NOUVEAU: Récupérer budget alloué pour ce tag ===
let budgetConstraints = '';
if (budgetManager && currentTag) {
const tagBudget = budgetManager.getBudgetForTag(currentTag);
const remainingGlobal = budgetManager.getRemainingBudget();
budgetConstraints = `
⚠️ === CONTRAINTES BUDGET EXPRESSIONS FAMILIÈRES (STRICTES) ===
Budget alloué pour CE tag uniquement:
- "costaud" : MAX ${tagBudget.costaud} fois
- "nickel" : MAX ${tagBudget.nickel} fois
- "tip-top" : MAX ${tagBudget.tipTop} fois
- "impeccable" : MAX ${tagBudget.impeccable} fois
- "solide" : MAX ${tagBudget.solide} fois
Budget global restant : ${remainingGlobal.remaining}/${remainingGlobal.total} expressions (${remainingGlobal.percentage}% consommé)
🚨 RÈGLE ABSOLUE: Ne dépasse JAMAIS ces limites. Si budget = 0, N'UTILISE PAS ce mot.
Si tu utilises un mot au-delà de son budget, le texte sera REJETÉ.`;
}
return `MISSION: Améliore UNIQUEMENT les aspects STYLE listés ci-dessous.
CONTENU ORIGINAL:
"${content}"
${mc0 ? `CONTEXTE SUJET: ${mc0}` : ''}
${personality ? `PERSONNALITÉ CIBLE: ${personality.nom} (${personality.style})
VOCABULAIRE PRÉFÉRÉ: ${personality.vocabulairePref || 'professionnel'}` : 'STYLE: Professionnel standard'}
INTENSITÉ: ${intensity.toFixed(1)}${budgetConstraints}
AMÉLIORATIONS STYLE À APPLIQUER:
${styleImprovements.map((imp, i) => `${i + 1}. ${imp}`).join('\n')}
${analysis.style.genericPhrases && analysis.style.genericPhrases.length > 0 ? `
EXPRESSIONS GÉNÉRIQUES À PERSONNALISER:
${analysis.style.genericPhrases.map(phrase => `- "${phrase}"`).join('\n')}
` : ''}
${analysis.style.toneIssues && analysis.style.toneIssues.length > 0 ? `
PROBLÈMES DE TON IDENTIFIÉS:
${analysis.style.toneIssues.map(issue => `- ${issue}`).join('\n')}
` : ''}
CONSIGNES STRICTES:
- Applique UNIQUEMENT les améliorations style listées ci-dessus
- NE CHANGE PAS le fond du message ni les informations factuelles
- GARDE la même structure et longueur (±15%)
${personality ? `- Applique style "${personality.style}" de façon MESURÉE (pas d'exagération)` : '- Style professionnel web standard'}
- ⚠️ MAINTIENS un TON PROFESSIONNEL (limite connecteurs oraux à 1-2 MAX)
- ⚠️ ÉVITE bombardement de marqueurs de personnalité
EXEMPLES AMÉLIORATION STYLE (génériques multi-secteurs):
**E-commerce mode:**
- ✅ BON: "Cette robe allie élégance et confort" → style commercial mesuré
- ❌ MAUVAIS: "Écoutez, du coup cette robe, voilà, elle est vraiment top" → trop oral
**Services professionnels:**
- ✅ BON: "Notre expertise comptable garantit votre conformité" → professionnel et spécifique
- ❌ MAUVAIS: "Nos solutions de qualité" → générique et vague
**SaaS/Tech:**
- ✅ BON: "Automatisez vos workflows en 3 clics" → action concrète
- ❌ MAUVAIS: "Notre plateforme innovante optimise vos processus" → buzzwords creux
**Contenu informatif:**
- ✅ BON: "Le réchauffement climatique atteint +1.2°C depuis 1850" → factuel et précis
- ❌ MAUVAIS: "Le réchauffement climatique est un problème important" → vague
RÈGLES VOCABULAIRE & TON:
- Remplace expressions génériques par spécificités
- 1-2 touches de personnalité par paragraphe MAXIMUM
- Pas de saturation de connecteurs familiers ("du coup", "voilà", "écoutez")
- Privilégie authenticité sur artifice
FORMAT RÉPONSE:
Retourne UNIQUEMENT le contenu stylisé, SANS balises, SANS métadonnées, SANS explications.`;
}
/**
* NETTOYER RÉPONSE
*/
cleanResponse(response) {
if (!response) return response;
let cleaned = response.trim();
// Supprimer balises
cleaned = cleaned.replace(/^TAG:\s*[^\s]+\s+/gi, '');
cleaned = cleaned.replace(/\bTAG:\s*[^\s]+\s+/gi, '');
cleaned = cleaned.replace(/^CONTENU:\s*/gi, '');
cleaned = cleaned.replace(/^CONTENU STYLISÉ:\s*/gi, '');
cleaned = cleaned.replace(/^(voici\s+)?le\s+contenu\s+(stylisé|avec\s+style)\s*[:.]?\s*/gi, '');
cleaned = cleaned.replace(/^(dans\s+le\s+style\s+de\s+)[^:]*[:.]?\s*/gi, '');
// Nettoyer formatage
cleaned = cleaned.replace(/\*\*([^*]+)\*\*/g, '$1');
cleaned = cleaned.replace(/\s{2,}/g, ' ');
cleaned = cleaned.trim();
return cleaned;
}
/**
* COMPTER MODIFICATIONS
*/
countModifications(original, improved) {
if (original === improved) return 0;
const originalWords = original.toLowerCase().split(/\s+/);
const improvedWords = improved.toLowerCase().split(/\s+/);
let differences = 0;
differences += Math.abs(originalWords.length - improvedWords.length);
const minLength = Math.min(originalWords.length, improvedWords.length);
for (let i = 0; i < minLength; i++) {
if (originalWords[i] !== improvedWords[i]) {
differences++;
}
}
return differences;
}
/**
* ✅ NOUVEAU: Compter expressions familières dans le contenu
*/
countFamiliarExpressions(content) {
const contentLower = content.toLowerCase();
return {
costaud: (contentLower.match(/costaud/g) || []).length,
nickel: (contentLower.match(/nickel/g) || []).length,
tipTop: (contentLower.match(/tip[\s-]?top/g) || []).length,
impeccable: (contentLower.match(/impeccable/g) || []).length,
solide: (contentLower.match(/solide/g) || []).length,
duCoup: (contentLower.match(/du coup/g) || []).length,
voila: (contentLower.match(/voilà/g) || []).length,
ecoutez: (contentLower.match(/écoutez/g) || []).length
};
}
}
module.exports = { SmartStyleLayer };