## 🎯 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>
310 lines
9.1 KiB
JavaScript
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 };
|