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

310 lines
9.1 KiB
JavaScript

// ========================================
// GLOBAL BUDGET MANAGER - Gestion budget global expressions familières
// Responsabilité: Empêcher spam en distribuant budget limité sur tous les tags
// Architecture: Budget défini par personality, distribué aléatoirement, consommé et tracké
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* GLOBAL BUDGET MANAGER
* Gère un budget global d'expressions familières pour toute une génération
*/
class GlobalBudgetManager {
constructor(personality, contentMap = {}, config = {}) {
this.personality = personality;
this.contentMap = contentMap;
this.config = config;
// Paramètre configurable : caractères par expression (default 4000)
this.charsPerExpression = config.charsPerExpression || 4000;
// Calculer taille totale du contenu
const totalChars = Object.values(contentMap).join('').length;
this.totalChars = totalChars;
// Budget par défaut (sera écrasé par calcul dynamique)
this.defaultBudget = {
costaud: 2,
nickel: 2,
tipTop: 1,
impeccable: 2,
solide: 3,
total: 10
};
// === ✅ NOUVEAU: Calculer budget dynamiquement ===
this.budget = this.calculateDynamicBudget(contentMap, personality);
// Tracking consommation
this.consumed = {};
this.totalConsumed = 0;
// Assignments par tag
this.tagAssignments = {};
}
/**
* ✅ NOUVEAU: Calculer budget dynamiquement selon taille texte
* Formule : budgetTotal = Math.floor(totalChars / charsPerExpression)
* Puis soustraire occurrences existantes
*/
calculateDynamicBudget(contentMap, personality) {
const fullText = Object.values(contentMap).join(' ');
const totalChars = fullText.length;
// Calculer budget total basé sur taille
const budgetTotal = Math.max(1, Math.floor(totalChars / this.charsPerExpression));
logSh(`📏 Taille texte: ${totalChars} chars → Budget calculé: ${budgetTotal} expressions (${this.charsPerExpression} chars/expr)`, 'INFO');
// Compter occurrences existantes de chaque expression
const fullTextLower = fullText.toLowerCase();
const existingOccurrences = {
costaud: (fullTextLower.match(/costaud/g) || []).length,
nickel: (fullTextLower.match(/nickel/g) || []).length,
tipTop: (fullTextLower.match(/tip[\s-]?top/g) || []).length,
impeccable: (fullTextLower.match(/impeccable/g) || []).length,
solide: (fullTextLower.match(/solide/g) || []).length
};
const totalExisting = Object.values(existingOccurrences).reduce((sum, count) => sum + count, 0);
logSh(`📊 Occurrences existantes: ${JSON.stringify(existingOccurrences)} (total: ${totalExisting})`, 'DEBUG');
// Budget disponible = Budget calculé - Occurrences existantes
let budgetAvailable = Math.max(0, budgetTotal - totalExisting);
logSh(`💰 Budget disponible après soustraction: ${budgetAvailable}/${budgetTotal}`, 'INFO');
// Si aucun budget disponible, retourner budget vide
if (budgetAvailable === 0) {
logSh(`⚠️ Budget épuisé par occurrences existantes, aucune expression familière supplémentaire autorisée`, 'WARN');
return {
costaud: 0,
nickel: 0,
tipTop: 0,
impeccable: 0,
solide: 0,
total: 0
};
}
// Distribuer budget disponible selon style personality
return this.distributeBudgetByStyle(budgetAvailable, personality);
}
/**
* Distribuer budget selon style personality
*/
distributeBudgetByStyle(budgetTotal, personality) {
if (!personality || !personality.style) {
// Fallback: distribution équitable
return this.equalDistribution(budgetTotal);
}
const style = personality.style.toLowerCase();
if (style.includes('familier') || style.includes('accessible')) {
// Kévin-like: plus d'expressions familières variées
return {
costaud: Math.floor(budgetTotal * 0.25),
nickel: Math.floor(budgetTotal * 0.25),
tipTop: Math.floor(budgetTotal * 0.15),
impeccable: Math.floor(budgetTotal * 0.2),
solide: Math.floor(budgetTotal * 0.15),
total: budgetTotal
};
} else if (style.includes('technique') || style.includes('précis')) {
// Marc-like: presque pas d'expressions familières (tout sur "solide")
return {
costaud: 0,
nickel: 0,
tipTop: 0,
impeccable: Math.floor(budgetTotal * 0.3),
solide: Math.floor(budgetTotal * 0.7),
total: budgetTotal
};
}
// Fallback: distribution équitable
return this.equalDistribution(budgetTotal);
}
/**
* Distribution équitable du budget
*/
equalDistribution(budgetTotal) {
const perExpression = Math.floor(budgetTotal / 5);
return {
costaud: perExpression,
nickel: perExpression,
tipTop: perExpression,
impeccable: perExpression,
solide: perExpression,
total: budgetTotal
};
}
/**
* Initialiser budget depuis personality ou fallback default (LEGACY - non utilisé)
*/
initializeBudget(personality) {
if (personality?.budgetExpressions) {
logSh(`📊 Budget expressions depuis personality: ${JSON.stringify(personality.budgetExpressions)}`, 'DEBUG');
return { ...personality.budgetExpressions };
}
// Fallback: adapter selon style personality
if (personality?.style) {
const style = personality.style.toLowerCase();
if (style.includes('familier') || style.includes('accessible')) {
// Kévin-like: plus d'expressions familières autorisées
return {
costaud: 2,
nickel: 2,
tipTop: 1,
impeccable: 2,
solide: 3,
total: 10
};
} else if (style.includes('technique') || style.includes('précis')) {
// Marc-like: presque pas d'expressions familières
return {
costaud: 0,
nickel: 0,
tipTop: 0,
impeccable: 1,
solide: 2,
total: 3
};
}
}
// Fallback ultime
logSh(`📊 Budget expressions par défaut: ${JSON.stringify(this.defaultBudget)}`, 'DEBUG');
return { ...this.defaultBudget };
}
/**
* Distribuer budget aléatoirement sur tous les tags
* @param {array} tags - Liste des tags à traiter
*/
distributeRandomly(tags) {
logSh(`🎲 Distribution aléatoire du budget sur ${tags.length} tags`, 'DEBUG');
const assignments = {};
// Initialiser tous les tags à 0
tags.forEach(tag => {
assignments[tag] = {
costaud: 0,
nickel: 0,
tipTop: 0,
impeccable: 0,
solide: 0
};
});
// Distribuer chaque expression
const expressions = ['costaud', 'nickel', 'tipTop', 'impeccable', 'solide'];
expressions.forEach(expr => {
const maxCount = this.budget[expr] || 0;
for (let i = 0; i < maxCount; i++) {
// Choisir tag aléatoire
const randomTag = tags[Math.floor(Math.random() * tags.length)];
assignments[randomTag][expr]++;
}
});
this.tagAssignments = assignments;
logSh(`✅ Budget distribué: ${JSON.stringify(assignments)}`, 'DEBUG');
return assignments;
}
/**
* Obtenir budget alloué pour un tag spécifique
*/
getBudgetForTag(tag) {
return this.tagAssignments[tag] || {
costaud: 0,
nickel: 0,
tipTop: 0,
impeccable: 0,
solide: 0
};
}
/**
* Consommer budget (marquer comme utilisé)
*/
consumeBudget(tag, expression, count = 1) {
if (!this.consumed[tag]) {
this.consumed[tag] = {
costaud: 0,
nickel: 0,
tipTop: 0,
impeccable: 0,
solide: 0
};
}
this.consumed[tag][expression] += count;
this.totalConsumed += count;
logSh(`📉 Budget consommé [${tag}] ${expression}: ${count} (total: ${this.totalConsumed}/${this.budget.total})`, 'DEBUG');
}
/**
* Vérifier si on peut encore utiliser une expression
*/
canUse(tag, expression) {
const allocated = this.tagAssignments[tag]?.[expression] || 0;
const used = this.consumed[tag]?.[expression] || 0;
const canUse = used < allocated && this.totalConsumed < this.budget.total;
if (!canUse) {
logSh(`🚫 Budget épuisé pour [${tag}] ${expression} (used: ${used}/${allocated}, total: ${this.totalConsumed}/${this.budget.total})`, 'DEBUG');
}
return canUse;
}
/**
* Obtenir budget restant global
*/
getRemainingBudget() {
return {
remaining: this.budget.total - this.totalConsumed,
total: this.budget.total,
consumed: this.totalConsumed,
percentage: ((this.totalConsumed / this.budget.total) * 100).toFixed(1)
};
}
/**
* Générer rapport final
*/
getReport() {
const remaining = this.getRemainingBudget();
return {
budget: this.budget,
totalConsumed: this.totalConsumed,
remaining: remaining.remaining,
percentageUsed: remaining.percentage,
consumedByTag: this.consumed,
allocatedByTag: this.tagAssignments,
overBudget: this.totalConsumed > this.budget.total
};
}
}
module.exports = { GlobalBudgetManager };