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>
This commit is contained in:
StillHammer 2025-10-14 01:06:28 +08:00
parent 74bf1b0f38
commit 9a2ef7da2b
34 changed files with 6552 additions and 251 deletions

View File

@ -10,6 +10,9 @@ const { getStoredArticle, getRecentArticles } = require('./ArticleStorage');
const { DynamicPromptEngine } = require('./prompt-engine/DynamicPromptEngine');
const { TrendManager } = require('./trend-prompts/TrendManager');
const { WorkflowEngine } = require('./workflow-configuration/WorkflowEngine');
const { ValidatorCore } = require('./validation/ValidatorCore');
const fs = require('fs').promises;
const path = require('path');
class APIController {
constructor() {
@ -21,6 +24,46 @@ class APIController {
this.promptEngine = new DynamicPromptEngine();
this.trendManager = new TrendManager();
this.workflowEngine = new WorkflowEngine();
// ✅ PHASE 3: Validation tracking
this.activeValidations = new Map(); // Track running validations
this.validationHistory = []; // Store completed validations
this.wsServer = null; // WebSocket server reference (injected by ManualServer)
}
/**
* PHASE 3: Injecte le serveur WebSocket pour broadcasting
*/
setWebSocketServer(wsServer) {
this.wsServer = wsServer;
logSh('📡 WebSocket server injecté dans APIController', 'DEBUG');
}
/**
* PHASE 3: Broadcast un message aux clients WebSocket
*/
broadcastToClients(data) {
if (!this.wsServer || !this.wsServer.clients) {
return;
}
const message = JSON.stringify(data);
let sent = 0;
this.wsServer.clients.forEach(client => {
if (client.readyState === 1) { // OPEN state
try {
client.send(message);
sent++;
} catch (error) {
logSh(`⚠️ Erreur envoi WebSocket: ${error.message}`, 'WARN');
}
}
});
if (sent > 0) {
logSh(`📡 Message broadcast à ${sent} clients`, 'TRACE');
}
}
// ========================================
@ -706,6 +749,372 @@ class APIController {
});
}
}
// ========================================
// VALIDATION API (PHASE 3)
// ========================================
/**
* POST /api/validation/start - Démarre une nouvelle validation
*/
async startValidation(req, res) {
try {
const {
pipelineConfig,
rowNumber = 2,
config = {}
} = req.body;
// Validation
if (!pipelineConfig) {
return res.status(400).json({
success: false,
error: 'Configuration pipeline requise'
});
}
logSh(`🚀 Démarrage validation: ${pipelineConfig.name || 'Sans nom'}`, 'INFO');
// ✅ PHASE 3: Créer nouvelle instance ValidatorCore avec broadcast callback
const validator = new ValidatorCore({
broadcastCallback: (data) => this.broadcastToClients(data)
});
// Démarrer validation en arrière-plan
const validationPromise = validator.runValidation(config, pipelineConfig, rowNumber);
// Stocker la validation active
this.activeValidations.set(validator.validationId, {
validator,
promise: validationPromise,
startTime: Date.now(),
pipelineConfig,
rowNumber,
status: 'running'
});
// Gérer la completion en arrière-plan
validationPromise.then(result => {
const validation = this.activeValidations.get(validator.validationId);
if (validation) {
validation.status = result.success ? 'completed' : 'error';
validation.result = result;
validation.endTime = Date.now();
// Déplacer vers historique après 5min
setTimeout(() => {
this.validationHistory.push(validation);
this.activeValidations.delete(validator.validationId);
}, 5 * 60 * 1000);
}
}).catch(error => {
const validation = this.activeValidations.get(validator.validationId);
if (validation) {
validation.status = 'error';
validation.error = error.message;
validation.endTime = Date.now();
}
});
// Répondre immédiatement avec validation ID
res.status(202).json({
success: true,
data: {
validationId: validator.validationId,
status: 'running',
message: 'Validation démarrée en arrière-plan'
},
timestamp: new Date().toISOString()
});
} catch (error) {
logSh(`❌ Erreur démarrage validation: ${error.message}`, 'ERROR');
res.status(500).json({
success: false,
error: 'Erreur lors du démarrage de la validation',
message: error.message
});
}
}
/**
* GET /api/validation/status/:id - Récupère le statut d'une validation
*/
async getValidationStatus(req, res) {
try {
const { id } = req.params;
logSh(`📊 Récupération statut validation: ${id}`, 'DEBUG');
// Chercher dans validations actives
const activeValidation = this.activeValidations.get(id);
if (activeValidation) {
const status = activeValidation.validator.getStatus();
return res.json({
success: true,
data: {
validationId: id,
status: activeValidation.status,
progress: status.progress,
startTime: activeValidation.startTime,
duration: Date.now() - activeValidation.startTime,
pipelineName: activeValidation.pipelineConfig.name,
result: activeValidation.result || null
},
timestamp: new Date().toISOString()
});
}
// Chercher dans historique
const historicalValidation = this.validationHistory.find(v => v.validator.validationId === id);
if (historicalValidation) {
return res.json({
success: true,
data: {
validationId: id,
status: historicalValidation.status,
startTime: historicalValidation.startTime,
endTime: historicalValidation.endTime,
duration: historicalValidation.endTime - historicalValidation.startTime,
pipelineName: historicalValidation.pipelineConfig.name,
result: historicalValidation.result
},
timestamp: new Date().toISOString()
});
}
// Non trouvé
return res.status(404).json({
success: false,
error: 'Validation non trouvée',
validationId: id
});
} catch (error) {
logSh(`❌ Erreur récupération statut validation: ${error.message}`, 'ERROR');
res.status(500).json({
success: false,
error: 'Erreur lors de la récupération du statut',
message: error.message
});
}
}
/**
* POST /api/validation/stop/:id - Arrête une validation en cours
*/
async stopValidation(req, res) {
try {
const { id } = req.params;
logSh(`🛑 Arrêt validation: ${id}`, 'INFO');
const validation = this.activeValidations.get(id);
if (!validation) {
return res.status(404).json({
success: false,
error: 'Validation non trouvée ou déjà terminée',
validationId: id
});
}
// Note: Pour l'instant, on ne peut pas vraiment interrompre une validation en cours
// On marque juste le statut comme "stopped"
validation.status = 'stopped';
validation.endTime = Date.now();
logSh(`⚠️ Validation ${id} marquée comme arrêtée (le processus continue en arrière-plan)`, 'WARN');
res.json({
success: true,
data: {
validationId: id,
status: 'stopped',
message: 'Validation marquée comme arrêtée'
},
timestamp: new Date().toISOString()
});
} catch (error) {
logSh(`❌ Erreur arrêt validation: ${error.message}`, 'ERROR');
res.status(500).json({
success: false,
error: 'Erreur lors de l\'arrêt de la validation',
message: error.message
});
}
}
/**
* GET /api/validation/list - Liste toutes les validations
*/
async listValidations(req, res) {
try {
const { status, limit = 50 } = req.query;
logSh(`📋 Récupération liste validations`, 'DEBUG');
// Collecter validations actives
const activeList = Array.from(this.activeValidations.values()).map(v => ({
validationId: v.validator.validationId,
status: v.status,
startTime: v.startTime,
duration: Date.now() - v.startTime,
pipelineName: v.pipelineConfig.name,
progress: v.validator.getStatus().progress
}));
// Collecter historique
const historyList = this.validationHistory.map(v => ({
validationId: v.validator.validationId,
status: v.status,
startTime: v.startTime,
endTime: v.endTime,
duration: v.endTime - v.startTime,
pipelineName: v.pipelineConfig.name
}));
// Combiner et filtrer
let allValidations = [...activeList, ...historyList];
if (status) {
allValidations = allValidations.filter(v => v.status === status);
}
// Limiter résultats
const limitedValidations = allValidations.slice(0, parseInt(limit));
res.json({
success: true,
data: {
validations: limitedValidations,
total: allValidations.length,
active: activeList.length,
historical: historyList.length
},
timestamp: new Date().toISOString()
});
} catch (error) {
logSh(`❌ Erreur liste validations: ${error.message}`, 'ERROR');
res.status(500).json({
success: false,
error: 'Erreur lors de la récupération de la liste',
message: error.message
});
}
}
/**
* GET /api/validation/:id/report - Récupère le rapport complet d'une validation
*/
async getValidationReport(req, res) {
try {
const { id } = req.params;
logSh(`📊 Récupération rapport validation: ${id}`, 'DEBUG');
// Chercher le dossier de validation
const validationDir = path.join(process.cwd(), 'validations', id);
const reportPath = path.join(validationDir, 'report.json');
try {
const reportContent = await fs.readFile(reportPath, 'utf8');
const report = JSON.parse(reportContent);
res.json({
success: true,
data: report,
timestamp: new Date().toISOString()
});
} catch (fileError) {
return res.status(404).json({
success: false,
error: 'Rapport de validation non trouvé',
validationId: id,
message: fileError.message
});
}
} catch (error) {
logSh(`❌ Erreur récupération rapport: ${error.message}`, 'ERROR');
res.status(500).json({
success: false,
error: 'Erreur lors de la récupération du rapport',
message: error.message
});
}
}
/**
* GET /api/validation/:id/evaluations - Récupère les évaluations détaillées
*/
async getValidationEvaluations(req, res) {
try {
const { id } = req.params;
logSh(`📊 Récupération évaluations validation: ${id}`, 'DEBUG');
const validationDir = path.join(process.cwd(), 'validations', id);
const evaluationsPath = path.join(validationDir, 'results', 'evaluations.json');
try {
const evaluationsContent = await fs.readFile(evaluationsPath, 'utf8');
const evaluations = JSON.parse(evaluationsContent);
res.json({
success: true,
data: evaluations,
timestamp: new Date().toISOString()
});
} catch (fileError) {
return res.status(404).json({
success: false,
error: 'Évaluations non trouvées',
validationId: id,
message: fileError.message
});
}
} catch (error) {
logSh(`❌ Erreur récupération évaluations: ${error.message}`, 'ERROR');
res.status(500).json({
success: false,
error: 'Erreur lors de la récupération des évaluations',
message: error.message
});
}
}
/**
* NOUVEAU: GET /api/validation/presets - Récupère les presets disponibles
*/
async getValidationPresets(req, res) {
try {
const { VALIDATION_PRESETS } = require('./validation/ValidatorCore');
logSh(`📋 Récupération presets validation`, 'DEBUG');
res.json({
success: true,
data: {
presets: VALIDATION_PRESETS,
count: Object.keys(VALIDATION_PRESETS).length
},
timestamp: new Date().toISOString()
});
} catch (error) {
logSh(`❌ Erreur récupération presets: ${error.message}`, 'ERROR');
res.status(500).json({
success: false,
error: 'Erreur lors de la récupération des presets',
message: error.message
});
}
}
}
module.exports = { APIController };

View File

@ -324,13 +324,59 @@ Nom1, Nom2, Nom3, Nom4`;
temperature: 1.0
};
const response = await axios.post(CONFIG.openai.endpoint, requestData, {
headers: {
'Authorization': `Bearer ${CONFIG.openai.apiKey}`,
'Content-Type': 'application/json'
},
timeout: 300000
});
// ✅ Retry logic avec backoff exponentiel
let response;
let lastError;
const maxRetries = 3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
logSh(`📡 Appel OpenAI (tentative ${attempt}/${maxRetries})...`, 'DEBUG');
response = await axios.post(CONFIG.openai.endpoint, requestData, {
headers: {
'Authorization': `Bearer ${CONFIG.openai.apiKey}`,
'Content-Type': 'application/json'
},
timeout: 30000 // ✅ Timeout réduit à 30s (au lieu de 300s)
});
logSh(`✅ Réponse OpenAI reçue (tentative ${attempt})`, 'DEBUG');
break; // Succès → sortir de la boucle
} catch (error) {
lastError = error;
logSh(`⚠️ Tentative ${attempt}/${maxRetries} échouée: ${error.message}`, 'WARNING');
if (attempt < maxRetries) {
const delayMs = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
logSh(`⏳ Attente ${delayMs/1000}s avant nouvelle tentative...`, 'DEBUG');
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
// Si toutes les tentatives ont échoué → Fallback sélection aléatoire
if (!response) {
logSh(`⚠️ FALLBACK: Toutes tentatives OpenAI échouées → Sélection aléatoire de 4 personnalités`, 'WARNING');
// Sélection aléatoire fallback
const shuffled = [...randomPersonalities];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
const fallbackPersonalities = shuffled.slice(0, 4);
logSh(`🎲 Équipe de 4 personnalités (FALLBACK ALÉATOIRE):`, "INFO");
fallbackPersonalities.forEach((p, index) => {
const roles = ['BASE', 'TECHNIQUE', 'FLUIDITÉ', 'STYLE'];
logSh(` ${index + 1}. ${roles[index]}: ${p.nom} (${p.style})`, "INFO");
});
return fallbackPersonalities;
}
const selectedNames = response.data.choices[0].message.content.trim()
.split(',')

View File

@ -235,9 +235,15 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
function buildRequestData(modelId, prompt, options, personality) {
const config = LLM_CONFIG[modelId];
const temperature = options.temperature || config.temperature;
let temperature = options.temperature || config.temperature;
let maxTokens = options.maxTokens || config.maxTokens;
// Anthropic Claude: temperature must be 0-1 (clamp if needed)
if (config.provider === 'anthropic' && temperature > 1.0) {
logSh(` ⚠️ Claude: temperature clamped from ${temperature.toFixed(2)} to 1.0 (API limit)`, 'WARNING');
temperature = 1.0;
}
// GPT-5: Force minimum tokens (reasoning tokens + content tokens)
if (modelId.startsWith('gpt-5')) {
const MIN_GPT5_TOKENS = 1500; // Minimum pour reasoning + contenu

View File

@ -269,7 +269,7 @@ etc...`;
*/
function createEnhancementPrompt(elementsToEnhance, config, strategy) {
const { detectorTarget, intensity } = config;
let prompt = `MISSION: Améliore subtilement ces contenus pour réduire détection ${detectorTarget}.
AMÉLIORATIONS CIBLÉES:
@ -287,10 +287,17 @@ CONSIGNES:
- Focus sur réduction détection ${detectorTarget}
- Intensité: ${intensity.toFixed(2)}
FORMAT:
[1] Contenu légèrement amélioré
[2] Contenu légèrement amélioré
etc...`;
FORMAT DE RÉPONSE OBLIGATOIRE (UN PAR LIGNE):
[1] Contenu légèrement amélioré pour élément 1
[2] Contenu légèrement amélioré pour élément 2
[3] Contenu légèrement amélioré pour élément 3
etc...
IMPORTANT:
- Réponds UNIQUEMENT avec les contenus améliorés
- GARDE le numéro [N] devant chaque contenu
- PAS d'explications, PAS de commentaires
- RESPECTE STRICTEMENT le format [N] Contenu`;
return prompt;
}
@ -330,23 +337,56 @@ function parseRegenerationResponse(response, chunk) {
*/
function parseEnhancementResponse(response, elementsToEnhance) {
const results = {};
// Log réponse brute pour debug
logSh(`📥 Réponse LLM (${response.length} chars): ${response.substring(0, 200)}...`, 'DEBUG');
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < elementsToEnhance.length) {
let enhancedContent = cleanAdversarialContent(match[2].trim());
const element = elementsToEnhance[index];
if (enhancedContent && enhancedContent.length > 10) {
results[element.tag] = enhancedContent;
} else {
results[element.tag] = element.content; // Fallback
const parsedIndexes = new Set();
while ((match = regex.exec(response)) !== null) {
const num = parseInt(match[1]);
const index = num - 1; // [1] = index 0
if (index >= 0 && index < elementsToEnhance.length && !parsedIndexes.has(index)) {
let enhancedContent = cleanAdversarialContent(match[2].trim());
const element = elementsToEnhance[index];
if (enhancedContent && enhancedContent.length > 10) {
results[element.tag] = enhancedContent;
parsedIndexes.add(index);
logSh(` ✅ Parsé [${num}] ${element.tag}: ${enhancedContent.substring(0, 50)}...`, 'DEBUG');
} else {
logSh(` ⚠️ [${num}] ${element.tag}: contenu trop court (${enhancedContent?.length || 0} chars)`, 'WARNING');
}
}
index++;
}
// Vérifier si parsing a échoué
if (Object.keys(results).length === 0 && elementsToEnhance.length > 0) {
logSh(`❌ PARSING ÉCHOUÉ: Aucun élément parsé (format LLM invalide)`, 'ERROR');
logSh(` Réponse complète: ${response}`, 'ERROR');
// FALLBACK: Essayer parsing alternatif (sans numéros)
logSh(` 🔄 Tentative parsing alternatif...`, 'WARNING');
// Diviser par double saut de ligne ou tirets
const chunks = response.split(/\n\n+|---+/).map(c => c.trim()).filter(c => c.length > 10);
chunks.forEach((chunk, idx) => {
if (idx < elementsToEnhance.length) {
const cleaned = cleanAdversarialContent(chunk);
if (cleaned && cleaned.length > 10) {
results[elementsToEnhance[idx].tag] = cleaned;
logSh(` ✅ Fallback [${idx + 1}]: ${cleaned.substring(0, 50)}...`, 'DEBUG');
}
}
});
}
logSh(`📦 Résultat parsing: ${Object.keys(results).length}/${elementsToEnhance.length} éléments extraits`, 'DEBUG');
return results;
}
@ -355,23 +395,36 @@ function parseEnhancementResponse(response, elementsToEnhance) {
*/
function selectElementsForEnhancement(existingContent, config) {
const elements = [];
// ✅ Threshold basé sur intensity
// intensity >= 1.0 → threshold = 0.3 (traiter risque moyen/élevé)
// intensity < 1.0 → threshold = 0.4 (traiter uniquement risque élevé)
const threshold = config.intensity >= 1.0 ? 0.3 : 0.4;
logSh(`🎯 Sélection enhancement avec threshold=${(threshold * 100).toFixed(0)}% (intensity=${config.intensity})`, 'DEBUG');
Object.entries(existingContent).forEach(([tag, content]) => {
const detectionRisk = assessDetectionRisk(content, config.detectorTarget);
if (detectionRisk.score > 0.6) { // Risque élevé
if (detectionRisk.score > threshold) {
elements.push({
tag,
content,
detectionRisk: detectionRisk.reasons.join(', '),
detectionRisk: detectionRisk.reasons.join(', ') || 'prévention_générale',
priority: detectionRisk.score
});
logSh(` ✅ [${tag}] Sélectionné: score=${(detectionRisk.score * 100).toFixed(0)}% > ${(threshold * 100).toFixed(0)}%`, 'INFO');
} else {
// Log éléments ignorés pour debug
logSh(` ⏭️ [${tag}] Ignoré: score=${(detectionRisk.score * 100).toFixed(0)}% ≤ ${(threshold * 100).toFixed(0)}%`, 'DEBUG');
}
});
// Trier par priorité (risque élevé en premier)
elements.sort((a, b) => b.priority - a.priority);
logSh(` 📊 Sélection: ${elements.length}/${Object.keys(existingContent).length} éléments (threshold=${(threshold * 100).toFixed(0)}%)`, 'DEBUG');
return elements;
}
@ -393,46 +446,316 @@ function selectKeyElementsForRegeneration(content, config) {
}
/**
* Évaluer risque de détection
* Évaluer risque de détection (approche statistique générique)
* Basé sur des métriques linguistiques universelles sans mots hardcodés
*/
function assessDetectionRisk(content, detectorTarget) {
const reasons = [];
// Parsing de base
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
const words = content.split(/\s+/).filter(w => w.length > 0);
// Validation & Mode texte court
if (words.length < 5) {
return { score: 0, reasons: ['texte_trop_court(<5_mots)'], metrics: {} };
}
// ✅ MODE TEXTE COURT (1 phrase ou <10 mots)
if (sentences.length < 2 || words.length < 10) {
return assessShortTextRisk(content, words, detectorTarget);
}
// === CALCULER TOUTES LES MÉTRIQUES ===
const metrics = {
lexicalDiversity: calculateLexicalDiversity(words),
burstiness: calculateBurstiness(sentences),
syntaxEntropy: calculateSyntaxEntropy(sentences),
punctuationComplexity: calculatePunctuationComplexity(content),
redundancy: calculateRedundancy(words),
wordUniformity: calculateWordUniformity(words)
};
// === SCORING ADAPTATIF PAR DÉTECTEUR ===
let score = 0;
if (detectorTarget === 'gptZero') {
// GPTZero privilégie : perplexité + burstiness
score += metrics.lexicalDiversity.score * 0.30;
score += metrics.burstiness.score * 0.25;
score += metrics.syntaxEntropy.score * 0.15;
score += metrics.punctuationComplexity.score * 0.10;
score += metrics.redundancy.score * 0.10;
score += metrics.wordUniformity.score * 0.10;
if (metrics.lexicalDiversity.score > 0.3 && metrics.burstiness.score > 0.3) {
score += 0.05; // Bonus si double flag
reasons.push('gptzero_double_flag');
}
} else if (detectorTarget === 'originality') {
// Originality.ai privilégie : redondance + entropie syntaxique
score += metrics.redundancy.score * 0.30;
score += metrics.syntaxEntropy.score * 0.25;
score += metrics.lexicalDiversity.score * 0.15;
score += metrics.burstiness.score * 0.15;
score += metrics.punctuationComplexity.score * 0.10;
score += metrics.wordUniformity.score * 0.05;
if (metrics.redundancy.score > 0.4) {
score += 0.05; // Bonus haute redondance
reasons.push('originality_redondance_élevée');
}
} else {
// Détecteur général : ponctuation = meilleur indicateur (40%)
// Les LLMs modernes ont bon TTR et burstiness, mais ponctuation trop simple
const weights = [0.10, 0.20, 0.10, 0.40, 0.15, 0.05];
const metricScores = Object.values(metrics).map(m => m.score);
score = metricScores.reduce((sum, s, i) => sum + s * weights[i], 0);
}
// Collecter raisons
Object.entries(metrics).forEach(([name, data]) => {
if (data.score > 0.3) { // Seuil significatif
reasons.push(data.reason);
}
});
return {
score: Math.min(1, score),
reasons: reasons.length > 0 ? reasons : ['analyse_générale'],
metrics // Retourner pour debug
};
}
// ============= HELPER FUNCTIONS - MÉTRIQUES STATISTIQUES =============
/**
* 1 Diversité lexicale (Type-Token Ratio)
*/
function calculateLexicalDiversity(words) {
const cleanWords = words.map(w => w.toLowerCase().replace(/[^\w]/g, '')).filter(w => w.length > 0);
const uniqueWords = new Set(cleanWords);
const ttr = uniqueWords.size / cleanWords.length;
// TTR < 0.5 = vocabulaire répétitif (IA)
let score = 0;
if (ttr < 0.5) {
score = (0.5 - ttr) / 0.5; // Normaliser 0.5→0 = 0, 0→0.5 = 1
}
return {
score,
value: ttr,
reason: `low_lexical_diversity(TTR=${ttr.toFixed(2)})`
};
}
/**
* 2 Burstiness (Variation longueur phrases)
*/
function calculateBurstiness(sentences) {
const lengths = sentences.map(s => s.length);
const avg = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((sum, len) => sum + Math.pow(len - avg, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
const cv = stdDev / avg; // Coefficient de variation
// ✅ FIX: Seuil abaissé de 0.35 à 0.25 (LLMs modernes plus uniformes)
// CV < 0.25 = phrases très uniformes (IA moderne)
let score = 0;
if (cv < 0.25) {
score = (0.25 - cv) / 0.25; // Normaliser 0.25→0 = 0, 0→0.25 = 1
}
return {
score,
value: cv,
reason: `low_burstiness(CV=${cv.toFixed(2)})`
};
}
/**
* 3 Entropie syntaxique (Débuts de phrases répétés)
*/
function calculateSyntaxEntropy(sentences) {
const starts = sentences.map(s => {
const words = s.trim().split(/\s+/);
return words.slice(0, 2).join(' ').toLowerCase();
});
const freq = {};
starts.forEach(start => {
freq[start] = (freq[start] || 0) + 1;
});
const maxFreq = Math.max(...Object.values(freq));
const entropy = maxFreq / sentences.length;
// Entropie > 0.5 = >50% phrases commencent pareil (monotone)
let score = 0;
if (entropy > 0.5) {
score = (entropy - 0.5) / 0.5; // Normaliser 0.5→1 = 0→1
}
return {
score,
value: entropy,
reason: `high_syntax_entropy(${(entropy * 100).toFixed(0)}%)`
};
}
/**
* 4 Complexité ponctuation
*/
function calculatePunctuationComplexity(content) {
const simplePunct = (content.match(/[.,]/g) || []).length;
const complexPunct = (content.match(/[;:!?()—…]/g) || []).length;
const total = simplePunct + complexPunct;
if (total === 0) {
return { score: 0, value: 0, reason: 'no_punctuation' };
}
const ratio = complexPunct / total;
// Ratio < 0.1 = ponctuation trop simple (IA)
let score = 0;
if (ratio < 0.1) {
score = (0.1 - ratio) / 0.1; // Normaliser 0.1→0 = 0, 0→0.1 = 1
}
return {
score,
value: ratio,
reason: `low_punctuation_complexity(${(ratio * 100).toFixed(0)}%)`
};
}
/**
* 5 Redondance structurelle (Bigrammes répétés)
*/
function calculateRedundancy(words) {
const bigrams = [];
for (let i = 0; i < words.length - 1; i++) {
const bigram = `${words[i]} ${words[i + 1]}`.toLowerCase();
bigrams.push(bigram);
}
const freq = {};
bigrams.forEach(bg => {
freq[bg] = (freq[bg] || 0) + 1;
});
const repeatedCount = Object.values(freq).filter(count => count > 1).length;
const redundancy = repeatedCount / bigrams.length;
// Redondance > 0.2 = 20%+ bigrammes répétés (IA)
let score = 0;
if (redundancy > 0.2) {
score = Math.min(1, (redundancy - 0.2) / 0.3); // Normaliser 0.2→0.5 = 0→1
}
return {
score,
value: redundancy,
reason: `high_redundancy(${(redundancy * 100).toFixed(0)}%)`
};
}
/**
* 6 Uniformité longueur mots
*/
function calculateWordUniformity(words) {
const lengths = words.map(w => w.replace(/[^\w]/g, '').length).filter(l => l > 0);
if (lengths.length === 0) {
return { score: 0, value: 0, reason: 'no_words' };
}
const avg = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((sum, len) => sum + Math.pow(len - avg, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
// StdDev < 2.5 ET moyenne 4-8 lettres = mots uniformes (IA)
let score = 0;
if (stdDev < 2.5 && avg >= 4 && avg <= 8) {
score = (2.5 - stdDev) / 2.5; // Normaliser 2.5→0 = 0, 0→2.5 = 1
}
return {
score,
value: stdDev,
reason: `uniform_word_length(σ=${stdDev.toFixed(1)}, avg=${avg.toFixed(1)})`
};
}
/**
* MODE SPÉCIAL: Évaluation textes courts (1 phrase ou <10 mots)
* Utilise métriques adaptées aux textes courts
*/
function assessShortTextRisk(content, words, detectorTarget) {
let score = 0;
const reasons = [];
// Indicateurs génériques de contenu IA
const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge'];
const aiCount = aiWords.reduce((count, word) => {
return count + (content.toLowerCase().includes(word) ? 1 : 0);
}, 0);
if (aiCount > 2) {
score += 0.4;
reasons.push('mots_typiques_ia');
// === MÉTRIQUE 1: Complexité ponctuation (poids 50%) ===
const simplePunct = (content.match(/[.,]/g) || []).length;
const complexPunct = (content.match(/[;:!?()—…]/g) || []).length;
const total = simplePunct + complexPunct;
let punctScore = 0;
if (total > 0) {
const ratio = complexPunct / total;
if (ratio < 0.1) {
punctScore = (0.1 - ratio) / 0.1;
reasons.push(`low_punctuation(${(ratio * 100).toFixed(0)}%)`);
}
} else {
// Aucune ponctuation = suspect
punctScore = 0.3;
reasons.push('no_punctuation');
}
// Structure trop parfaite
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length > 2) {
const avgLength = sentences.reduce((sum, s) => sum + s.length, 0) / sentences.length;
const variance = sentences.reduce((sum, s) => sum + Math.pow(s.length - avgLength, 2), 0) / sentences.length;
const uniformity = 1 - (Math.sqrt(variance) / avgLength);
if (uniformity > 0.8) {
score += 0.3;
reasons.push('structure_uniforme');
score += punctScore * 0.50;
// === MÉTRIQUE 2: Longueur moyenne mots (poids 30%) ===
const lengths = words.map(w => w.replace(/[^\w]/g, '').length).filter(l => l > 0);
if (lengths.length > 0) {
const avg = lengths.reduce((a, b) => a + b) / lengths.length;
// Mots trop longs = formel/IA (avg > 7 lettres)
if (avg > 7) {
const wordLengthScore = (avg - 7) / 5; // Normaliser 7→12 = 0→1
score += Math.min(1, wordLengthScore) * 0.30;
reasons.push(`long_words(avg=${avg.toFixed(1)})`);
}
}
// Spécifique selon détecteur
if (detectorTarget === 'gptZero') {
// GPTZero détecte la prévisibilité
if (content.includes('par ailleurs') && content.includes('en effet')) {
score += 0.3;
reasons.push('connecteurs_prévisibles');
}
// === MÉTRIQUE 3: Ton formel (poids 20%) ===
const lowerContent = content.toLowerCase();
// Mots formels suspects
const formalWords = ['optimal', 'idéal', 'efficace', 'robuste', 'innovant', 'essentiel', 'crucial'];
const formalCount = formalWords.reduce((c, w) => c + (lowerContent.includes(w) ? 1 : 0), 0);
// Mots casual
const casualWords = ['super', 'top', 'cool', 'bref', 'truc', 'machin', 'genre'];
const casualCount = casualWords.reduce((c, w) => c + (lowerContent.includes(w) ? 1 : 0), 0);
if (formalCount > 0 && casualCount === 0 && words.length > 5) {
score += 0.20;
reasons.push(`formal_tone(${formalCount}_mots)`);
}
return { score: Math.min(1, score), reasons };
return {
score: Math.min(1, score),
reasons: reasons.length > 0 ? reasons : ['short_text_ok'],
metrics: {
textLength: words.length,
punctuationRatio: total > 0 ? complexPunct / total : 0,
avgWordLength: lengths.length > 0 ? lengths.reduce((a, b) => a + b) / lengths.length : 0
}
};
}
/**

View File

@ -288,8 +288,23 @@ async function applyAdaptiveLayers(content, options = {}) {
*/
async function applyLayerByConfig(content, layerConfig, globalOptions = {}) {
const { type, intensity, method, ...layerOptions } = layerConfig;
const options = { ...globalOptions, ...layerOptions, intensity, method };
// ✅ FIX: Ne override que si la valeur est explicitement définie (pas undefined/null)
const options = {
...globalOptions,
...layerOptions
};
// Override intensity seulement si défini dans layerConfig
if (intensity !== undefined && intensity !== null) {
options.intensity = intensity;
}
// Override method seulement si défini dans layerConfig
if (method !== undefined && method !== null) {
options.method = method;
}
switch (type) {
case 'general':
return await applyGeneralAdversarialLayer(content, options);

View File

@ -137,42 +137,39 @@ function applyLightFatigue(content, intensity) {
let modified = content;
let count = 0;
// Probabilité d'application - TOUJOURS APPLIQUER
const shouldApply = true; // FIXÉ V2: Application garantie (était Math.random() < 0.9)
// Probabilité d'application - ÉQUILIBRÉE (20-30% chance)
const shouldApply = Math.random() < (intensity * 0.3); // FIXÉ V3.1: ÉQUILIBRÉ - 30% max
if (!shouldApply) return { content: modified, count };
// Simplification des connecteurs complexes - ÉLARGI
// Simplification des connecteurs complexes - FIXÉ: Word boundaries
const complexConnectors = [
{ from: /néanmoins/gi, to: 'cependant' },
{ from: /par conséquent/gi, to: 'donc' },
{ from: /ainsi que/gi, to: 'et' },
{ from: /en outre/gi, to: 'aussi' },
{ from: /de surcroît/gi, to: 'de plus' },
{ from: /\bnéanmoins\b/gi, to: 'cependant' },
{ from: /\bpar conséquent\b/gi, to: 'donc' },
{ from: /\bainsi que\b/gi, to: 'et' },
{ from: /\ben outre\b/gi, to: 'aussi' },
{ from: /\bde surcroît\b/gi, to: 'de plus' },
// NOUVEAUX AJOUTS AGRESSIFS
{ from: /toutefois/gi, to: 'mais' },
{ from: /cependant/gi, to: 'mais bon' },
{ from: /par ailleurs/gi, to: 'sinon' },
{ from: /en effet/gi, to: 'effectivement' },
{ from: /de fait/gi, to: 'en fait' }
{ from: /\btoutefois\b/gi, to: 'mais' },
{ from: /\bcependant\b/gi, to: 'mais bon' },
{ from: /\bpar ailleurs\b/gi, to: 'sinon' },
{ from: /\ben effet\b/gi, to: 'effectivement' },
{ from: /\bde fait\b/gi, to: 'en fait' }
];
complexConnectors.forEach(connector => {
const matches = modified.match(connector.from);
if (matches) { // FIXÉ V2: 100% de remplacement garanti (était Math.random() < 0.9)
if (matches && Math.random() < 0.25) { // FIXÉ V3.1: ÉQUILIBRÉ - 25% chance
modified = modified.replace(connector.from, connector.to);
count++;
}
});
// AJOUT FIX V2: Si aucun connecteur complexe trouvé, appliquer une modification alternative
if (count === 0) {
// Injecter des simplifications basiques - GARANTI
if (modified.includes(' et ')) {
// AJOUT FIX V3: Fallback subtil SEULEMENT si très rare
if (count === 0 && Math.random() < 0.15) { // FIXÉ V3.1: 15% chance
// Injecter UNE SEULE simplification basique
if (modified.includes(' et ') && Math.random() < 0.5) {
modified = modified.replace(' et ', ' puis ');
count++;
} else if (modified.includes(' mais ')) {
modified = modified.replace(' mais ', ' mais bon ');
count++;
}
}
@ -186,7 +183,7 @@ function applyModerateFatigue(content, intensity) {
let modified = content;
let count = 0;
const shouldApply = Math.random() < (intensity * 0.8); // FIXÉ V2: Augmenté de 0.5 à 0.8
const shouldApply = Math.random() < (intensity * 0.25); // FIXÉ V3.1: ÉQUILIBRÉ - 25% max
if (!shouldApply) return { content: modified, count };
// Découpage phrases longues (>120 caractères)
@ -209,12 +206,12 @@ function applyModerateFatigue(content, intensity) {
modified = processedSentences.join('. ');
// Vocabulaire plus simple
// Vocabulaire plus simple - FIXÉ: Word boundaries
const simplifications = [
{ from: /optimisation/gi, to: 'amélioration' },
{ from: /méthodologie/gi, to: 'méthode' },
{ from: /problématique/gi, to: 'problème' },
{ from: /spécifications/gi, to: 'détails' }
{ from: /\boptimisation\b/gi, to: 'amélioration' },
{ from: /\bméthodologie\b/gi, to: 'méthode' },
{ from: /\bproblématique\b/gi, to: 'problème' },
{ from: /\bspécifications\b/gi, to: 'détails' }
];
simplifications.forEach(simpl => {
@ -234,7 +231,7 @@ function applyHeavyFatigue(content, intensity) {
let modified = content;
let count = 0;
const shouldApply = Math.random() < (intensity * 0.9); // FIXÉ V2: Augmenté de 0.7 à 0.9
const shouldApply = Math.random() < (intensity * 0.3); // FIXÉ V3.1: ÉQUILIBRÉ - 30% max
if (!shouldApply) return { content: modified, count };
// Injection répétitions naturelles
@ -255,13 +252,13 @@ function applyHeavyFatigue(content, intensity) {
modified = sentences.join('. ');
// Vocabulaire très basique
// Vocabulaire très basique - FIXÉ: Word boundaries
const basicVocab = [
{ from: /excellente?/gi, to: 'bonne' },
{ from: /remarquable/gi, to: 'bien' },
{ from: /sophistiqué/gi, to: 'avancé' },
{ from: /performant/gi, to: 'efficace' },
{ from: /innovations?/gi, to: 'nouveautés' }
{ from: /\bexcellente?\b/gi, to: 'bonne' },
{ from: /\bremarquable\b/gi, to: 'bien' },
{ from: /\bsophistiqué\b/gi, to: 'avancé' },
{ from: /\bperformant\b/gi, to: 'efficace' },
{ from: /\binnovations?\b/gi, to: 'nouveautés' }
];
basicVocab.forEach(vocab => {
@ -271,9 +268,41 @@ function applyHeavyFatigue(content, intensity) {
}
});
// Hésitations légères (rare)
// Hésitations légères (rare) - ÉLARGI 20+ variantes
if (Math.random() < 0.1) { // 10% chance
const hesitations = ['... enfin', '... disons', '... comment dire'];
const hesitations = [
// Hésitations classiques
'... enfin',
'... disons',
'... comment dire',
// Nuances et précisions
'... en quelque sorte',
'... si l\'on peut dire',
'... pour ainsi dire',
'... d\'une certaine manière',
// Relativisation
'... en tout cas',
'... de toute façon',
'... quoi qu\'il en soit',
// Confirmations hésitantes
'... n\'est-ce pas',
'... vous voyez',
'... si vous voulez',
// Reformulations
'... ou plutôt',
'... enfin bref',
'... en fait',
'... à vrai dire',
// Approximations
'... grosso modo',
'... en gros',
'... plus ou moins',
// Transitions hésitantes
'... bon',
'... eh bien',
'... alors',
'... du coup'
];
const hesitation = hesitations[Math.floor(Math.random() * hesitations.length)];
const words = modified.split(' ');
const insertIndex = Math.floor(words.length * 0.7); // Vers la fin

View File

@ -9,24 +9,28 @@ const { tracer } = require('../trace');
const { calculateFatigue, injectFatigueMarkers, getFatigueProfile } = require('./FatiguePatterns');
const { injectPersonalityErrors, getPersonalityErrorPatterns } = require('./PersonalityErrors');
const { applyTemporalStyle, getTemporalStyle } = require('./TemporalStyles');
const {
analyzeContentComplexity,
calculateReadabilityScore,
const { HumanSimulationTracker } = require('./HumanSimulationTracker');
const { selectAndApplyErrors } = require('./error-profiles/ErrorSelector'); // ✅ NOUVEAU: Système erreurs graduées
const {
analyzeContentComplexity,
calculateReadabilityScore,
preserveKeywords,
validateSimulationQuality
validateSimulationQuality
} = require('./HumanSimulationUtils');
/**
* CONFIGURATION PAR DÉFAUT
* VALIDATION DÉSACTIVÉE - Imperfections volontaires acceptées
*/
const DEFAULT_CONFIG = {
fatigueEnabled: true,
personalityErrorsEnabled: true,
temporalStyleEnabled: true,
imperfectionIntensity: 1.0, // FIXÉ V2: Intensité maximale par défaut (était 0.8)
graduatedErrorsEnabled: true, // ✅ NOUVEAU: Système erreurs graduées (grave/moyenne/légère)
imperfectionIntensity: 0.5,
naturalRepetitions: true,
qualityThreshold: 0.35, // FIXÉ V2: Seuil encore plus bas (était 0.4)
maxModificationsPerElement: 6 // FIXÉ V2: Plus de modifs possibles (était 5)
qualityThreshold: 0, // ✅ VALIDATION DÉSACTIVÉE (threshold=0)
maxModificationsPerElement: 3
};
/**
@ -53,13 +57,18 @@ async function applyHumanSimulationLayer(content, options = {}) {
try {
// Configuration fusionnée
const config = { ...DEFAULT_CONFIG, ...options };
// ✅ INITIALISATION TRACKER ANTI-RÉPÉTITION
const tracker = new HumanSimulationTracker();
logSh(`🧠 Tracker anti-répétition initialisé`, 'DEBUG');
// Stats de simulation
const simulationStats = {
elementsProcessed: 0,
fatigueModifications: 0,
personalityModifications: 0,
temporalModifications: 0,
spellingModifications: 0,
totalModifications: 0,
qualityScore: 0,
fallbackUsed: false
@ -67,7 +76,7 @@ async function applyHumanSimulationLayer(content, options = {}) {
// Contenu simulé
let simulatedContent = { ...content };
// ========================================
// 1. ANALYSE CONTEXTE GLOBAL
// ========================================
@ -98,25 +107,40 @@ async function applyHumanSimulationLayer(content, options = {}) {
// 2b. Erreurs Personnalité
if (config.personalityErrorsEnabled && globalContext.personalityProfile) {
const personalityResult = await applyPersonalitySimulation(processedContent, globalContext, config);
const personalityResult = await applyPersonalitySimulation(processedContent, globalContext, config, tracker);
processedContent = personalityResult.content;
elementModifications += personalityResult.modifications;
simulationStats.personalityModifications += personalityResult.modifications;
logSh(` 🎭 Personnalité: ${personalityResult.modifications} erreurs injectées`, 'DEBUG');
}
// 2c. Style Temporel
if (config.temporalStyleEnabled && globalContext.temporalStyle) {
const temporalResult = await applyTemporalSimulation(processedContent, globalContext, config);
const temporalResult = await applyTemporalSimulation(processedContent, globalContext, config, tracker);
processedContent = temporalResult.content;
elementModifications += temporalResult.modifications;
simulationStats.temporalModifications += temporalResult.modifications;
logSh(` ⏰ Temporel: ${temporalResult.modifications} ajustements (${globalContext.temporalStyle.period})`, 'DEBUG');
}
// 2d. Validation Qualité
// 2d. Erreurs Graduées Procédurales (NOUVEAU - grave 10% / moyenne 30% / légère 50%)
if (config.graduatedErrorsEnabled) {
const errorResult = selectAndApplyErrors(processedContent, {
currentHour: globalContext.currentHour,
tracker
});
processedContent = errorResult.content;
elementModifications += errorResult.errorsApplied;
simulationStats.graduatedErrors = (simulationStats.graduatedErrors || 0) + errorResult.errorsApplied;
if (errorResult.errorsApplied > 0) {
logSh(` 🎲 Erreurs graduées: ${errorResult.errorsApplied} (${errorResult.errorDetails.severity})`, 'DEBUG');
}
}
// 2e. Validation Qualité
const qualityCheck = validateSimulationQuality(elementContent, processedContent, config.qualityThreshold);
if (qualityCheck.acceptable) {
@ -155,7 +179,7 @@ async function applyHumanSimulationLayer(content, options = {}) {
logSh(`🧠 HUMAN SIMULATION - Terminé (${duration}ms)`, 'INFO');
logSh(`${simulationStats.elementsProcessed}/${Object.keys(content).length} éléments simulés`, 'INFO');
logSh(` 📊 ${simulationStats.fatigueModifications} fatigue | ${simulationStats.personalityModifications} personnalité | ${simulationStats.temporalModifications} temporel`, 'INFO');
logSh(` 📊 ${simulationStats.fatigueModifications} fatigue | ${simulationStats.personalityModifications} personnalité | ${simulationStats.temporalModifications} temporel | ${simulationStats.spellingModifications || 0} fautes`, 'INFO');
logSh(` 🎯 Score qualité: ${simulationStats.qualityScore.toFixed(2)} | Fallback: ${simulationStats.fallbackUsed ? 'OUI' : 'NON'}`, 'INFO');
await tracer.event('Human Simulation terminée', {
@ -240,11 +264,12 @@ async function applyFatigueSimulation(content, globalContext, config) {
/**
* APPLICATION SIMULATION PERSONNALITÉ
*/
async function applyPersonalitySimulation(content, globalContext, config) {
async function applyPersonalitySimulation(content, globalContext, config, tracker) {
const personalityResult = injectPersonalityErrors(
content,
globalContext.personalityProfile,
config.imperfectionIntensity
content,
globalContext.personalityProfile,
config.imperfectionIntensity,
tracker
);
return {
@ -256,9 +281,10 @@ async function applyPersonalitySimulation(content, globalContext, config) {
/**
* APPLICATION SIMULATION TEMPORELLE
*/
async function applyTemporalSimulation(content, globalContext, config) {
async function applyTemporalSimulation(content, globalContext, config, tracker) {
const temporalResult = applyTemporalStyle(content, globalContext.temporalStyle, {
intensity: config.imperfectionIntensity
intensity: config.imperfectionIntensity,
tracker
});
return {
@ -267,6 +293,7 @@ async function applyTemporalSimulation(content, globalContext, config) {
};
}
/**
* CALCUL SCORE QUALITÉ GLOBAL
*/

View File

@ -25,10 +25,11 @@ const HUMAN_SIMULATION_STACKS = {
fatigueEnabled: true,
personalityErrorsEnabled: true,
temporalStyleEnabled: false, // Désactivé en mode light
imperfectionIntensity: 0.5, // FIXÉ V2: Augmenté (était 0.3)
graduatedErrorsEnabled: true, // ✅ Erreurs graduées procédurales
imperfectionIntensity: 0.3,
naturalRepetitions: true,
qualityThreshold: 0.3, // FIXÉ V2: Abaissé drastiquement (était 0.8)
maxModificationsPerElement: 3 // FIXÉ V2: Augmenté (était 2)
qualityThreshold: 0, // ✅ VALIDATION DÉSACTIVÉE
maxModificationsPerElement: 2
},
expectedImpact: {
modificationsPerElement: '1-2',
@ -48,11 +49,12 @@ const HUMAN_SIMULATION_STACKS = {
config: {
fatigueEnabled: true,
personalityErrorsEnabled: true,
temporalStyleEnabled: true, // Activé
imperfectionIntensity: 0.8, // FIXÉ V2: Augmenté (était 0.6)
temporalStyleEnabled: true,
graduatedErrorsEnabled: true, // ✅ Erreurs graduées procédurales
imperfectionIntensity: 0.5,
naturalRepetitions: true,
qualityThreshold: 0.35, // FIXÉ V2: Abaissé drastiquement (était 0.7)
maxModificationsPerElement: 4 // FIXÉ V2: Augmenté (était 3)
qualityThreshold: 0, // ✅ VALIDATION DÉSACTIVÉE
maxModificationsPerElement: 3
},
expectedImpact: {
modificationsPerElement: '2-3',
@ -73,10 +75,11 @@ const HUMAN_SIMULATION_STACKS = {
fatigueEnabled: true,
personalityErrorsEnabled: true,
temporalStyleEnabled: true,
imperfectionIntensity: 1.2, // FIXÉ V2: Augmenté fortement (était 0.9)
spellingErrorsEnabled: true, // ✅ NOUVEAU
imperfectionIntensity: 0.7,
naturalRepetitions: true,
qualityThreshold: 0.3, // FIXÉ V2: Abaissé drastiquement (était 0.6)
maxModificationsPerElement: 6 // FIXÉ V2: Augmenté (était 5)
qualityThreshold: 0, // ✅ VALIDATION DÉSACTIVÉE
maxModificationsPerElement: 4
},
expectedImpact: {
modificationsPerElement: '3-5',

View File

@ -0,0 +1,181 @@
// ========================================
// FICHIER: HumanSimulationTracker.js
// RESPONSABILITÉ: Système anti-répétition centralisé
// Empêche spam de mots/phrases identiques
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* CLASSE TRACKER CENTRALISÉ
* Partage entre tous les modules Human Simulation
*/
class HumanSimulationTracker {
constructor() {
// Mots injectés (répétitions personnalité, fatigue)
this.injectedWords = new Set();
// Développements de phrases utilisés (soir)
this.usedDevelopments = new Set();
// Hésitations utilisées (fatigue élevée)
this.usedHesitations = new Set();
// Compteur fautes orthographe/grammaire
this.spellingErrorsApplied = 0;
// Stats globales
this.stats = {
wordsInjected: 0,
developmentsAdded: 0,
hesitationsAdded: 0,
spellingErrorsAdded: 0,
blockedRepetitions: 0
};
logSh('🧠 HumanSimulationTracker initialisé', 'DEBUG');
}
/**
* VÉRIFIER SI UN MOT PEUT ÊTRE INJECTÉ
* @param {string} word - Mot à injecter
* @param {string} content - Contenu actuel
* @param {number} maxOccurrences - Maximum occurrences autorisées (défaut: 2)
* @returns {boolean} - true si injection autorisée
*/
canInjectWord(word, content, maxOccurrences = 2) {
// Compter occurrences actuelles dans le contenu
const regex = new RegExp(`\\b${word}\\b`, 'gi');
const currentCount = (content.match(regex) || []).length;
// Vérifier si déjà injecté précédemment
const alreadyInjected = this.injectedWords.has(word.toLowerCase());
// Autoriser si < maxOccurrences ET pas déjà injecté
const canInject = currentCount < maxOccurrences && !alreadyInjected;
if (!canInject) {
logSh(` 🚫 Injection bloquée: "${word}" (déjà ${currentCount}× présent ou déjà injecté)`, 'DEBUG');
this.stats.blockedRepetitions++;
}
return canInject;
}
/**
* ENREGISTRER MOT INJECTÉ
* @param {string} word - Mot qui a été injecté
*/
trackInjectedWord(word) {
this.injectedWords.add(word.toLowerCase());
this.stats.wordsInjected++;
logSh(` ✅ Mot tracké: "${word}" (total: ${this.stats.wordsInjected})`, 'DEBUG');
}
/**
* VÉRIFIER SI UN DÉVELOPPEMENT PEUT ÊTRE UTILISÉ
* @param {string} development - Développement à ajouter
* @returns {boolean} - true si autorisation
*/
canUseDevelopment(development) {
const canUse = !this.usedDevelopments.has(development);
if (!canUse) {
logSh(` 🚫 Développement bloqué: déjà utilisé dans ce texte`, 'DEBUG');
this.stats.blockedRepetitions++;
}
return canUse;
}
/**
* ENREGISTRER DÉVELOPPEMENT UTILISÉ
* @param {string} development - Développement ajouté
*/
trackDevelopment(development) {
this.usedDevelopments.add(development);
this.stats.developmentsAdded++;
logSh(` ✅ Développement tracké (total: ${this.stats.developmentsAdded})`, 'DEBUG');
}
/**
* VÉRIFIER SI HÉSITATION PEUT ÊTRE AJOUTÉE
* @param {string} hesitation - Hésitation à ajouter
* @returns {boolean} - true si autorisation
*/
canUseHesitation(hesitation) {
const canUse = !this.usedHesitations.has(hesitation);
if (!canUse) {
logSh(` 🚫 Hésitation bloquée: déjà utilisée`, 'DEBUG');
this.stats.blockedRepetitions++;
}
return canUse;
}
/**
* ENREGISTRER HÉSITATION UTILISÉE
* @param {string} hesitation - Hésitation ajoutée
*/
trackHesitation(hesitation) {
this.usedHesitations.add(hesitation);
this.stats.hesitationsAdded++;
logSh(` ✅ Hésitation trackée: "${hesitation}" (total: ${this.stats.hesitationsAdded})`, 'DEBUG');
}
/**
* VÉRIFIER SI FAUTE ORTHOGRAPHE PEUT ÊTRE APPLIQUÉE
* Maximum 1 faute par texte complet
* @returns {boolean} - true si autorisation
*/
canApplySpellingError() {
const canApply = this.spellingErrorsApplied === 0;
if (!canApply) {
logSh(` 🚫 Faute spelling bloquée: déjà ${this.spellingErrorsApplied} faute(s) dans ce texte`, 'DEBUG');
}
return canApply;
}
/**
* ENREGISTRER FAUTE ORTHOGRAPHE APPLIQUÉE
*/
trackSpellingError() {
this.spellingErrorsApplied++;
this.stats.spellingErrorsAdded++;
logSh(` ✅ Faute spelling trackée (total: ${this.stats.spellingErrorsAdded})`, 'DEBUG');
}
/**
* OBTENIR STATISTIQUES
* @returns {object} - Stats complètes
*/
getStats() {
return {
...this.stats,
injectedWords: Array.from(this.injectedWords),
usedDevelopments: this.usedDevelopments.size,
usedHesitations: this.usedHesitations.size,
spellingErrorsApplied: this.spellingErrorsApplied
};
}
/**
* RÉINITIALISER TRACKER (pour nouveau texte)
*/
reset() {
this.injectedWords.clear();
this.usedDevelopments.clear();
this.usedHesitations.clear();
this.spellingErrorsApplied = 0;
logSh('🔄 HumanSimulationTracker réinitialisé', 'DEBUG');
}
}
// ============= EXPORTS =============
module.exports = {
HumanSimulationTracker
};

View File

@ -219,11 +219,12 @@ function createGenericErrorProfile() {
/**
* INJECTION ERREURS PERSONNALITÉ
* @param {string} content - Contenu à modifier
* @param {object} personalityProfile - Profil personnalité
* @param {object} personalityProfile - Profil personnalité
* @param {number} intensity - Intensité (0-2.0)
* @param {object} tracker - HumanSimulationTracker instance (optionnel)
* @returns {object} - { content, modifications }
*/
function injectPersonalityErrors(content, personalityProfile, intensity = 1.0) {
function injectPersonalityErrors(content, personalityProfile, intensity = 1.0, tracker = null) {
if (!content || !personalityProfile) {
return { content, modifications: 0 };
}
@ -242,7 +243,7 @@ function injectPersonalityErrors(content, personalityProfile, intensity = 1.0) {
// ========================================
// 1. RÉPÉTITIONS CARACTÉRISTIQUES
// ========================================
const repetitionResult = injectRepetitions(modifiedContent, personalityProfile, adjustedProbability);
const repetitionResult = injectRepetitions(modifiedContent, personalityProfile, adjustedProbability, tracker);
modifiedContent = repetitionResult.content;
modifications += repetitionResult.count;
@ -279,8 +280,9 @@ function injectPersonalityErrors(content, personalityProfile, intensity = 1.0) {
/**
* INJECTION RÉPÉTITIONS CARACTÉRISTIQUES
* @param {object} tracker - HumanSimulationTracker instance (optionnel)
*/
function injectRepetitions(content, profile, probability) {
function injectRepetitions(content, profile, probability, tracker = null) {
let modified = content;
let count = 0;
@ -288,33 +290,45 @@ function injectRepetitions(content, profile, probability) {
return { content: modified, count };
}
// Sélectionner 1-3 mots répétitifs pour ce contenu - FIXÉ: Plus de mots
// Sélectionner MAXIMUM 1 mot répétitif - FIXÉ V3: SUBTIL
const selectedWords = profile.repetitions
.sort(() => 0.5 - Math.random())
.slice(0, Math.random() < 0.5 ? 2 : 3); // FIXÉ: Au moins 2 mots sélectionnés
.slice(0, 1); // FIXÉ V3: UN SEUL mot maximum
selectedWords.forEach(word => {
if (Math.random() < probability) {
// ✅ ANTI-RÉPÉTITION: Vérifier avec tracker si autorisé
if (tracker && !tracker.canInjectWord(word, modified, 2)) {
logSh(` 🚫 Mot "${word}" bloqué par tracker (déjà trop présent)`, 'DEBUG');
return; // Skip ce mot
}
// FIXÉ V3.1: Probabilité ÉQUILIBRÉE (20%)
if (Math.random() < (probability * 0.2)) {
// Chercher des endroits appropriés pour injecter le mot
const sentences = modified.split('. ');
const targetSentenceIndex = Math.floor(Math.random() * sentences.length);
if (sentences[targetSentenceIndex] &&
if (sentences[targetSentenceIndex] &&
sentences[targetSentenceIndex].length > 30 &&
!sentences[targetSentenceIndex].toLowerCase().includes(word.toLowerCase())) {
// Injecter le mot de façon naturelle
const words = sentences[targetSentenceIndex].split(' ');
const insertIndex = Math.floor(words.length * (0.3 + Math.random() * 0.4)); // 30-70% de la phrase
// Adaptations contextuelles
const adaptedWord = adaptWordToContext(word, words[insertIndex] || '');
words.splice(insertIndex, 0, adaptedWord);
sentences[targetSentenceIndex] = words.join(' ');
modified = sentences.join('. ');
count++;
// ✅ Enregistrer dans tracker
if (tracker) {
tracker.trackInjectedWord(adaptedWord);
}
logSh(` 📝 Répétition injectée: "${adaptedWord}" dans phrase ${targetSentenceIndex + 1}`, 'DEBUG');
}
}
@ -337,13 +351,13 @@ function injectVocabularyTics(content, profile, probability) {
const selectedTics = profile.vocabularyTics.slice(0, 1); // Un seul tic par contenu
selectedTics.forEach(tic => {
if (Math.random() < probability * 0.8) { // Probabilité réduite pour les tics
if (Math.random() < probability * 0.15) { // FIXÉ V3.1: Probabilité ÉQUILIBRÉE (15%)
// Remplacer des connecteurs standards par le tic
const standardConnectors = ['par ailleurs', 'de plus', 'également', 'aussi'];
standardConnectors.forEach(connector => {
const regex = new RegExp(`\\b${connector}\\b`, 'gi');
if (modified.match(regex) && Math.random() < 0.4) {
if (modified.match(regex) && Math.random() < 0.2) { // FIXÉ V3.1: 20%
modified = modified.replace(regex, tic);
count++;
logSh(` 🗣️ Tic vocabulaire: "${connector}" → "${tic}"`, 'DEBUG');
@ -379,7 +393,7 @@ function injectAnglicisms(content, profile, probability) {
};
Object.entries(replacements).forEach(([french, english]) => {
if (profile.anglicisms.includes(english) && Math.random() < probability) {
if (profile.anglicisms.includes(english) && Math.random() < (probability * 0.1)) { // FIXÉ V3.1: 10%
const regex = new RegExp(`\\b${french}\\b`, 'gi');
if (modified.match(regex)) {
modified = modified.replace(regex, english);

View File

@ -0,0 +1,312 @@
// ========================================
// FICHIER: SpellingErrors.js
// RESPONSABILITÉ: Fautes d'orthographe et grammaire réalistes
// Probabilité MINUSCULE (1-3%) pour réalisme maximal
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* FAUTES D'ORTHOGRAPHE COURANTES EN FRANÇAIS
* Basées sur erreurs humaines fréquentes
*/
const COMMON_SPELLING_ERRORS = [
// Doubles consonnes manquantes
{ correct: 'appeler', wrong: 'apeler' },
{ correct: 'apparaître', wrong: 'aparaître' },
{ correct: 'occurrence', wrong: 'occurence' },
{ correct: 'connexion', wrong: 'connection' },
{ correct: 'professionnel', wrong: 'profesionnel' },
{ correct: 'efficace', wrong: 'éficace' },
{ correct: 'différent', wrong: 'différant' },
{ correct: 'développement', wrong: 'dévelopement' },
// Accents oubliés/inversés
{ correct: 'élément', wrong: 'element' },
{ correct: 'système', wrong: 'systeme' },
{ correct: 'intéressant', wrong: 'interessant' },
{ correct: 'qualité', wrong: 'qualite' },
{ correct: 'créer', wrong: 'creer' },
{ correct: 'dépôt', wrong: 'depot' },
// Homophones
{ correct: 'et', wrong: 'est', context: 'coord' }, // et/est
{ correct: 'a', wrong: 'à', context: 'verb' }, // a/à
{ correct: 'ce', wrong: 'se', context: 'demo' }, // ce/se
{ correct: 'leur', wrong: 'leurs', context: 'sing' }, // leur/leurs
{ correct: 'son', wrong: 'sont', context: 'poss' }, // son/sont
// Terminaisons -é/-er
{ correct: 'utilisé', wrong: 'utiliser', context: 'past' },
{ correct: 'développé', wrong: 'développer', context: 'past' },
{ correct: 'créé', wrong: 'créer', context: 'past' },
// Pluriels oubliés
{ correct: 'les éléments', wrong: 'les élément' },
{ correct: 'des solutions', wrong: 'des solution' },
{ correct: 'nos services', wrong: 'nos service' }
];
/**
* ERREURS DE GRAMMAIRE COURANTES
*/
const COMMON_GRAMMAR_ERRORS = [
// Accord sujet-verbe oublié
{ correct: /nous (sommes|avons|faisons)/gi, wrong: (match) => match.replace(/sommes|avons|faisons/, m => m.slice(0, -1)) },
{ correct: /ils (sont|ont|font)/gi, wrong: (match) => match.replace(/sont|ont|font/, m => m === 'sont' ? 'est' : m === 'ont' ? 'a' : 'fait') },
// Participes passés non accordés
{ correct: /les solutions sont (\w+)ées/gi, wrong: (match) => match.replace(/ées/, 'é') },
{ correct: /qui sont (\w+)és/gi, wrong: (match) => match.replace(/és/, 'é') },
// Virgules manquantes
{ correct: /(Néanmoins|Cependant|Toutefois|Par ailleurs),/gi, wrong: (match) => match.replace(',', '') },
{ correct: /(Ainsi|Donc|En effet),/gi, wrong: (match) => match.replace(',', '') }
];
/**
* FAUTES DE FRAPPE RÉALISTES
* Touches proches sur clavier AZERTY
*/
const TYPO_ERRORS = [
{ correct: 'q', wrong: 'a' }, // Touches adjacentes
{ correct: 's', wrong: 'd' },
{ correct: 'e', wrong: 'r' },
{ correct: 'o', wrong: 'p' },
{ correct: 'i', wrong: 'u' },
{ correct: 'n', wrong: 'b' },
{ correct: 'm', wrong: 'n' }
];
/**
* INJECTION FAUTES D'ORTHOGRAPHE
* Probabilité TRÈS FAIBLE (1-2%) - MAXIMUM 1 FAUTE PAR TEXTE
* @param {string} content - Contenu à modifier
* @param {number} intensity - Intensité (0-1)
* @param {object} tracker - HumanSimulationTracker instance (optionnel)
* @returns {object} - { content, modifications }
*/
function injectSpellingErrors(content, intensity = 0.5, tracker = null) {
if (!content || typeof content !== 'string') {
return { content, modifications: 0 };
}
// ✅ LIMITE 1 FAUTE: Vérifier avec tracker
if (tracker && !tracker.canApplySpellingError()) {
logSh(`🚫 Faute spelling bloquée: déjà ${tracker.spellingErrorsApplied} faute(s) appliquée(s)`, 'DEBUG');
return { content, modifications: 0 };
}
let modified = content;
let count = 0;
// Probabilité MINUSCULE: 1-2% base × intensité
const spellingErrorChance = 0.01 * intensity; // 1% max
logSh(`🔤 Injection fautes orthographe (chance: ${(spellingErrorChance * 100).toFixed(1)}%)`, 'DEBUG');
// Parcourir les fautes courantes - STOPPER APRÈS PREMIÈRE FAUTE
for (const error of COMMON_SPELLING_ERRORS) {
if (count > 0) break; // ✅ STOPPER après 1 faute
if (Math.random() < spellingErrorChance) {
// Vérifier présence du mot correct
const regex = new RegExp(`\\b${error.correct}\\b`, 'gi');
if (modified.match(regex)) {
// Remplacer UNE SEULE occurrence (pas toutes)
modified = modified.replace(regex, error.wrong);
count++;
// ✅ Enregistrer dans tracker
if (tracker) {
tracker.trackSpellingError();
}
logSh(` 📝 Faute ortho: "${error.correct}" → "${error.wrong}"`, 'DEBUG');
}
}
}
return { content: modified, modifications: count };
}
/**
* INJECTION FAUTES DE GRAMMAIRE
* Probabilité TRÈS FAIBLE (0.5-1%)
* @param {string} content - Contenu à modifier
* @param {number} intensity - Intensité (0-1)
* @returns {object} - { content, modifications }
*/
function injectGrammarErrors(content, intensity = 0.5) {
if (!content || typeof content !== 'string') {
return { content, modifications: 0 };
}
let modified = content;
let count = 0;
// Probabilité MINUSCULE: 0.5% base × intensité
const grammarErrorChance = 0.005 * intensity; // 0.5% max
logSh(`📐 Injection fautes grammaire (chance: ${(grammarErrorChance * 100).toFixed(1)}%)`, 'DEBUG');
// Virgules manquantes (plus fréquent)
if (Math.random() < grammarErrorChance * 3) { // 1.5% max
const commaPattern = /(Néanmoins|Cependant|Toutefois|Par ailleurs|Ainsi|Donc),/gi;
if (modified.match(commaPattern)) {
modified = modified.replace(commaPattern, (match) => match.replace(',', ''));
count++;
logSh(` 📝 Virgule oubliée après connecteur`, 'DEBUG');
}
}
// Accords sujet-verbe (rare)
if (Math.random() < grammarErrorChance) {
const subjectVerbPattern = /nous (sommes|avons|faisons)/gi;
if (modified.match(subjectVerbPattern)) {
modified = modified.replace(subjectVerbPattern, (match) => {
return match.replace(/sommes|avons|faisons/, m => {
if (m === 'sommes') return 'est';
if (m === 'avons') return 'a';
return 'fait';
});
});
count++;
logSh(` 📝 Accord sujet-verbe incorrect`, 'DEBUG');
}
}
return { content: modified, modifications: count };
}
/**
* INJECTION FAUTES DE FRAPPE
* Probabilité ULTRA MINUSCULE (0.1-0.5%)
* @param {string} content - Contenu à modifier
* @param {number} intensity - Intensité (0-1)
* @returns {object} - { content, modifications }
*/
function injectTypoErrors(content, intensity = 0.5) {
if (!content || typeof content !== 'string') {
return { content, modifications: 0 };
}
let modified = content;
let count = 0;
// Probabilité ULTRA MINUSCULE: 0.1% base × intensité
const typoChance = 0.001 * intensity; // 0.1% max
if (Math.random() > typoChance) {
return { content: modified, modifications: count };
}
logSh(`⌨️ Injection faute de frappe (chance: ${(typoChance * 100).toFixed(2)}%)`, 'DEBUG');
// Sélectionner UN mot au hasard
const words = modified.split(/\s+/);
if (words.length === 0) return { content: modified, modifications: count };
const targetWordIndex = Math.floor(Math.random() * words.length);
let targetWord = words[targetWordIndex];
// Appliquer UNE faute de frappe (touche adjacente)
if (targetWord.length > 3) { // Seulement sur mots > 3 lettres
const charIndex = Math.floor(Math.random() * targetWord.length);
const char = targetWord[charIndex].toLowerCase();
// Trouver remplacement touche adjacente
const typoError = TYPO_ERRORS.find(t => t.correct === char);
if (typoError) {
const newWord = targetWord.substring(0, charIndex) + typoError.wrong + targetWord.substring(charIndex + 1);
words[targetWordIndex] = newWord;
modified = words.join(' ');
count++;
logSh(` ⌨️ Faute de frappe: "${targetWord}" → "${newWord}"`, 'DEBUG');
}
}
return { content: modified, modifications: count };
}
/**
* APPLICATION COMPLÈTE FAUTES
* Orchestre tous les types de fautes
* @param {string} content - Contenu à modifier
* @param {object} options - { intensity, spellingEnabled, grammarEnabled, typoEnabled, tracker }
* @returns {object} - { content, modifications }
*/
function applySpellingErrors(content, options = {}) {
if (!content || typeof content !== 'string') {
return { content, modifications: 0 };
}
const {
intensity = 0.5,
spellingEnabled = true,
grammarEnabled = true,
typoEnabled = false, // Désactivé par défaut (trop risqué)
tracker = null
} = options;
let modified = content;
let totalModifications = 0;
// 1. Fautes d'orthographe (1-2% chance) - MAX 1 FAUTE
if (spellingEnabled) {
const spellingResult = injectSpellingErrors(modified, intensity, tracker);
modified = spellingResult.content;
totalModifications += spellingResult.modifications;
}
// 2. Fautes de grammaire (0.5-1% chance)
if (grammarEnabled) {
const grammarResult = injectGrammarErrors(modified, intensity);
modified = grammarResult.content;
totalModifications += grammarResult.modifications;
}
// 3. Fautes de frappe (0.1% chance - ULTRA RARE)
if (typoEnabled) {
const typoResult = injectTypoErrors(modified, intensity);
modified = typoResult.content;
totalModifications += typoResult.modifications;
}
if (totalModifications > 0) {
logSh(`✅ Fautes injectées: ${totalModifications} modification(s)`, 'DEBUG');
}
return {
content: modified,
modifications: totalModifications
};
}
/**
* OBTENIR STATISTIQUES FAUTES
*/
function getSpellingErrorStats() {
return {
totalSpellingErrors: COMMON_SPELLING_ERRORS.length,
totalGrammarErrors: COMMON_GRAMMAR_ERRORS.length,
totalTypoErrors: TYPO_ERRORS.length,
defaultProbabilities: {
spelling: '1-2%',
grammar: '0.5-1%',
typo: '0.1%'
}
};
}
// ============= EXPORTS =============
module.exports = {
applySpellingErrors,
injectSpellingErrors,
injectGrammarErrors,
injectTypoErrors,
getSpellingErrorStats,
COMMON_SPELLING_ERRORS,
COMMON_GRAMMAR_ERRORS,
TYPO_ERRORS
};

View File

@ -154,7 +154,7 @@ function getTemporalStyle(currentHour) {
* APPLICATION STYLE TEMPOREL
* @param {string} content - Contenu à modifier
* @param {object} temporalStyle - Style temporel à appliquer
* @param {object} options - Options { intensity }
* @param {object} options - Options { intensity, tracker }
* @returns {object} - { content, modifications }
*/
function applyTemporalStyle(content, temporalStyle, options = {}) {
@ -163,7 +163,8 @@ function applyTemporalStyle(content, temporalStyle, options = {}) {
}
const intensity = options.intensity || 1.0;
const tracker = options.tracker || null;
logSh(`⏰ Application style temporel: ${temporalStyle.period} (intensité: ${intensity})`, 'DEBUG');
let modifiedContent = content;
@ -172,7 +173,7 @@ function applyTemporalStyle(content, temporalStyle, options = {}) {
// ========================================
// 1. AJUSTEMENT LONGUEUR PHRASES
// ========================================
const sentenceResult = adjustSentenceLength(modifiedContent, temporalStyle, intensity);
const sentenceResult = adjustSentenceLength(modifiedContent, temporalStyle, intensity, tracker);
modifiedContent = sentenceResult.content;
modifications += sentenceResult.count;
@ -207,26 +208,27 @@ function applyTemporalStyle(content, temporalStyle, options = {}) {
/**
* AJUSTEMENT LONGUEUR PHRASES
* @param {object} tracker - HumanSimulationTracker instance (optionnel)
*/
function adjustSentenceLength(content, temporalStyle, intensity) {
function adjustSentenceLength(content, temporalStyle, intensity, tracker = null) {
let modified = content;
let count = 0;
const bias = temporalStyle.styleTendencies.shortSentencesBias * intensity;
const sentences = modified.split('. ');
// Probabilité d'appliquer les modifications - TOUJOURS APPLIQUER SI INTENSITÉ > 0.5
if (intensity < 0.5 && Math.random() > 0.3) { // FIXÉ V2: Seulement skip si très faible intensité
// Probabilité d'appliquer - ÉQUILIBRÉ (25% max)
if (Math.random() > (intensity * 0.25)) { // FIXÉ V3.1: ÉQUILIBRÉ - 25% max
return { content: modified, count };
}
const processedSentences = sentences.map(sentence => {
if (sentence.length < 20) return sentence; // Ignorer phrases très courtes
// Style MATIN/NUIT - Raccourcir phrases longues
if ((temporalStyle.period === 'matin' || temporalStyle.period === 'nuit') &&
if ((temporalStyle.period === 'matin' || temporalStyle.period === 'nuit') &&
sentence.length > 100 && Math.random() < bias) {
// Chercher point de coupe naturel
const cutPoints = [', qui', ', que', ', dont', ' et ', ' car ', ' mais '];
for (const cutPoint of cutPoints) {
@ -234,26 +236,70 @@ function adjustSentenceLength(content, temporalStyle, intensity) {
if (cutIndex > 30 && cutIndex < sentence.length - 30) {
count++;
logSh(` ✂️ Phrase raccourcie (${temporalStyle.period}): ${sentence.length}${cutIndex} chars`, 'DEBUG');
return sentence.substring(0, cutIndex) + '. ' +
return sentence.substring(0, cutIndex) + '. ' +
sentence.substring(cutIndex + cutPoint.length);
}
}
}
// Style SOIR - Allonger phrases courtes
if (temporalStyle.period === 'soir' &&
sentence.length > 30 && sentence.length < 80 &&
if (temporalStyle.period === 'soir' &&
sentence.length > 30 && sentence.length < 80 &&
Math.random() < (1 - bias)) {
// Ajouter développements
// Ajouter développements - ÉLARGI 20+ variantes
const developments = [
// Avantages et bénéfices
', ce qui constitue un avantage notable',
', permettant ainsi d\'optimiser les résultats',
', contribuant à l\'efficacité globale',
', offrant ainsi des perspectives intéressantes',
', garantissant une meilleure qualité',
// Processus et démarches
', dans une démarche d\'amélioration continue',
', contribuant à l\'efficacité globale'
', dans le cadre d\'une stratégie cohérente',
', selon une approche méthodique',
', grâce à une mise en œuvre rigoureuse',
// Contexte et perspective
', tout en respectant les standards en vigueur',
', conformément aux attentes du marché',
', en adéquation avec les besoins identifiés',
', dans le respect des contraintes établies',
// Résultats et impacts
', avec des résultats mesurables',
', pour un impact significatif',
', assurant une performance optimale',
', favorisant ainsi la satisfaction client',
// Approches techniques
', en utilisant des méthodes éprouvées',
', par le biais d\'une expertise reconnue',
', moyennant une analyse approfondie',
// Continuité et évolution
', dans une perspective d\'évolution constante',
', tout en maintenant un haut niveau d\'exigence',
', en veillant à la pérennité du système',
', garantissant une adaptation progressive'
];
const development = developments[Math.floor(Math.random() * developments.length)];
// ✅ ANTI-RÉPÉTITION: Filtrer développements déjà utilisés
let availableDevelopments = developments;
if (tracker) {
availableDevelopments = developments.filter(d => tracker.canUseDevelopment(d));
}
// Si aucun développement disponible, skip
if (availableDevelopments.length === 0) {
logSh(` 🚫 Aucun développement disponible (tous déjà utilisés)`, 'DEBUG');
return sentence;
}
const development = availableDevelopments[Math.floor(Math.random() * availableDevelopments.length)];
// ✅ Enregistrer dans tracker
if (tracker) {
tracker.trackDevelopment(development);
}
count++;
logSh(` 📝 Phrase allongée (soir): ${sentence.length}${sentence.length + development.length} chars`, 'DEBUG');
return sentence + development;
@ -276,8 +322,8 @@ function adaptVocabulary(content, temporalStyle, intensity) {
const vocabularyPrefs = temporalStyle.vocabularyPreferences;
const energyBias = temporalStyle.styleTendencies.energyWordsBias * intensity;
// Probabilité d'appliquer - TOUJOURS SI INTENSITÉ > 0.5
if (intensity < 0.5 && Math.random() > 0.3) { // FIXÉ V2: Seulement skip si très faible intensité
// Probabilité d'appliquer - ÉQUILIBRÉ (20% max)
if (Math.random() > (intensity * 0.2)) { // FIXÉ V3.1: ÉQUILIBRÉ - 20% max
return { content: modified, count };
}
@ -285,7 +331,13 @@ function adaptVocabulary(content, temporalStyle, intensity) {
const replacements = buildVocabularyReplacements(temporalStyle.period, vocabularyPrefs);
replacements.forEach(replacement => {
if (Math.random() < Math.max(0.8, energyBias)) { // FIXÉ V2: Minimum 80% chance (était 60%)
if (Math.random() < (energyBias * 0.3)) { // FIXÉ V3.1: ÉQUILIBRÉ - 30%
// ✅ PROTECTION: Vérifier expressions idiomatiques
if (isInProtectedExpression(modified, replacement.from)) {
logSh(` 🛡️ Remplacement bloqué: "${replacement.from}" dans expression protégée`, 'DEBUG');
return; // Skip ce remplacement
}
const regex = new RegExp(`\\b${replacement.from}\\b`, 'gi');
if (modified.match(regex)) {
modified = modified.replace(regex, replacement.to);
@ -295,32 +347,67 @@ function adaptVocabulary(content, temporalStyle, intensity) {
}
});
// AJOUT FIX V2: Si aucun remplacement, TOUJOURS forcer au moins une modification
if (count === 0) {
// Modification basique selon période - GARANTI
if (temporalStyle.period === 'matin') {
if (modified.includes('utiliser')) {
modified = modified.replace(/\butiliser\b/gi, 'optimiser');
count++;
} else if (modified.includes('bon')) {
modified = modified.replace(/\bbon\b/gi, 'excellent');
count++;
}
} else if (temporalStyle.period === 'soir' && modified.includes('bon')) {
modified = modified.replace(/\bbon\b/gi, 'considérable');
count++;
} else if (temporalStyle.period === 'nuit' && modified.includes('excellent')) {
modified = modified.replace(/\bexcellent\b/gi, 'bien');
count++;
}
if (count > 0) {
logSh(` 📚 Modification temporelle forcée (${temporalStyle.period})`, 'DEBUG');
}
}
// FIXÉ V3: PAS de fallback garanti - SUBTILITÉ MAXIMALE
// Aucune modification forcée
return { content: modified, count };
}
/**
* EXPRESSIONS IDIOMATIQUES FRANÇAISES PROTÉGÉES
* Ne JAMAIS remplacer de mots dans ces expressions
*/
const PROTECTED_EXPRESSIONS = [
// Expressions courantes avec "faire"
'faire toute la différence',
'faire attention',
'faire les choses',
'faire face',
'faire preuve',
'faire partie',
'faire confiance',
'faire référence',
'faire appel',
'faire en sorte',
// Expressions courantes avec "prendre"
'prendre en compte',
'prendre en charge',
'prendre conscience',
'prendre part',
'prendre soin',
// Expressions courantes avec "mettre"
'mettre en œuvre',
'mettre en place',
'mettre en avant',
'mettre l\'accent',
// Autres expressions figées
'avoir lieu',
'donner suite',
'tenir compte',
'bien entendu',
'en effet',
'en fait',
'tout à fait',
'par exemple'
];
/**
* VÉRIFIER SI UN MOT EST DANS UNE EXPRESSION PROTÉGÉE
* @param {string} content - Contenu complet
* @param {string} word - Mot à remplacer
* @returns {boolean} - true si dans expression protégée
*/
function isInProtectedExpression(content, word) {
const lowerContent = content.toLowerCase();
const lowerWord = word.toLowerCase();
return PROTECTED_EXPRESSIONS.some(expr => {
const exprLower = expr.toLowerCase();
// Vérifier si l'expression existe ET contient le mot
return exprLower.includes(lowerWord) && lowerContent.includes(exprLower);
});
}
/**
* CONSTRUCTION REMPLACEMENTS VOCABULAIRE
*/
@ -377,22 +464,22 @@ function adjustConnectors(content, temporalStyle, intensity) {
return { content: modified, count };
}
// Connecteurs selon période
// Connecteurs selon période - FIXÉ: Word boundaries pour éviter "maison" → "néanmoinson"
const connectorMappings = {
matin: [
{ from: /par conséquent/gi, to: 'donc' },
{ from: /néanmoins/gi, to: 'mais' },
{ from: /en outre/gi, to: 'aussi' }
{ from: /\bpar conséquent\b/gi, to: 'donc' },
{ from: /\bnéanmoins\b/gi, to: 'mais' },
{ from: /\ben outre\b/gi, to: 'aussi' }
],
soir: [
{ from: /donc/gi, to: 'par conséquent' },
{ from: /mais/gi, to: 'néanmoins' },
{ from: /aussi/gi, to: 'en outre' }
{ from: /\bdonc\b/gi, to: 'par conséquent' },
{ from: /\bmais\b/gi, to: 'néanmoins' }, // ✅ FIXÉ: \b empêche match dans "maison"
{ from: /\baussi\b/gi, to: 'en outre' }
],
nuit: [
{ from: /par conséquent/gi, to: 'donc' },
{ from: /néanmoins/gi, to: 'mais' },
{ from: /cependant/gi, to: 'mais' }
{ from: /\bpar conséquent\b/gi, to: 'donc' },
{ from: /\bnéanmoins\b/gi, to: 'mais' },
{ from: /\bcependant\b/gi, to: 'mais' }
]
};
@ -440,10 +527,28 @@ function adjustRhythm(content, temporalStyle, intensity) {
case 'relaxed': // Soir - plus de pauses
if (Math.random() < 0.3) {
// Ajouter quelques pauses réflexives
modified = modified.replace(/\. ([A-Z])/g, '. Ainsi, $1');
// Ajouter quelques pauses réflexives - 15+ variantes
const reflexivePauses = [
'Ainsi',
'Par ailleurs',
'De plus',
'En outre',
'D\'ailleurs',
'Également',
'Qui plus est',
'De surcroît',
'Par conséquent',
'Effectivement',
'En effet',
'Cela dit',
'Toutefois',
'Néanmoins',
'Pour autant'
];
const pause = reflexivePauses[Math.floor(Math.random() * reflexivePauses.length)];
modified = modified.replace(/\. ([A-Z])/g, `. ${pause}, $1`);
count++;
logSh(` 🧘 Rythme ralenti: pauses ajoutées`, 'DEBUG');
logSh(` 🧘 Rythme ralenti: pause "${pause}" ajoutée`, 'DEBUG');
}
break;
@ -520,5 +625,7 @@ module.exports = {
analyzeTemporalCoherence,
calculateCoherenceScore,
buildVocabularyReplacements,
TEMPORAL_STYLES
isInProtectedExpression,
TEMPORAL_STYLES,
PROTECTED_EXPRESSIONS
};

View File

@ -0,0 +1,219 @@
// ========================================
// FICHIER: ErrorGrave.js
// RESPONSABILITÉ: Erreurs GRAVES (10% articles max)
// Visibles mais crédibles - faute de frappe sérieuse ou distraction
// ========================================
const { logSh } = require('../../ErrorReporting');
/**
* DÉFINITIONS ERREURS GRAVES
* Probabilité globale: 10% des articles
* Maximum: 1 erreur grave par article
*/
const ERREURS_GRAVES = {
// ========================================
// ACCORD SUJET-VERBE INCORRECT
// ========================================
accord_sujet_verbe: {
name: 'Accord sujet-verbe incorrect',
probability: 0.3, // 30% si erreur grave sélectionnée
examples: [
{ pattern: /\bils (est|a)\b/gi, correction: 'ils sont/ont' },
{ pattern: /\bnous (est|a)\b/gi, correction: 'nous sommes/avons' },
{ pattern: /\belles (est|a)\b/gi, correction: 'elles sont/ont' }
],
apply: (content) => {
let modified = content;
let applied = false;
// Chercher "ils sont" ou "ils ont" et les remplacer
if (!applied && Math.random() < 0.5) {
const regex = /\bils sont\b/gi;
if (modified.match(regex)) {
modified = modified.replace(regex, 'ils est');
applied = true;
logSh(` ❌ Erreur grave: "ils sont" → "ils est"`, 'DEBUG');
}
}
if (!applied && Math.random() < 0.5) {
const regex = /\bils ont\b/gi;
if (modified.match(regex)) {
modified = modified.replace(regex, 'ils a');
applied = true;
logSh(` ❌ Erreur grave: "ils ont" → "ils a"`, 'DEBUG');
}
}
return { content: modified, applied };
}
},
// ========================================
// MOT MANQUANT (omission)
// ========================================
mot_manquant: {
name: 'Mot manquant (omission)',
probability: 0.25, // 25% si erreur grave
examples: [
{ pattern: 'pour garantir la qualité', error: 'pour garantir qualité' },
{ pattern: 'dans le but de', error: 'dans but de' },
{ pattern: 'il est important de', error: 'il important de' }
],
apply: (content) => {
let modified = content;
let applied = false;
// Supprimer article défini aléatoirement
const patterns = [
{ from: /\bpour garantir la qualité\b/gi, to: 'pour garantir qualité' },
{ from: /\bdans le but de\b/gi, to: 'dans but de' },
{ from: /\bil est important de\b/gi, to: 'il important de' },
{ from: /\bpour la durabilité\b/gi, to: 'pour durabilité' }
];
for (const pattern of patterns) {
if (applied) break;
if (modified.match(pattern.from) && Math.random() < 0.5) {
modified = modified.replace(pattern.from, pattern.to);
applied = true;
logSh(` ❌ Erreur grave: mot manquant`, 'DEBUG');
break;
}
}
return { content: modified, applied };
}
},
// ========================================
// DOUBLE MOT (copier-coller raté)
// ========================================
double_mot: {
name: 'Double mot (répétition)',
probability: 0.25, // 25% si erreur grave
examples: [
{ pattern: 'pour garantir', error: 'pour pour garantir' },
{ pattern: 'de la qualité', error: 'de de la qualité' }
],
apply: (content) => {
let modified = content;
let applied = false;
// Insérer doublon sur mots courants
const words = content.split(' ');
const targetWords = ['pour', 'de', 'la', 'le', 'et', 'dans'];
for (let i = 0; i < words.length && !applied; i++) {
const word = words[i].toLowerCase();
if (targetWords.includes(word) && Math.random() < 0.3) {
words.splice(i, 0, words[i]); // Dupliquer le mot
applied = true;
logSh(` ❌ Erreur grave: "${word}" dupliqué`, 'DEBUG');
break;
}
}
if (applied) {
modified = words.join(' ');
}
return { content: modified, applied };
}
},
// ========================================
// NÉGATION OUBLIÉE
// ========================================
negation_oubliee: {
name: 'Négation oubliée',
probability: 0.20, // 20% si erreur grave
examples: [
{ pattern: "n'est pas nécessaire", error: "est pas nécessaire" },
{ pattern: "ne sont pas", error: "sont pas" }
],
apply: (content) => {
let modified = content;
let applied = false;
// Supprimer "ne" ou "n'" dans négations
const patterns = [
{ from: /\bn'est pas\b/gi, to: 'est pas' },
{ from: /\bne sont pas\b/gi, to: 'sont pas' },
{ from: /\bn'ont pas\b/gi, to: 'ont pas' }
];
for (const pattern of patterns) {
if (applied) break;
if (modified.match(pattern.from) && Math.random() < 0.5) {
modified = modified.replace(pattern.from, pattern.to);
applied = true;
logSh(` ❌ Erreur grave: négation oubliée`, 'DEBUG');
break;
}
}
return { content: modified, applied };
}
}
};
/**
* APPLIQUER UNE ERREUR GRAVE
* @param {string} content - Contenu à modifier
* @param {object} tracker - HumanSimulationTracker instance
* @returns {object} - { content, applied, errorType }
*/
function applyErrorGrave(content, tracker = null) {
// Vérifier avec tracker si erreur grave déjà appliquée
if (tracker && tracker.graveErrorApplied) {
logSh(`🚫 Erreur grave bloquée: déjà 1 erreur grave dans cet article`, 'DEBUG');
return { content, applied: false, errorType: null };
}
// Sélectionner type d'erreur aléatoirement selon probabilités
const errorTypes = Object.keys(ERREURS_GRAVES);
const selectedType = errorTypes[Math.floor(Math.random() * errorTypes.length)];
const errorDefinition = ERREURS_GRAVES[selectedType];
logSh(`🎲 Tentative erreur grave: ${errorDefinition.name}`, 'DEBUG');
// Appliquer l'erreur
const result = errorDefinition.apply(content);
if (result.applied) {
logSh(`✅ Erreur grave appliquée: ${errorDefinition.name}`, 'DEBUG');
// Marquer dans tracker
if (tracker) {
tracker.graveErrorApplied = true;
}
}
return {
content: result.content,
applied: result.applied,
errorType: result.applied ? selectedType : null
};
}
/**
* OBTENIR STATISTIQUES ERREURS GRAVES
*/
function getErrorGraveStats() {
return {
totalTypes: Object.keys(ERREURS_GRAVES).length,
types: Object.keys(ERREURS_GRAVES),
globalProbability: '10%',
maxPerArticle: 1
};
}
// ============= EXPORTS =============
module.exports = {
ERREURS_GRAVES,
applyErrorGrave,
getErrorGraveStats
};

View File

@ -0,0 +1,245 @@
// ========================================
// FICHIER: ErrorLegere.js
// RESPONSABILITÉ: Erreurs LÉGÈRES (50% articles)
// Micro-erreurs très subtiles - quasi indétectables
// ========================================
const { logSh } = require('../../ErrorReporting');
/**
* DÉFINITIONS ERREURS LÉGÈRES
* Probabilité globale: 50% des articles
* Maximum: 3 erreurs légères par article
*/
const ERREURS_LEGERES = {
// ========================================
// DOUBLE ESPACE
// ========================================
double_espace: {
name: 'Double espace',
probability: 0.30,
examples: [
{ pattern: 'de votre', error: 'de votre' },
{ pattern: 'pour la', error: 'pour la' }
],
apply: (content) => {
let modified = content;
let applied = false;
// Insérer double espace aléatoirement
const words = content.split(' ');
if (words.length < 10) return { content: modified, applied };
const targetIndex = Math.floor(Math.random() * (words.length - 1));
words[targetIndex] = words[targetIndex] + ' '; // Ajouter espace supplémentaire
modified = words.join(' ');
applied = true;
logSh(` · Erreur légère: double espace à position ${targetIndex}`, 'DEBUG');
return { content: modified, applied };
}
},
// ========================================
// TRAIT D'UNION OUBLIÉ
// ========================================
trait_union_oublie: {
name: 'Trait d\'union oublié',
probability: 0.25,
examples: [
{ pattern: "c'est-à-dire", error: "c'est à dire" },
{ pattern: 'peut-être', error: 'peut être' },
{ pattern: 'vis-à-vis', error: 'vis à vis' }
],
apply: (content) => {
let modified = content;
let applied = false;
// Supprimer traits d'union
const patterns = [
{ from: /\bc'est-à-dire\b/gi, to: "c'est à dire" },
{ from: /\bpeut-être\b/gi, to: 'peut être' },
{ from: /\bvis-à-vis\b/gi, to: 'vis à vis' }
];
for (const pattern of patterns) {
if (applied) break;
if (modified.match(pattern.from) && Math.random() < 0.6) {
modified = modified.replace(pattern.from, pattern.to);
applied = true;
logSh(` · Erreur légère: trait d'union oublié`, 'DEBUG');
break;
}
}
return { content: modified, applied };
}
},
// ========================================
// ESPACE AVANT PONCTUATION
// ========================================
espace_avant_ponctuation: {
name: 'Espace avant ponctuation manquante',
probability: 0.20,
examples: [
{ pattern: 'qualité ?', error: 'qualité?' },
{ pattern: 'résistance !', error: 'résistance!' }
],
apply: (content) => {
let modified = content;
let applied = false;
// Supprimer espace avant ? ou !
if (Math.random() < 0.5) {
modified = modified.replace(/ \?/g, '?');
if (modified !== content) {
applied = true;
logSh(` · Erreur légère: espace manquant avant "?"`, 'DEBUG');
}
} else {
modified = modified.replace(/ !/g, '!');
if (modified !== content) {
applied = true;
logSh(` · Erreur légère: espace manquant avant "!"`, 'DEBUG');
}
}
return { content: modified, applied };
}
},
// ========================================
// MAJUSCULE INCORRECTE
// ========================================
majuscule_incorrecte: {
name: 'Majuscule incorrecte',
probability: 0.15,
examples: [
{ pattern: 'la France', error: 'la france' },
{ pattern: 'Toutenplaque', error: 'toutenplaque' }
],
apply: (content) => {
let modified = content;
let applied = false;
// Mettre en minuscule un nom propre
const properNouns = ['France', 'Paris', 'Toutenplaque'];
for (const noun of properNouns) {
if (applied) break;
const regex = new RegExp(`\\b${noun}\\b`, 'g');
if (modified.match(regex) && Math.random() < 0.4) {
modified = modified.replace(regex, noun.toLowerCase());
applied = true;
logSh(` · Erreur légère: majuscule incorrecte sur "${noun}"`, 'DEBUG');
break;
}
}
return { content: modified, applied };
}
},
// ========================================
// APOSTROPHE DROITE (au lieu de courbe)
// ========================================
apostrophe_droite: {
name: 'Apostrophe droite au lieu de courbe',
probability: 0.10,
examples: [
{ pattern: "l'article", error: "l'article" },
{ pattern: "d'une", error: "d'une" }
],
apply: (content) => {
let modified = content;
let applied = false;
// Remplacer apostrophe courbe par droite
const apostropheCourbe = '\u2019'; // ' (apostrophe typographique)
if (modified.includes(apostropheCourbe) && Math.random() < 0.5) {
// Remplacer UNE occurrence seulement
const index = modified.indexOf(apostropheCourbe);
if (index !== -1) {
modified = modified.substring(0, index) + "'" + modified.substring(index + 1);
applied = true;
logSh(` · Erreur légère: apostrophe droite au lieu de courbe`, 'DEBUG');
}
}
return { content: modified, applied };
}
}
};
/**
* APPLIQUER ERREURS LÉGÈRES
* @param {string} content - Contenu à modifier
* @param {number} maxErrors - Maximum erreurs légères (défaut: 3)
* @param {object} tracker - HumanSimulationTracker instance
* @returns {object} - { content, errorsApplied, errorTypes }
*/
function applyErrorsLegeres(content, maxErrors = 3, tracker = null) {
let modified = content;
let errorsApplied = 0;
const errorTypes = [];
// Vérifier avec tracker combien d'erreurs légères déjà appliquées
if (tracker && tracker.legereErrorsApplied >= maxErrors) {
logSh(`🚫 Erreurs légères bloquées: déjà ${tracker.legereErrorsApplied} erreur(s) dans cet article`, 'DEBUG');
return { content: modified, errorsApplied: 0, errorTypes: [] };
}
// Appliquer jusqu'à maxErrors
const availableErrors = Object.keys(ERREURS_LEGERES);
while (errorsApplied < maxErrors && availableErrors.length > 0) {
// Sélectionner type aléatoire
const randomIndex = Math.floor(Math.random() * availableErrors.length);
const selectedType = availableErrors[randomIndex];
const errorDefinition = ERREURS_LEGERES[selectedType];
logSh(`🎲 Tentative erreur légère: ${errorDefinition.name}`, 'DEBUG');
// Appliquer
const result = errorDefinition.apply(modified);
if (result.applied) {
modified = result.content;
errorsApplied++;
errorTypes.push(selectedType);
logSh(`✅ Erreur légère appliquée: ${errorDefinition.name}`, 'DEBUG');
// Marquer dans tracker
if (tracker) {
tracker.legereErrorsApplied = (tracker.legereErrorsApplied || 0) + 1;
}
}
// Retirer de la liste pour éviter doublon
availableErrors.splice(randomIndex, 1);
}
return { content: modified, errorsApplied, errorTypes };
}
/**
* OBTENIR STATISTIQUES ERREURS LÉGÈRES
*/
function getErrorLegereStats() {
return {
totalTypes: Object.keys(ERREURS_LEGERES).length,
types: Object.keys(ERREURS_LEGERES),
globalProbability: '50%',
maxPerArticle: 3
};
}
// ============= EXPORTS =============
module.exports = {
ERREURS_LEGERES,
applyErrorsLegeres,
getErrorLegereStats
};

View File

@ -0,0 +1,253 @@
// ========================================
// FICHIER: ErrorMoyenne.js
// RESPONSABILITÉ: Erreurs MOYENNES (30% articles)
// Subtiles mais détectables - erreurs d'inattention
// ========================================
const { logSh } = require('../../ErrorReporting');
/**
* DÉFINITIONS ERREURS MOYENNES
* Probabilité globale: 30% des articles
* Maximum: 2 erreurs moyennes par article
*/
const ERREURS_MOYENNES = {
// ========================================
// ACCORD PLURIEL OUBLIÉ
// ========================================
accord_pluriel: {
name: 'Accord pluriel oublié',
probability: 0.25,
examples: [
{ pattern: 'les plaques résistantes', error: 'les plaques résistant' },
{ pattern: 'des matériaux durables', error: 'des matériaux durable' }
],
apply: (content) => {
let modified = content;
let applied = false;
// Supprimer 's' final sur adjectif après "les" ou "des"
const patterns = [
{ from: /\bles plaques résistantes\b/gi, to: 'les plaques résistant' },
{ from: /\bdes matériaux durables\b/gi, to: 'des matériaux durable' },
{ from: /\bdes solutions efficaces\b/gi, to: 'des solutions efficace' },
{ from: /\bdes produits innovants\b/gi, to: 'des produits innovant' }
];
for (const pattern of patterns) {
if (applied) break;
if (modified.match(pattern.from) && Math.random() < 0.4) {
modified = modified.replace(pattern.from, pattern.to);
applied = true;
logSh(` ⚠️ Erreur moyenne: accord pluriel oublié`, 'DEBUG');
break;
}
}
return { content: modified, applied };
}
},
// ========================================
// VIRGULE MANQUANTE
// ========================================
virgule_manquante: {
name: 'Virgule manquante',
probability: 0.30,
examples: [
{ pattern: 'Ainsi, il est', error: 'Ainsi il est' },
{ pattern: 'Par conséquent, nous', error: 'Par conséquent nous' }
],
apply: (content) => {
let modified = content;
let applied = false;
// Supprimer virgule après connecteurs
const patterns = [
{ from: /\bAinsi, /gi, to: 'Ainsi ' },
{ from: /\bPar conséquent, /gi, to: 'Par conséquent ' },
{ from: /\bToutefois, /gi, to: 'Toutefois ' },
{ from: /\bCependant, /gi, to: 'Cependant ' },
{ from: /\bEn effet, /gi, to: 'En effet ' }
];
for (const pattern of patterns) {
if (applied) break;
if (modified.match(pattern.from) && Math.random() < 0.5) {
modified = modified.replace(pattern.from, pattern.to);
applied = true;
logSh(` ⚠️ Erreur moyenne: virgule manquante après connecteur`, 'DEBUG');
break;
}
}
return { content: modified, applied };
}
},
// ========================================
// CHANGEMENT REGISTRE INAPPROPRIÉ
// ========================================
registre_changement: {
name: 'Changement registre inapproprié',
probability: 0.20,
examples: [
{ pattern: 'Par conséquent', error: 'Du coup' },
{ pattern: 'toutefois', error: 'mais bon' }
],
apply: (content) => {
let modified = content;
let applied = false;
// Remplacer formel par familier
const patterns = [
{ from: /\bPar conséquent\b/g, to: 'Du coup' },
{ from: /\btoutefois\b/gi, to: 'mais bon' },
{ from: /\bnéanmoins\b/gi, to: 'quand même' }
];
for (const pattern of patterns) {
if (applied) break;
if (modified.match(pattern.from) && Math.random() < 0.4) {
modified = modified.replace(pattern.from, pattern.to);
applied = true;
logSh(` ⚠️ Erreur moyenne: registre inapproprié (formel → familier)`, 'DEBUG');
break;
}
}
return { content: modified, applied };
}
},
// ========================================
// PRÉPOSITION INCORRECTE
// ========================================
preposition_incorrecte: {
name: 'Préposition incorrecte',
probability: 0.15,
examples: [
{ pattern: 'résistant aux intempéries', error: 'résistant des intempéries' },
{ pattern: 'adapté à vos besoins', error: 'adapté pour vos besoins' }
],
apply: (content) => {
let modified = content;
let applied = false;
// Remplacer préposition correcte par incorrecte
const patterns = [
{ from: /\brésistant aux intempéries\b/gi, to: 'résistant des intempéries' },
{ from: /\badapté à vos besoins\b/gi, to: 'adapté pour vos besoins' },
{ from: /\bconçu pour résister\b/gi, to: 'conçu à résister' }
];
for (const pattern of patterns) {
if (applied) break;
if (modified.match(pattern.from) && Math.random() < 0.5) {
modified = modified.replace(pattern.from, pattern.to);
applied = true;
logSh(` ⚠️ Erreur moyenne: préposition incorrecte`, 'DEBUG');
break;
}
}
return { content: modified, applied };
}
},
// ========================================
// CONNECTEUR INAPPROPRIÉ
// ========================================
connecteur_inapproprie: {
name: 'Connecteur logique inapproprié',
probability: 0.10,
examples: [
{ pattern: 'et donc', error: 'et mais' },
{ pattern: 'cependant il faut', error: 'donc il faut' }
],
apply: (content) => {
let modified = content;
let applied = false;
// Remplacer connecteur par un illogique
if (modified.includes('cependant') && Math.random() < 0.5) {
modified = modified.replace(/\bcependant\b/i, 'donc');
applied = true;
logSh(` ⚠️ Erreur moyenne: connecteur inapproprié "cependant" → "donc"`, 'DEBUG');
}
return { content: modified, applied };
}
}
};
/**
* APPLIQUER ERREURS MOYENNES
* @param {string} content - Contenu à modifier
* @param {number} maxErrors - Maximum erreurs moyennes (défaut: 2)
* @param {object} tracker - HumanSimulationTracker instance
* @returns {object} - { content, errorsApplied, errorTypes }
*/
function applyErrorsMoyennes(content, maxErrors = 2, tracker = null) {
let modified = content;
let errorsApplied = 0;
const errorTypes = [];
// Vérifier avec tracker combien d'erreurs moyennes déjà appliquées
if (tracker && tracker.moyenneErrorsApplied >= maxErrors) {
logSh(`🚫 Erreurs moyennes bloquées: déjà ${tracker.moyenneErrorsApplied} erreur(s) dans cet article`, 'DEBUG');
return { content: modified, errorsApplied: 0, errorTypes: [] };
}
// Appliquer jusqu'à maxErrors
const availableErrors = Object.keys(ERREURS_MOYENNES);
while (errorsApplied < maxErrors && availableErrors.length > 0) {
// Sélectionner type aléatoire
const randomIndex = Math.floor(Math.random() * availableErrors.length);
const selectedType = availableErrors[randomIndex];
const errorDefinition = ERREURS_MOYENNES[selectedType];
logSh(`🎲 Tentative erreur moyenne: ${errorDefinition.name}`, 'DEBUG');
// Appliquer
const result = errorDefinition.apply(modified);
if (result.applied) {
modified = result.content;
errorsApplied++;
errorTypes.push(selectedType);
logSh(`✅ Erreur moyenne appliquée: ${errorDefinition.name}`, 'DEBUG');
// Marquer dans tracker
if (tracker) {
tracker.moyenneErrorsApplied = (tracker.moyenneErrorsApplied || 0) + 1;
}
}
// Retirer de la liste pour éviter doublon
availableErrors.splice(randomIndex, 1);
}
return { content: modified, errorsApplied, errorTypes };
}
/**
* OBTENIR STATISTIQUES ERREURS MOYENNES
*/
function getErrorMoyenneStats() {
return {
totalTypes: Object.keys(ERREURS_MOYENNES).length,
types: Object.keys(ERREURS_MOYENNES),
globalProbability: '30%',
maxPerArticle: 2
};
}
// ============= EXPORTS =============
module.exports = {
ERREURS_MOYENNES,
applyErrorsMoyennes,
getErrorMoyenneStats
};

View File

@ -0,0 +1,258 @@
// ========================================
// FICHIER: ErrorProfiles.js
// RESPONSABILITÉ: Définitions des profils d'erreurs par gravité
// Système procédural intelligent avec probabilités graduées
// ========================================
const { logSh } = require('../../ErrorReporting');
/**
* PROFILS D'ERREURS PAR GRAVITÉ
* Système à 3 niveaux : Légère (50%) Moyenne (30%) Grave (10%)
*/
const ERROR_SEVERITY_PROFILES = {
// ========================================
// ERREURS GRAVES (10% articles max - ULTRA RARE)
// ========================================
grave: {
name: 'Erreurs Graves',
globalProbability: 0.10, // 10% des articles
maxPerArticle: 1, // MAX 1 erreur grave par article
weight: 10, // Poids pour scoring
description: 'Erreurs visibles mais crédibles - fatigué/distrait',
conditions: {
minArticleLength: 300, // Seulement textes longs
maxPerBatch: 0.10, // Max 10% du batch
avoidTechnical: true // Éviter contenus techniques
},
types: [
'accord_sujet_verbe',
'mot_manquant',
'double_mot',
'negation_oubliee'
]
},
// ========================================
// ERREURS MOYENNES (30% articles)
// ========================================
moyenne: {
name: 'Erreurs Moyennes',
globalProbability: 0.30, // 30% des articles
maxPerArticle: 2, // MAX 2 erreurs moyennes
weight: 5, // Poids moyen
description: 'Erreurs subtiles mais détectables',
conditions: {
minArticleLength: 150, // Textes moyens+
maxPerBatch: 0.30, // Max 30% du batch
avoidTechnical: false
},
types: [
'accord_pluriel',
'virgule_manquante',
'registre_changement',
'preposition_incorrecte',
'connecteur_inapproprie'
]
},
// ========================================
// ERREURS LÉGÈRES (50% articles)
// ========================================
legere: {
name: 'Erreurs Légères',
globalProbability: 0.50, // 50% des articles
maxPerArticle: 3, // MAX 3 erreurs légères
weight: 1, // Poids faible
description: 'Micro-erreurs très subtiles - quasi indétectables',
conditions: {
minArticleLength: 50, // Tous textes
maxPerBatch: 0.50, // Max 50% du batch
avoidTechnical: false
},
types: [
'double_espace',
'trait_union_oublie',
'espace_avant_ponctuation',
'majuscule_incorrecte',
'apostrophe_droite'
]
}
};
/**
* CARACTÉRISTIQUES DE TEXTE POUR SÉLECTION PROCÉDURALE
*/
const TEXT_CHARACTERISTICS = {
// Longueur texte
length: {
short: { min: 0, max: 150, errorMultiplier: 0.7 }, // Moins d'erreurs
medium: { min: 150, max: 500, errorMultiplier: 1.0 }, // Normal
long: { min: 500, max: Infinity, errorMultiplier: 1.3 } // Plus d'erreurs (fatigue)
},
// Complexité technique
technical: {
high: { keywords: ['technique', 'système', 'processus', 'méthode'], errorMultiplier: 0.5 },
medium: { keywords: ['qualité', 'standard', 'professionnel'], errorMultiplier: 1.0 },
low: { keywords: ['simple', 'facile', 'pratique'], errorMultiplier: 1.2 }
},
// Période temporelle (heure)
temporal: {
morning: { hours: [6, 11], errorMultiplier: 0.8 }, // Moins fatigué
afternoon: { hours: [12, 17], errorMultiplier: 1.0 }, // Normal
evening: { hours: [18, 23], errorMultiplier: 1.2 }, // Légère fatigue
night: { hours: [0, 5], errorMultiplier: 1.5 } // Très fatigué
}
};
/**
* OBTENIR PROFIL PAR GRAVITÉ
* @param {string} severity - 'grave', 'moyenne', 'legere'
* @returns {object} - Profil d'erreur
*/
function getErrorProfile(severity) {
const profile = ERROR_SEVERITY_PROFILES[severity.toLowerCase()];
if (!profile) {
logSh(`⚠️ Profil erreur non trouvé: ${severity}`, 'WARNING');
return ERROR_SEVERITY_PROFILES.legere; // Fallback
}
return profile;
}
/**
* DÉTERMINER GRAVITÉ SELON PROBABILITÉ
* Tire aléatoirement selon distribution : 50% légère, 30% moyenne, 10% grave
* @returns {string|null} - 'grave', 'moyenne', 'legere' ou null (pas d'erreur)
*/
function determineSeverityLevel() {
const roll = Math.random();
// 10% chance erreur grave
if (roll < 0.10) {
logSh(`🎲 Gravité déterminée: GRAVE (roll: ${roll.toFixed(3)})`, 'DEBUG');
return 'grave';
}
// 30% chance erreur moyenne (10% + 30% = 40%)
if (roll < 0.40) {
logSh(`🎲 Gravité déterminée: MOYENNE (roll: ${roll.toFixed(3)})`, 'DEBUG');
return 'moyenne';
}
// 50% chance erreur légère (40% + 50% = 90%)
if (roll < 0.90) {
logSh(`🎲 Gravité déterminée: LÉGÈRE (roll: ${roll.toFixed(3)})`, 'DEBUG');
return 'legere';
}
// 10% chance aucune erreur
logSh(`🎲 Aucune erreur pour cet article (roll: ${roll.toFixed(3)})`, 'DEBUG');
return null;
}
/**
* ANALYSER CARACTÉRISTIQUES TEXTE
* @param {string} content - Contenu à analyser
* @param {number} currentHour - Heure actuelle
* @returns {object} - Caractéristiques détectées
*/
function analyzeTextCharacteristics(content, currentHour = new Date().getHours()) {
const wordCount = content.split(/\s+/).length;
const contentLower = content.toLowerCase();
// Déterminer longueur
let lengthCategory = 'medium';
let lengthMultiplier = 1.0;
for (const [category, config] of Object.entries(TEXT_CHARACTERISTICS.length)) {
if (wordCount >= config.min && wordCount < config.max) {
lengthCategory = category;
lengthMultiplier = config.errorMultiplier;
break;
}
}
// Déterminer complexité technique
let technicalCategory = 'medium';
let technicalMultiplier = 1.0;
for (const [category, config] of Object.entries(TEXT_CHARACTERISTICS.technical)) {
const keywordMatches = config.keywords.filter(kw => contentLower.includes(kw)).length;
if (keywordMatches >= 2) {
technicalCategory = category;
technicalMultiplier = config.errorMultiplier;
break;
}
}
// Déterminer période temporelle
let temporalCategory = 'afternoon';
let temporalMultiplier = 1.0;
for (const [category, config] of Object.entries(TEXT_CHARACTERISTICS.temporal)) {
if (currentHour >= config.hours[0] && currentHour <= config.hours[1]) {
temporalCategory = category;
temporalMultiplier = config.errorMultiplier;
break;
}
}
// Multiplicateur global
const globalMultiplier = lengthMultiplier * technicalMultiplier * temporalMultiplier;
logSh(`📊 Caractéristiques: longueur=${lengthCategory}, technique=${technicalCategory}, temporel=${temporalCategory}, mult=${globalMultiplier.toFixed(2)}`, 'DEBUG');
return {
wordCount,
lengthCategory,
lengthMultiplier,
technicalCategory,
technicalMultiplier,
temporalCategory,
temporalMultiplier,
globalMultiplier
};
}
/**
* VÉRIFIER SI ERREUR AUTORISÉE SELON CONDITIONS
* @param {string} severity - Gravité
* @param {object} textCharacteristics - Caractéristiques texte
* @returns {boolean} - true si autorisée
*/
function isErrorAllowed(severity, textCharacteristics) {
const profile = getErrorProfile(severity);
// Vérifier longueur minimale
if (textCharacteristics.wordCount < profile.conditions.minArticleLength) {
logSh(`🚫 Erreur ${severity} bloquée: texte trop court (${textCharacteristics.wordCount} mots < ${profile.conditions.minArticleLength})`, 'DEBUG');
return false;
}
// Vérifier si contenu technique et grave
if (profile.conditions.avoidTechnical && textCharacteristics.technicalCategory === 'high') {
logSh(`🚫 Erreur ${severity} bloquée: contenu trop technique`, 'DEBUG');
return false;
}
return true;
}
// ============= EXPORTS =============
module.exports = {
ERROR_SEVERITY_PROFILES,
TEXT_CHARACTERISTICS,
getErrorProfile,
determineSeverityLevel,
analyzeTextCharacteristics,
isErrorAllowed
};

View File

@ -0,0 +1,161 @@
// ========================================
// FICHIER: ErrorSelector.js
// RESPONSABILITÉ: Sélection procédurale intelligente des erreurs
// Orchestrateur qui décide quelles erreurs appliquer selon contexte
// ========================================
const { logSh } = require('../../ErrorReporting');
const {
determineSeverityLevel,
analyzeTextCharacteristics,
isErrorAllowed,
getErrorProfile
} = require('./ErrorProfiles');
const { applyErrorGrave } = require('./ErrorGrave');
const { applyErrorsMoyennes } = require('./ErrorMoyenne');
const { applyErrorsLegeres } = require('./ErrorLegere');
/**
* SÉLECTION ET APPLICATION ERREURS PROCÉDURALES
* Orchestrateur principal du système d'erreurs graduées
* @param {string} content - Contenu à modifier
* @param {object} options - Options { currentHour, tracker }
* @returns {object} - { content, errorsApplied, errorDetails }
*/
function selectAndApplyErrors(content, options = {}) {
const { currentHour, tracker } = options;
logSh('🎲 SÉLECTION PROCÉDURALE ERREURS - Début', 'INFO');
// ========================================
// 1. ANALYSER CARACTÉRISTIQUES TEXTE
// ========================================
const textCharacteristics = analyzeTextCharacteristics(content, currentHour);
logSh(`📊 Analyse: ${textCharacteristics.wordCount} mots, ${textCharacteristics.lengthCategory}, ${textCharacteristics.technicalCategory}, mult=${textCharacteristics.globalMultiplier.toFixed(2)}`, 'DEBUG');
// ========================================
// 2. DÉTERMINER NIVEAU GRAVITÉ
// ========================================
const severityLevel = determineSeverityLevel();
// Pas d'erreur pour cet article
if (!severityLevel) {
logSh('✅ Aucune erreur sélectionnée pour cet article (10% roll)', 'INFO');
return {
content,
errorsApplied: 0,
errorDetails: {
severity: null,
types: [],
characteristics: textCharacteristics
}
};
}
// ========================================
// 3. VÉRIFIER SI ERREUR AUTORISÉE
// ========================================
if (!isErrorAllowed(severityLevel, textCharacteristics)) {
logSh(`🚫 Erreur ${severityLevel} bloquée par conditions`, 'INFO');
return {
content,
errorsApplied: 0,
errorDetails: {
severity: severityLevel,
blocked: true,
reason: 'conditions_not_met',
characteristics: textCharacteristics
}
};
}
// ========================================
// 4. APPLIQUER ERREURS SELON GRAVITÉ
// ========================================
let modifiedContent = content;
let totalErrorsApplied = 0;
const errorTypes = [];
const profile = getErrorProfile(severityLevel);
logSh(`🎯 Application erreurs: ${profile.name} (max: ${profile.maxPerArticle})`, 'INFO');
switch (severityLevel) {
case 'grave':
// MAX 1 erreur grave
const graveResult = applyErrorGrave(modifiedContent, tracker);
if (graveResult.applied) {
modifiedContent = graveResult.content;
totalErrorsApplied++;
errorTypes.push(graveResult.errorType);
logSh(`✅ 1 erreur GRAVE appliquée: ${graveResult.errorType}`, 'INFO');
}
break;
case 'moyenne':
// MAX 2 erreurs moyennes
const moyenneResult = applyErrorsMoyennes(modifiedContent, 2, tracker);
modifiedContent = moyenneResult.content;
totalErrorsApplied = moyenneResult.errorsApplied;
errorTypes.push(...moyenneResult.errorTypes);
logSh(`${moyenneResult.errorsApplied} erreur(s) MOYENNE(s) appliquée(s)`, 'INFO');
break;
case 'legere':
// MAX 3 erreurs légères
const legereResult = applyErrorsLegeres(modifiedContent, 3, tracker);
modifiedContent = legereResult.content;
totalErrorsApplied = legereResult.errorsApplied;
errorTypes.push(...legereResult.errorTypes);
logSh(`${legereResult.errorsApplied} erreur(s) LÉGÈRE(s) appliquée(s)`, 'INFO');
break;
}
// ========================================
// 5. RETOURNER RÉSULTATS
// ========================================
logSh(`🎲 SÉLECTION PROCÉDURALE ERREURS - Terminé: ${totalErrorsApplied} erreur(s)`, 'INFO');
return {
content: modifiedContent,
errorsApplied: totalErrorsApplied,
errorDetails: {
severity: severityLevel,
profile: profile.name,
types: errorTypes,
characteristics: textCharacteristics,
blocked: false
}
};
}
/**
* STATISTIQUES SYSTÈME ERREURS
* @returns {object} - Stats complètes
*/
function getErrorSystemStats() {
return {
severityLevels: {
grave: { probability: '10%', maxPerArticle: 1 },
moyenne: { probability: '30%', maxPerArticle: 2 },
legere: { probability: '50%', maxPerArticle: 3 },
none: { probability: '10%' }
},
characteristics: {
length: ['short', 'medium', 'long'],
technical: ['high', 'medium', 'low'],
temporal: ['morning', 'afternoon', 'evening', 'night']
},
totalErrorTypes: {
grave: 4,
moyenne: 5,
legere: 5
}
};
}
// ============= EXPORTS =============
module.exports = {
selectAndApplyErrors,
getErrorSystemStats
};

View File

@ -116,6 +116,12 @@ class ManualServer {
logSh('⏱️ [4/7] Démarrage WebSocket serveur...', 'INFO');
const t4 = Date.now();
await this.setupWebSocketServer();
// ✅ PHASE 3: Injecter WebSocket server dans APIController
if (this.wsServer) {
this.apiController.setWebSocketServer(this.wsServer);
}
logSh(`✓ WebSocket démarré en ${Date.now() - t4}ms`, 'INFO');
// 5. Démarrage serveur HTTP
@ -927,6 +933,30 @@ class ManualServer {
await this.apiController.executeConfigurableWorkflow(req, res);
});
// === VALIDATION API (PHASE 3) ===
this.app.post('/api/validation/start', async (req, res) => {
await this.apiController.startValidation(req, res);
});
this.app.get('/api/validation/status/:id', async (req, res) => {
await this.apiController.getValidationStatus(req, res);
});
this.app.post('/api/validation/stop/:id', async (req, res) => {
await this.apiController.stopValidation(req, res);
});
this.app.get('/api/validation/list', async (req, res) => {
await this.apiController.listValidations(req, res);
});
this.app.get('/api/validation/:id/report', async (req, res) => {
await this.apiController.getValidationReport(req, res);
});
this.app.get('/api/validation/:id/evaluations', async (req, res) => {
await this.apiController.getValidationEvaluations(req, res);
});
// ✅ NOUVEAU: Presets validation
this.app.get('/api/validation/presets', async (req, res) => {
await this.apiController.getValidationPresets(req, res);
});
// Gestion d'erreurs API
this.app.use('/api/*', (error, req, res, next) => {
logSh(`❌ Erreur API ${req.path}: ${error.message}`, 'ERROR');

View File

@ -60,7 +60,9 @@ const AVAILABLE_MODULES = {
parameters: {
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'gpt-4o-mini' },
skipAnalysis: { type: 'boolean', default: false, description: 'Passer l\'analyse (mode legacy)' },
layersOrder: { type: 'array', default: ['technical', 'style', 'readability'], description: 'Ordre d\'application des couches' }
layersOrder: { type: 'array', default: ['technical', 'style', 'readability'], description: 'Ordre d\'application des couches' },
charsPerExpression: { type: 'number', min: 1000, max: 10000, default: 4000, description: 'Caractères par expression familière (budget dynamique)' },
personalityName: { type: 'string', required: false, description: 'Nom de la personnalité à utiliser (ex: "Sophie", "Marc"). Si non spécifié, utilise celle du csvData.' }
}
},
adversarial: {

View File

@ -13,6 +13,8 @@ const { extractElements, buildSmartHierarchy } = require('../ElementExtraction')
const { generateMissingKeywords, generateMissingSheetVariables } = require('../MissingKeywords');
const { injectGeneratedContent } = require('../ContentAssembly');
const { saveGeneratedArticleOrganic } = require('../ArticleStorage');
const fs = require('fs').promises;
const path = require('path');
// Modules d'exécution
const { generateSimple } = require('../selective-enhancement/SelectiveUtils');
@ -38,6 +40,7 @@ class PipelineExecutor {
this.parentArticleId = null; // ✅ ID parent pour versioning
this.csvData = null; // ✅ Données CSV pour sauvegarde
this.finalElements = null; // ✅ Éléments extraits pour assemblage
this.versionPaths = []; // ✅ NOUVEAU: Chemins des versions JSON locales
this.metadata = {
startTime: null,
endTime: null,
@ -64,6 +67,13 @@ class PipelineExecutor {
this.checkpoints = [];
this.versionHistory = []; // ✅ Reset version history
this.parentArticleId = null; // ✅ Reset parent ID
this.versionPaths = []; // ✅ Reset version paths
// ✅ NOUVEAU: Créer outputDir si saveAllVersions activé
if (options.saveAllVersions && options.outputDir) {
await fs.mkdir(options.outputDir, { recursive: true });
logSh(`📁 Dossier versions créé: ${options.outputDir}`, 'DEBUG');
}
// Charger les données
const csvData = await this.loadData(rowNumber);
@ -114,6 +124,11 @@ class PipelineExecutor {
await this.saveStepVersion(step, result.modifications || 0, pipelineConfig.name);
}
// ✅ NOUVEAU: Sauvegarde JSON locale si saveAllVersions activé
if (options.saveAllVersions && options.outputDir && this.currentContent) {
await this.saveVersionJSON(step, i, options.outputDir);
}
logSh(`✔ Étape ${step.step} terminée (${stepDuration}ms, ${result.modifications || 0} modifs)`, 'INFO');
} catch (error) {
@ -138,6 +153,14 @@ class PipelineExecutor {
this.metadata.endTime = Date.now();
this.metadata.totalDuration = this.metadata.endTime - this.metadata.startTime;
// ✅ NOUVEAU: Sauvegarder version finale v2.0 si saveAllVersions activé
if (options.saveAllVersions && options.outputDir && this.currentContent) {
const finalVersionPath = path.join(options.outputDir, 'v2.0.json');
await fs.writeFile(finalVersionPath, JSON.stringify(this.currentContent, null, 2), 'utf8');
this.versionPaths.push(finalVersionPath);
logSh(`💾 Version finale v2.0 sauvegardée: ${finalVersionPath}`, 'DEBUG');
}
logSh(`✅ Pipeline terminé: ${this.metadata.totalDuration}ms`, 'INFO');
return {
@ -146,6 +169,7 @@ class PipelineExecutor {
executionLog: this.executionLog,
checkpoints: this.checkpoints,
versionHistory: this.versionHistory, // ✅ Inclure version history
versionPaths: this.versionPaths, // ✅ NOUVEAU: Chemins des versions JSON
metadata: {
...this.metadata,
pipelineName: pipelineConfig.name,
@ -314,6 +338,20 @@ class PipelineExecutor {
logSh(`🧠 SMART TOUCH: Mode ${step.mode}, LLM: ${llmProvider}`, 'INFO');
// ✅ NOUVEAU: Charger personnalité spécifique si demandée
let effectiveCsvData = { ...csvData };
if (step.parameters?.personalityName) {
const personalities = await getPersonalities();
const requestedPersonality = personalities.find(p => p.nom === step.parameters.personalityName);
if (requestedPersonality) {
effectiveCsvData.personality = requestedPersonality;
logSh(`🎭 Personnalité override: ${requestedPersonality.nom} (au lieu de ${csvData.personality?.nom})`, 'INFO');
} else {
logSh(`⚠️ Personnalité "${step.parameters.personalityName}" non trouvée, utilisation de ${csvData.personality?.nom}`, 'WARN');
}
}
// Instancier SmartTouchCore
const smartTouch = new SmartTouchCore();
@ -321,10 +359,11 @@ class PipelineExecutor {
const config = {
mode: step.mode || 'full', // full, analysis_only, technical_only, style_only, readability_only
intensity: step.intensity || 1.0,
csvData,
csvData: effectiveCsvData, // ✅ Utiliser csvData avec personnalité potentiellement overridée
llmProvider: llmProvider, // ✅ Passer le LLM choisi dans pipeline
skipAnalysis: step.parameters?.skipAnalysis || false,
layersOrder: step.parameters?.layersOrder || ['technical', 'style', 'readability']
layersOrder: step.parameters?.layersOrder || ['technical', 'style', 'readability'],
charsPerExpression: step.parameters?.charsPerExpression || 4000 // ✅ NOUVEAU: Budget dynamique
};
// Exécuter SmartTouch
@ -532,6 +571,7 @@ class PipelineExecutor {
this.parentArticleId = null;
this.csvData = null;
this.finalElements = null;
this.versionPaths = []; // ✅ NOUVEAU: Reset version paths
this.metadata = {
startTime: null,
endTime: null,
@ -540,6 +580,35 @@ class PipelineExecutor {
};
}
/**
* NOUVEAU: Sauvegarde une version JSON locale pour Pipeline Validator
*/
async saveVersionJSON(step, stepIndex, outputDir) {
try {
// Déterminer le nom de la version
let versionName;
if (step.module === 'generation') {
versionName = 'v1.0'; // Version initiale après génération
} else {
versionName = `v1.${stepIndex + 1}`; // v1.1, v1.2, v1.3...
}
const versionPath = path.join(outputDir, `${versionName}.json`);
// Sauvegarder le contenu actuel en JSON
await fs.writeFile(versionPath, JSON.stringify(this.currentContent, null, 2), 'utf8');
// Ajouter au tableau des versions
this.versionPaths.push(versionPath);
logSh(`💾 Version ${versionName} sauvegardée: ${versionPath}`, 'DEBUG');
} catch (error) {
logSh(`❌ Erreur sauvegarde version JSON: ${error.message}`, 'ERROR');
// Ne pas propager l'erreur pour ne pas bloquer l'exécution
}
}
/**
* Sauvegarde une version intermédiaire dans Google Sheets
*/

View File

@ -0,0 +1,309 @@
// ========================================
// 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 };

View File

@ -37,11 +37,29 @@ class SmartStyleLayer {
};
}
// === 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
intensity,
familiarExpressionsCount: totalFamiliar
});
// ✅ Utiliser LLM fourni dans context, sinon fallback sur defaultLLM
@ -95,7 +113,7 @@ class SmartStyleLayer {
* CRÉER PROMPT CIBLÉ
*/
createTargetedPrompt(content, analysis, context) {
const { mc0, personality, intensity = 1.0 } = context;
const { mc0, personality, intensity = 1.0, budgetManager, currentTag } = context;
// Extraire améliorations style
const styleImprovements = analysis.improvements.filter(imp =>
@ -106,6 +124,28 @@ class SmartStyleLayer {
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:
@ -114,7 +154,7 @@ CONTENU ORIGINAL:
${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)}
INTENSITÉ: ${intensity.toFixed(1)}${budgetConstraints}
AMÉLIORATIONS STYLE À APPLIQUER:
${styleImprovements.map((imp, i) => `${i + 1}. ${imp}`).join('\n')}
@ -210,6 +250,24 @@ Retourne UNIQUEMENT le contenu stylisé, SANS balises, SANS métadonnées, SANS
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 };

View File

@ -194,12 +194,25 @@ ${analysis.technical.missing.map((item, i) => `- ${item}`).join('\n')}
` : ''}
CONSIGNES STRICTES:
- Applique UNIQUEMENT les améliorations listées ci-dessus
RÈGLE ABSOLUE: REMPLACE la phrase originale, N'AJOUTE PAS de texte après
- Reformule la phrase en intégrant les améliorations techniques
- NE CHANGE PAS le ton, style ou structure générale
- NE TOUCHE PAS aux aspects non mentionnés
- Garde la même longueur approximative (±20%)
- Intègre les éléments manquants de façon NATURELLE
- Garde la même longueur approximative (±20%, PAS +100%)
- ${isB2C ? 'PRIORITÉ ABSOLUE: Reste SIMPLE et ACCESSIBLE' : 'Reste ACCESSIBLE - pas de jargon excessif'}
- Si la phrase est une question, garde-la sous forme de question (réponds DANS la question)
EXEMPLES REMPLACER vs AJOUTER:
BON (REMPLACER):
AVANT: "Est-ce que les plaques sont résistantes aux intempéries ?"
APRÈS: "Les plaques en aluminium résistent-elles aux intempéries (-20°C à 50°C) ?"
Phrase REMPLACÉE, même longueur, garde format question
MAUVAIS (AJOUTER):
AVANT: "Est-ce que les plaques sont résistantes aux intempéries ?"
APRÈS: "Est-ce que les plaques sont résistantes aux intempéries ? Les plaques sont en aluminium..."
Texte AJOUTÉ après = INTERDIT
EXEMPLES D'AMÉLIORATION TECHNIQUE (génériques):
${isB2C ? `

View File

@ -10,6 +10,7 @@ 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
@ -40,7 +41,8 @@ class SmartTouchCore {
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
layersOrder = ['technical', 'style', 'readability'], // Ordre d'application personnalisable
charsPerExpression = 4000 // ✅ NOUVEAU: Caractères par expression familière (configurable)
} = config;
await tracer.annotate({
@ -66,6 +68,17 @@ class SmartTouchCore {
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
// ========================================
@ -117,6 +130,41 @@ class SmartTouchCore {
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');
@ -124,32 +172,16 @@ class SmartTouchCore {
let layerModifications = 0;
const layerResults = {};
// Appliquer la couche sur chaque élément
// 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 analysis = analysisResults[tag];
if (!analysis) continue;
const tagData = segmentsByTag[tag];
if (!tagData) continue;
try {
// === SYSTÈME 10% SEGMENTS ===
// Calculer pourcentage de texte à améliorer selon intensity
// intensity 1.0 = 10%, 0.5 = 5%, 1.5 = 15%
const percentageToImprove = intensity * 0.1;
// ✅ Utiliser les segments PRÉ-SÉLECTIONNÉS (pas de nouvelle sélection)
const { segments, weakestSegments, analysis } = tagData;
// Analyser par segments pour identifier les plus faibles
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
);
logSh(` 📊 [${tag}] ${segments.length} segments, ${weakestSegments.length} sélectionnés (${(percentageToImprove * 100).toFixed(0)}%)`, 'DEBUG');
// Appliquer amélioration UNIQUEMENT sur segments sélectionnés
// Appliquer amélioration UNIQUEMENT sur segments déjà sélectionnés
const result = await this.applyLayerToSegments(
layerName,
segments,
@ -160,14 +192,16 @@ class SmartTouchCore {
personality: csvData?.personality,
intensity,
contentContext, // Passer contexte aux layers
llmProvider // ✅ Passer LLM choisi dans pipeline
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;
stats.elementsImproved++;
improvedTags.add(tag); // ✅ FIX: Ajouter au Set (pas de duplicata)
}
layerResults[tag] = result;
@ -190,6 +224,9 @@ class SmartTouchCore {
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');
@ -204,6 +241,20 @@ class SmartTouchCore {
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');
@ -216,7 +267,8 @@ class SmartTouchCore {
content: currentContent,
stats,
modifications: stats.totalModifications,
analysisResults: stats.analysisResults
analysisResults: stats.analysisResults,
budgetReport // ✅ NOUVEAU: Inclure rapport budget
};
} catch (error) {
@ -315,11 +367,19 @@ class SmartTouchCore {
} catch (error) {
logSh(` ⚠️ Échec amélioration segment ${segment.index}: ${error.message}`, 'WARN');
// Fallback: garder segment original
improvedSegments.push({ ...segment, improved: false });
improvedSegments.push({
...segment,
content: segment.content, // ✅ FIX: Copier content original
improved: false
});
}
} else {
// GARDER segment intact
improvedSegments.push({ ...segment, improved: false });
improvedSegments.push({
...segment,
content: segment.content, // ✅ FIX: Copier content original
improved: false
});
}
}

View File

@ -0,0 +1,477 @@
/**
* CriteriaEvaluator.js
*
* Évaluateur multi-critères pour Pipeline Validator
* Évalue la qualité du contenu via LLM selon 5 critères universels
*/
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { callLLM } = require('../LLMManager');
/**
* Définition des 5 critères universels
*/
const CRITERIA = {
qualite: {
id: 'qualite',
name: 'Qualité globale',
description: 'Grammaire, orthographe, syntaxe, cohérence et pertinence contextuelle',
weight: 1.0
},
verbosite: {
id: 'verbosite',
name: 'Verbosité / Concision',
description: 'Densité informationnelle, longueur appropriée, absence de fluff',
weight: 1.0
},
seo: {
id: 'seo',
name: 'SEO et mots-clés',
description: 'Intégration naturelle des mots-clés, structure SEO-friendly',
weight: 1.0
},
repetitions: {
id: 'repetitions',
name: 'Répétitions et variations',
description: 'Variété lexicale, évite répétitions, usage synonymes',
weight: 1.0
},
naturalite: {
id: 'naturalite',
name: 'Naturalité humaine',
description: 'Semble écrit par un humain, évite patterns IA',
weight: 1.5 // Critère le plus important pour SEO anti-détection
}
};
/**
* Classe CriteriaEvaluator
*/
class CriteriaEvaluator {
constructor() {
this.defaultLLM = 'claude-sonnet-4-5'; // Claude pour objectivité
this.temperature = 0.3; // Cohérence entre évaluations
this.maxRetries = 2;
this.evaluationCache = {}; // Cache pour éviter réévaluations inutiles
}
/**
* Évalue un échantillon selon tous les critères à travers toutes les versions
* @param {Object} sample - Échantillon avec versions
* @param {Object} context - Contexte (MC0, T0, personality)
* @param {Array} criteriaFilter - NOUVEAU: Liste des critères à évaluer (optionnel)
* @returns {Object} - Évaluations par critère et version
*/
async evaluateSample(sample, context, criteriaFilter = null) {
return tracer.run('CriteriaEvaluator.evaluateSample', async () => {
logSh(`🎯 Évaluation échantillon: ${sample.tag} (${sample.type})`, 'INFO');
const evaluations = {};
const versionNames = Object.keys(sample.versions);
// ✅ Filtrer critères si spécifié
const criteriaIds = criteriaFilter && criteriaFilter.length > 0
? criteriaFilter.filter(id => CRITERIA[id]) // Valider que le critère existe
: Object.keys(CRITERIA);
// Pour chaque critère
for (const criteriaId of criteriaIds) {
const criteria = CRITERIA[criteriaId];
evaluations[criteriaId] = {};
logSh(` 📊 Critère: ${criteria.name}`, 'DEBUG');
// Pour chaque version
for (const versionName of versionNames) {
const text = sample.versions[versionName];
// Skip si non disponible
if (text === "[Non disponible à cette étape]" || text === "[Erreur lecture]") {
evaluations[criteriaId][versionName] = {
score: null,
reasoning: "Contenu non disponible à cette étape",
skipped: true
};
continue;
}
try {
// Évaluer avec retry
const evaluation = await this.evaluateWithRetry(
text,
criteria,
sample.type,
context,
versionName
);
evaluations[criteriaId][versionName] = evaluation;
logSh(`${versionName}: ${evaluation.score}/10`, 'DEBUG');
} catch (error) {
logSh(`${versionName}: ${error.message}`, 'ERROR');
evaluations[criteriaId][versionName] = {
score: null,
reasoning: `Erreur évaluation: ${error.message}`,
error: true
};
}
}
}
logSh(` ✅ Échantillon évalué: ${Object.keys(CRITERIA).length} critères × ${versionNames.length} versions`, 'INFO');
return evaluations;
}, { tag: sample.tag, type: sample.type });
}
/**
* Évalue avec retry logic
*/
async evaluateWithRetry(text, criteria, type, context, versionName) {
let lastError;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
if (attempt > 0) {
logSh(` 🔄 Retry ${attempt}/${this.maxRetries}...`, 'DEBUG');
}
return await this.evaluate(text, criteria, type, context);
} catch (error) {
lastError = error;
if (attempt < this.maxRetries) {
// Attendre avant retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
}
}
}
throw lastError;
}
/**
* Évalue un texte selon un critère
*/
async evaluate(text, criteria, type, context) {
const prompt = this.buildPrompt(text, criteria, type, context);
// Appel LLM
const response = await callLLM(
this.defaultLLM,
prompt,
this.temperature,
4000 // max tokens
);
// Parser la réponse JSON
const evaluation = this.parseEvaluation(response);
// Valider
this.validateEvaluation(evaluation);
return evaluation;
}
/**
* Construit le prompt d'évaluation structuré
*/
buildPrompt(text, criteria, type, context) {
const { mc0 = '', t0 = '', personality = {} } = context;
// Texte tronqué si trop long (max 2000 chars pour contexte)
const truncatedText = text.length > 2000
? text.substring(0, 2000) + '... [tronqué]'
: text;
return `Tu es un évaluateur objectif de contenu SEO.
CONTEXTE:
- Mot-clé principal: ${mc0}
- Thématique: ${t0}
- Personnalité: ${personality.nom || 'Non spécifiée'}
- Type de contenu: ${type} (title/content/faq)
ÉLÉMENT À ÉVALUER:
"${truncatedText}"
CRITÈRE: ${criteria.name}
Description: ${criteria.description}
${this.getCriteriaPromptDetails(criteria.id, type)}
TÂCHE:
Évalue cet élément selon le critère ci-dessus.
Donne une note de 0 à 10 (précision: 0.5).
Justifie ta notation en 2-3 phrases concrètes.
RÉPONSE ATTENDUE (JSON strict):
{
"score": 7.5,
"reasoning": "Justification détaillée en 2-3 phrases..."
}`;
}
/**
* Obtient les détails spécifiques d'un critère
*/
getCriteriaPromptDetails(criteriaId, type) {
const details = {
qualite: `ÉCHELLE:
10 = Qualité exceptionnelle, aucune faute
7-9 = Bonne qualité, légères imperfections
4-6 = Qualité moyenne, plusieurs problèmes
1-3 = Faible qualité, nombreuses erreurs
0 = Inutilisable
Évalue:
- Grammaire et syntaxe impeccables ?
- Texte fluide et cohérent ?
- Pertinent par rapport au mot-clé "${this.context?.mc0 || 'principal'}" ?`,
verbosite: `ÉCHELLE:
10 = Parfaitement concis, chaque mot compte
7-9 = Plutôt concis, peu de superflu
4-6 = Moyennement verbeux, du remplissage
1-3 = Très verbeux, beaucoup de fluff
0 = Délayage excessif
Évalue:
- Densité informationnelle élevée (info utile / longueur totale) ?
- Longueur appropriée pour un ${type} (ni trop court, ni verbeux) ?
- Absence de fluff et remplissage inutile ?`,
seo: `ÉCHELLE:
10 = SEO optimal et naturel
7-9 = Bon SEO, quelques améliorations possibles
4-6 = SEO moyen, manque d'optimisation ou sur-optimisé
1-3 = SEO faible ou contre-productif
0 = Aucune considération SEO
Évalue:
- Mots-clés (notamment "${this.context?.mc0 || 'principal'}") intégrés naturellement ?
- Densité appropriée (ni trop faible, ni keyword stuffing) ?
- Structure SEO-friendly ?`,
repetitions: `ÉCHELLE:
10 = Très varié, aucune répétition notable
7-9 = Plutôt varié, quelques répétitions mineures
4-6 = Variété moyenne, répétitions visibles
1-3 = Très répétitif, vocabulaire pauvre
0 = Répétitions excessives
Évalue:
- Répétitions de mots/expressions évitées ?
- Vocabulaire varié et riche ?
- Paraphrases et synonymes utilisés intelligemment ?`,
naturalite: `ÉCHELLE:
10 = 100% indétectable, parfaitement humain
7-9 = Très naturel, légères traces IA
4-6 = Moyennement naturel, patterns IA visibles
1-3 = Clairement IA, très artificiel
0 = Robotique et détectable immédiatement
Évalue:
- Semble-t-il rédigé par un humain authentique ?
- Présence de variations naturelles et imperfections réalistes ?
- Absence de patterns IA typiques (phrases trop parfaites, formules creuses, superlatifs excessifs) ?`
};
return details[criteriaId] || '';
}
/**
* Parse la réponse LLM en JSON
*/
parseEvaluation(response) {
try {
// Nettoyer la réponse (enlever markdown si présent)
let cleaned = response.trim();
// Si la réponse contient des backticks, extraire le JSON
if (cleaned.includes('```')) {
const match = cleaned.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
if (match) {
cleaned = match[1];
}
}
// Parser JSON
const parsed = JSON.parse(cleaned);
return {
score: parsed.score,
reasoning: parsed.reasoning
};
} catch (error) {
logSh(`❌ Erreur parsing JSON: ${error.message}`, 'ERROR');
logSh(` Réponse brute: ${response.substring(0, 200)}...`, 'DEBUG');
// Fallback: extraire score et reasoning par regex
return this.fallbackParse(response);
}
}
/**
* Parsing fallback si JSON invalide
*/
fallbackParse(response) {
// Chercher score avec regex
const scoreMatch = response.match(/(?:score|note)[:\s]*([0-9]+(?:\.[0-9]+)?)/i);
const score = scoreMatch ? parseFloat(scoreMatch[1]) : null;
// Chercher reasoning
const reasoningMatch = response.match(/(?:reasoning|justification)[:\s]*"?([^"]+)"?/i);
const reasoning = reasoningMatch ? reasoningMatch[1].trim() : response.substring(0, 200);
logSh(`⚠️ Fallback parsing: score=${score}, reasoning=${reasoning.substring(0, 50)}...`, 'WARN');
return { score, reasoning };
}
/**
* Valide une évaluation
*/
validateEvaluation(evaluation) {
if (evaluation.score === null || evaluation.score === undefined) {
throw new Error('Score manquant dans évaluation');
}
if (evaluation.score < 0 || evaluation.score > 10) {
throw new Error(`Score invalide: ${evaluation.score} (doit être entre 0 et 10)`);
}
if (!evaluation.reasoning || evaluation.reasoning.length < 10) {
throw new Error('Reasoning manquant ou trop court');
}
}
/**
* Évalue plusieurs échantillons en parallèle (avec limite de concurrence)
* @param {Object} samples - Échantillons à évaluer
* @param {Object} context - Contexte
* @param {number} maxConcurrent - Limite concurrence
* @param {Array} criteriaFilter - NOUVEAU: Filtrer critères (optionnel)
*/
async evaluateBatch(samples, context, maxConcurrent = 3, criteriaFilter = null) {
return tracer.run('CriteriaEvaluator.evaluateBatch', async () => {
const criteriaInfo = criteriaFilter && criteriaFilter.length > 0
? criteriaFilter.join(', ')
: 'tous critères';
logSh(`🎯 Évaluation batch: ${Object.keys(samples).length} échantillons (concurrence: ${maxConcurrent}, critères: ${criteriaInfo})`, 'INFO');
const results = {};
const sampleEntries = Object.entries(samples);
// Traiter par batch pour limiter concurrence
for (let i = 0; i < sampleEntries.length; i += maxConcurrent) {
const batch = sampleEntries.slice(i, i + maxConcurrent);
logSh(` 📦 Batch ${Math.floor(i / maxConcurrent) + 1}/${Math.ceil(sampleEntries.length / maxConcurrent)}: ${batch.length} échantillons`, 'INFO');
// Évaluer en parallèle dans le batch
const batchPromises = batch.map(async ([tag, sample]) => {
const evaluations = await this.evaluateSample(sample, context, criteriaFilter); // ✅ Passer filtre
return [tag, evaluations];
});
const batchResults = await Promise.all(batchPromises);
// Ajouter aux résultats
batchResults.forEach(([tag, evaluations]) => {
results[tag] = evaluations;
});
}
logSh(`✅ Batch évaluation terminée: ${Object.keys(results).length} échantillons évalués`, 'INFO');
return results;
}, { samplesCount: Object.keys(samples).length, maxConcurrent });
}
/**
* Calcule les scores moyens par version
*/
aggregateScores(evaluations) {
const aggregated = {
byVersion: {},
byCriteria: {},
overall: { avgScore: 0, totalEvaluations: 0 }
};
// Collecter tous les scores par version
const versionScores = {};
const criteriaScores = {};
for (const [tag, sampleEvals] of Object.entries(evaluations)) {
for (const [criteriaId, versionEvals] of Object.entries(sampleEvals)) {
if (!criteriaScores[criteriaId]) {
criteriaScores[criteriaId] = [];
}
for (const [versionName, evaluation] of Object.entries(versionEvals)) {
if (evaluation.score !== null && !evaluation.skipped && !evaluation.error) {
if (!versionScores[versionName]) {
versionScores[versionName] = [];
}
versionScores[versionName].push(evaluation.score);
criteriaScores[criteriaId].push(evaluation.score);
}
}
}
}
// Calculer moyennes par version
for (const [versionName, scores] of Object.entries(versionScores)) {
const avg = scores.reduce((sum, s) => sum + s, 0) / scores.length;
aggregated.byVersion[versionName] = {
avgScore: Math.round(avg * 10) / 10,
count: scores.length
};
}
// Calculer moyennes par critère
for (const [criteriaId, scores] of Object.entries(criteriaScores)) {
const avg = scores.reduce((sum, s) => sum + s, 0) / scores.length;
aggregated.byCriteria[criteriaId] = {
avgScore: Math.round(avg * 10) / 10,
count: scores.length
};
}
// Calculer moyenne globale
const allScores = Object.values(versionScores).flat();
if (allScores.length > 0) {
aggregated.overall.avgScore = Math.round((allScores.reduce((sum, s) => sum + s, 0) / allScores.length) * 10) / 10;
aggregated.overall.totalEvaluations = allScores.length;
}
return aggregated;
}
/**
* Obtient les critères disponibles
*/
static getCriteria() {
return CRITERIA;
}
/**
* Reset le cache
*/
resetCache() {
this.evaluationCache = {};
}
}
module.exports = { CriteriaEvaluator, CRITERIA };

429
lib/validation/README.md Normal file
View File

@ -0,0 +1,429 @@
# Pipeline Validator - Backend Core
## 📋 Description
Système de validation qualitative pour évaluer objectivement l'évolution du contenu généré à travers les différentes étapes d'un pipeline via évaluations LLM.
## 🎯 Fonctionnalités
- **Exécution pipeline** avec sauvegarde automatique de toutes les versions (v1.0, v1.1, v1.2, v2.0)
- **Échantillonnage intelligent** : sélection automatique des balises représentatives
- Tous les titres (balises `T*`)
- 4 contenus principaux (balises `MC*`, `L*`)
- 4 FAQ (balises `FAQ*`)
- **Évaluation LLM** selon 5 critères universels :
1. **Qualité globale** - Grammaire, syntaxe, cohérence
2. **Verbosité** - Concision vs fluff
3. **SEO** - Intégration naturelle des mots-clés
4. **Répétitions** - Variété lexicale
5. **Naturalité humaine** - Détection vs IA
- **Agrégation scores** : moyennes par version et par critère
- **Rapport complet** : JSON structuré avec tous les résultats
## 📁 Architecture
```
lib/validation/
├── ValidatorCore.js # Orchestrateur principal
├── SamplingEngine.js # Échantillonnage automatique
├── CriteriaEvaluator.js # Évaluations LLM multi-critères
├── test-validator.js # Test Phase 1 (sans LLM)
├── test-validator-phase2.js # Test Phase 2 (avec LLM)
└── README.md # Ce fichier
validations/{uuid}/ # Dossier généré par validation
├── config.json # Configuration utilisée
├── report.json # Rapport final
├── versions/
│ ├── v1.0.json # Contenu après génération
│ ├── v1.1.json # Contenu après step 1
│ ├── v1.2.json # Contenu après step 2
│ └── v2.0.json # Contenu final
├── samples/
│ ├── all-samples.json # Échantillons extraits
│ └── summary.json # Résumé échantillons
└── results/
└── evaluations.json # Évaluations LLM complètes
```
## 🚀 Utilisation
### Test Phase 1 (sans évaluations LLM)
```bash
node lib/validation/test-validator.js
```
**Durée** : ~2-3 minutes (génération + échantillonnage)
**Résultat** : Validation avec pipeline exécuté et échantillons extraits, SANS évaluations LLM.
### Test Phase 2 (avec évaluations LLM)
```bash
node lib/validation/test-validator-phase2.js
```
**Durée** : ~3-5 minutes (génération + échantillonnage + évaluations)
**Résultat** : Validation COMPLÈTE avec scores LLM pour chaque échantillon et version.
### Phase 3 (API & WebSocket) - Utilisation REST API
**Démarrer une validation**:
```bash
curl -X POST http://localhost:3000/api/validation/start \
-H "Content-Type: application/json" \
-d '{
"pipelineConfig": {
"name": "Test Pipeline",
"pipeline": [
{ "step": 1, "module": "generation", "mode": "simple", "intensity": 1.0 }
]
},
"rowNumber": 2
}'
```
**Récupérer le statut**:
```bash
curl http://localhost:3000/api/validation/status/{validationId}
```
**Lister toutes les validations**:
```bash
curl http://localhost:3000/api/validation/list
```
**Récupérer le rapport complet**:
```bash
curl http://localhost:3000/api/validation/{validationId}/report
```
**Récupérer les évaluations détaillées**:
```bash
curl http://localhost:3000/api/validation/{validationId}/evaluations
```
### WebSocket - Progression temps réel
Connecter un client WebSocket sur `ws://localhost:8081`:
```javascript
const ws = new WebSocket('ws://localhost:8081');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'validation_progress') {
console.log(`[${data.progress.percentage}%] ${data.progress.phase}: ${data.progress.message}`);
}
};
```
### Utilisation programmatique
```javascript
const { ValidatorCore } = require('./lib/validation/ValidatorCore');
const validator = new ValidatorCore();
const pipelineConfig = {
name: 'Mon Pipeline',
pipeline: [
{ step: 1, module: 'generation', mode: 'simple', intensity: 1.0 },
{ step: 2, module: 'selective', mode: 'lightEnhancement', intensity: 0.5 }
]
};
const result = await validator.runValidation(
{}, // config validation (optionnel)
pipelineConfig, // config pipeline
2 // rowNumber Google Sheets
);
if (result.success) {
console.log('✅ Validation réussie');
console.log(`Score global: ${result.report.evaluations.overallScore}/10`);
console.log(`Dossier: ${result.validationDir}`);
}
```
## 📊 Format du rapport
### Rapport JSON (`report.json`)
```json
{
"validationId": "uuid",
"timestamp": "2025-...",
"status": "completed",
"pipeline": {
"name": "Pipeline name",
"totalSteps": 2,
"successfulSteps": 2,
"totalDuration": 125000
},
"versions": {
"count": 3,
"paths": ["v1.0.json", "v1.1.json", "v2.0.json"]
},
"samples": {
"total": 9,
"titles": 5,
"content": 4,
"faqs": 0
},
"evaluations": {
"totalEvaluations": 135,
"overallScore": 7.3,
"byVersion": {
"v1.0": { "avgScore": 6.5, "count": 45 },
"v1.1": { "avgScore": 7.2, "count": 45 },
"v2.0": { "avgScore": 7.8, "count": 45 }
},
"byCriteria": {
"qualite": { "avgScore": 8.1, "count": 27 },
"verbosite": { "avgScore": 7.5, "count": 27 },
"seo": { "avgScore": 7.8, "count": 27 },
"repetitions": { "avgScore": 7.2, "count": 27 },
"naturalite": { "avgScore": 6.0, "count": 27 }
}
}
}
```
### Évaluations détaillées (`results/evaluations.json`)
```json
{
"evaluations": {
"|T0|": {
"qualite": {
"v1.0": { "score": 8.0, "reasoning": "Titre clair..." },
"v1.1": { "score": 8.5, "reasoning": "Amélioration..." }
},
"naturalite": {
"v1.0": { "score": 6.5, "reasoning": "Légère..." },
"v1.1": { "score": 7.0, "reasoning": "Plus naturel..." }
}
}
},
"aggregated": {
"byVersion": {...},
"byCriteria": {...},
"overall": { "avgScore": 7.3, "totalEvaluations": 135 }
}
}
```
## 🎯 Critères d'évaluation
### 1. Qualité globale (0-10)
- Grammaire, orthographe, syntaxe
- Cohérence et fluidité
- Pertinence contextuelle
### 2. Verbosité / Concision (0-10)
- Densité informationnelle
- Longueur appropriée
- Absence de fluff
### 3. SEO et mots-clés (0-10)
- Intégration naturelle mots-clés
- Densité appropriée
- Structure SEO-friendly
### 4. Répétitions et variations (0-10)
- Évite répétitions lexicales
- Vocabulaire varié
- Usage synonymes
### 5. Naturalité humaine (0-10)
- Semble écrit par humain
- Variations naturelles
- Évite patterns IA
## ⚙️ Configuration
### Limiter concurrence évaluations LLM
Par défaut : 3 évaluations en parallèle.
Modifier dans `ValidatorCore.js` :
```javascript
const evaluations = await this.criteriaEvaluator.evaluateBatch(samples, context, 5);
// ^^^ maxConcurrent
```
### Changer LLM évaluateur
Par défaut : `claude-sonnet-4-5`
Modifier dans `CriteriaEvaluator.js` :
```javascript
this.defaultLLM = 'gpt-4o'; // Ou autre LLM
```
### Désactiver retry logic
Modifier dans `CriteriaEvaluator.js` :
```javascript
this.maxRetries = 0; // Pas de retry
```
## 💰 Coûts estimés
### Configuration standard
- Pipeline : 4 steps → 4 versions
- Échantillons : ~13 balises
- Critères : 5 critères
- **Total appels LLM** : 13 × 5 × 4 = **260 appels**
- **Coût** : ~$1.00 par validation (Claude Sonnet 4.5)
### Configuration économique
- Pipeline : 2 steps → 2 versions
- Échantillons : ~7 balises
- Critères : 3 critères prioritaires
- **Total appels LLM** : 7 × 3 × 2 = **42 appels**
- **Coût** : ~$0.16 par validation
## 🐛 Debugging
### Logs détaillés
Les logs incluent :
- `[INFO]` Progression phases
- `[DEBUG]` Détails échantillonnage et évaluations
- `[ERROR]` Erreurs avec stack trace
### Vérifier contenu versions
```bash
cat validations/{uuid}/versions/v1.0.json | jq
```
### Vérifier échantillons
```bash
cat validations/{uuid}/samples/all-samples.json | jq
```
### Vérifier évaluations
```bash
cat validations/{uuid}/results/evaluations.json | jq '.aggregated'
```
## 🔧 Développement
### Ajouter un nouveau critère
1. Modifier `CRITERIA` dans `CriteriaEvaluator.js`
2. Ajouter prompt détails dans `getCriteriaPromptDetails()`
### Modifier algorithme échantillonnage
Modifier méthodes dans `SamplingEngine.js` :
- `extractSamples()` - Logique principale
- `extractVersionsForTag()` - Extraction versions
### Ajouter nouvelle phase validation
Modifier `ValidatorCore.js` :
- Ajouter méthode `runMyPhase()`
- Insérer appel dans workflow `runValidation()`
- Ajouter sauvegarde dans `generateReport()`
## 🌐 API Endpoints (Phase 3)
### POST /api/validation/start
Démarre une nouvelle validation en arrière-plan.
**Body**:
```json
{
"pipelineConfig": {
"name": "Pipeline Name",
"pipeline": [...]
},
"rowNumber": 2,
"config": {}
}
```
**Response** (202 Accepted):
```json
{
"success": true,
"data": {
"validationId": "uuid-xxx",
"status": "running",
"message": "Validation démarrée en arrière-plan"
}
}
```
### GET /api/validation/status/:id
Récupère le statut et la progression d'une validation.
**Response**:
```json
{
"success": true,
"data": {
"validationId": "uuid-xxx",
"status": "running",
"progress": {
"phase": "evaluation",
"message": "Évaluation LLM des échantillons...",
"percentage": 65
},
"duration": 120000,
"pipelineName": "Test Pipeline",
"result": null
}
}
```
### POST /api/validation/stop/:id
Arrête une validation en cours (marque comme stopped, le processus continue en arrière-plan).
### GET /api/validation/list
Liste toutes les validations (actives + historique).
**Query Params**:
- `status`: Filtrer par statut (running, completed, error)
- `limit`: Nombre max de résultats (défaut: 50)
### GET /api/validation/:id/report
Récupère le rapport complet JSON d'une validation terminée.
### GET /api/validation/:id/evaluations
Récupère les évaluations LLM détaillées avec scores par version et critère.
## 📡 WebSocket Events (Phase 3)
**Type: `validation_progress`**
```json
{
"type": "validation_progress",
"validationId": "uuid-xxx",
"status": "running",
"progress": {
"phase": "pipeline|sampling|evaluation|saving|report",
"message": "Description de l'étape en cours",
"percentage": 65
},
"timestamp": "2025-01-13T..."
}
```
---
**Statut** : ✅ Phase 3 complète et opérationnelle (API + WebSocket)
**Version** : 1.1.0
**Date** : 2025-01-13

View File

@ -0,0 +1,175 @@
/**
* SamplingEngine.js
*
* Moteur d'échantillonnage pour Pipeline Validator
* Extrait automatiquement des échantillons représentatifs du contenu généré
*/
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const fs = require('fs').promises;
const path = require('path');
/**
* Classe SamplingEngine
*/
class SamplingEngine {
constructor() {
this.samples = {
titles: [],
content: [],
faqs: []
};
}
/**
* Extrait les échantillons depuis les versions sauvegardées
* @param {Array<string>} versionPaths - Chemins des fichiers JSON versions
* @returns {Object} - Échantillons avec leurs versions
*/
async extractSamples(versionPaths) {
return tracer.run('SamplingEngine.extractSamples', async () => {
logSh(`📊 Démarrage échantillonnage: ${versionPaths.length} versions`, 'INFO');
// Charger la version finale pour identifier les échantillons
const finalVersionPath = versionPaths.find(p => p.includes('v2.0.json'));
if (!finalVersionPath) {
throw new Error('Version finale v2.0.json introuvable');
}
const finalContent = await this.loadVersion(finalVersionPath);
const allTags = Object.keys(finalContent);
logSh(` 📋 ${allTags.length} balises trouvées dans version finale`, 'DEBUG');
// Catégoriser les balises automatiquement
const titleTags = allTags.filter(tag => tag.includes('T'));
const contentTags = allTags.filter(tag => tag.includes('MC') || tag.includes('L')).slice(0, 4);
const faqTags = allTags.filter(tag => tag.includes('FAQ')).slice(0, 4);
logSh(` ✓ Catégorisation: ${titleTags.length} titres, ${contentTags.length} contenus, ${faqTags.length} FAQ`, 'INFO');
// Extraire versions pour chaque échantillon
const samplesData = {};
// Titres
for (const tag of titleTags) {
samplesData[tag] = await this.extractVersionsForTag(tag, versionPaths);
samplesData[tag].type = 'title';
this.samples.titles.push(tag);
}
// Contenus
for (const tag of contentTags) {
samplesData[tag] = await this.extractVersionsForTag(tag, versionPaths);
samplesData[tag].type = 'content';
this.samples.content.push(tag);
}
// FAQ
for (const tag of faqTags) {
samplesData[tag] = await this.extractVersionsForTag(tag, versionPaths);
samplesData[tag].type = 'faq';
this.samples.faqs.push(tag);
}
const totalSamples = titleTags.length + contentTags.length + faqTags.length;
logSh(`✅ Échantillonnage terminé: ${totalSamples} échantillons extraits`, 'INFO');
return {
samples: samplesData,
summary: {
totalSamples,
titles: titleTags.length,
content: contentTags.length,
faqs: faqTags.length
}
};
}, { versionsCount: versionPaths.length });
}
/**
* Extrait les versions d'une balise à travers toutes les étapes
* @param {string} tag - Balise à extraire
* @param {Array<string>} versionPaths - Chemins des versions
* @returns {Object} - Versions de la balise
*/
async extractVersionsForTag(tag, versionPaths) {
const versions = {};
for (const versionPath of versionPaths) {
try {
const content = await this.loadVersion(versionPath);
const versionName = path.basename(versionPath, '.json');
// Stocker le contenu de cette balise pour cette version
versions[versionName] = content[tag] || "[Non disponible à cette étape]";
} catch (error) {
logSh(`⚠️ Erreur lecture version ${versionPath}: ${error.message}`, 'WARN');
versions[path.basename(versionPath, '.json')] = "[Erreur lecture]";
}
}
return {
tag,
versions
};
}
/**
* Charge un fichier version JSON
* @param {string} versionPath - Chemin du fichier
* @returns {Object} - Contenu JSON
*/
async loadVersion(versionPath) {
try {
const data = await fs.readFile(versionPath, 'utf8');
return JSON.parse(data);
} catch (error) {
logSh(`❌ Erreur chargement version ${versionPath}: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Sauvegarde les échantillons dans un fichier
* @param {Object} samplesData - Données échantillons
* @param {string} outputPath - Chemin de sauvegarde
*/
async saveSamples(samplesData, outputPath) {
try {
await fs.writeFile(outputPath, JSON.stringify(samplesData, null, 2), 'utf8');
logSh(`💾 Échantillons sauvegardés: ${outputPath}`, 'DEBUG');
} catch (error) {
logSh(`❌ Erreur sauvegarde échantillons: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Obtient le résumé des échantillons
*/
getSummary() {
return {
titles: this.samples.titles,
content: this.samples.content,
faqs: this.samples.faqs,
total: this.samples.titles.length + this.samples.content.length + this.samples.faqs.length
};
}
/**
* Reset l'état
*/
reset() {
this.samples = {
titles: [],
content: [],
faqs: []
};
}
}
module.exports = { SamplingEngine };

View File

@ -0,0 +1,495 @@
/**
* ValidatorCore.js
*
* Orchestrateur principal du Pipeline Validator
* Coordonne l'exécution du pipeline, l'échantillonnage et l'évaluation
*/
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { PipelineExecutor } = require('../pipeline/PipelineExecutor');
const { SamplingEngine } = require('./SamplingEngine');
const { CriteriaEvaluator } = require('./CriteriaEvaluator');
const { v4: uuidv4 } = require('uuid');
const fs = require('fs').promises;
const path = require('path');
/**
* Presets de validation configurables
*/
const VALIDATION_PRESETS = {
'ultra-rapid': {
name: 'Ultra-rapide',
description: 'Validation minimaliste (v1.0 + v2.0, 2 critères, 3 échantillons)',
versions: ['v1.0', 'v2.0'],
criteria: ['qualite', 'naturalite'],
maxSamples: 3,
estimatedCost: 0.05,
estimatedDuration: '30s'
},
'economical': {
name: 'Économique',
description: 'Validation légère (v1.0 + v2.0, 3 critères, 5 échantillons)',
versions: ['v1.0', 'v2.0'],
criteria: ['qualite', 'naturalite', 'seo'],
maxSamples: 5,
estimatedCost: 0.12,
estimatedDuration: '1min'
},
'standard': {
name: 'Standard',
description: 'Validation équilibrée (3 versions, tous critères, 5 échantillons)',
versions: ['v1.0', 'v1.2', 'v2.0'],
criteria: ['qualite', 'verbosite', 'seo', 'repetitions', 'naturalite'],
maxSamples: 5,
estimatedCost: 0.30,
estimatedDuration: '2min'
},
'complete': {
name: 'Complet',
description: 'Validation exhaustive (toutes versions, tous critères, tous échantillons)',
versions: null, // null = toutes les versions
criteria: ['qualite', 'verbosite', 'seo', 'repetitions', 'naturalite'],
maxSamples: null, // null = tous les échantillons
estimatedCost: 1.00,
estimatedDuration: '5min'
}
};
/**
* Classe ValidatorCore
*/
class ValidatorCore {
constructor(options = {}) {
this.validationId = null;
this.validationDir = null;
this.executor = new PipelineExecutor();
this.samplingEngine = new SamplingEngine();
this.criteriaEvaluator = new CriteriaEvaluator();
this.csvData = null;
this.status = 'idle';
this.progress = {
phase: null,
message: '',
percentage: 0
};
this.validationConfig = null; // ✅ NOUVEAU: Config validation active
// ✅ PHASE 3: WebSocket callback pour broadcast temps réel
this.broadcastCallback = options.broadcastCallback || null;
}
/**
* NOUVEAU: Obtient les presets disponibles
*/
static getPresets() {
return VALIDATION_PRESETS;
}
/**
* NOUVEAU: Applique un preset ou une config custom
*/
applyConfig(config) {
// Si config est un string, utiliser le preset correspondant
if (typeof config === 'string') {
if (!VALIDATION_PRESETS[config]) {
throw new Error(`Preset inconnu: ${config}`);
}
this.validationConfig = { preset: config, ...VALIDATION_PRESETS[config] };
} else if (config && typeof config === 'object') {
// Config custom
this.validationConfig = {
preset: 'custom',
name: config.name || 'Custom',
description: config.description || 'Configuration personnalisée',
versions: config.versions || null,
criteria: config.criteria || ['qualite', 'verbosite', 'seo', 'repetitions', 'naturalite'],
maxSamples: config.maxSamples || null
};
} else {
// Par défaut: mode complet
this.validationConfig = { preset: 'complete', ...VALIDATION_PRESETS.complete };
}
logSh(`⚙️ Configuration validation: ${this.validationConfig.name}`, 'INFO');
logSh(` Versions: ${this.validationConfig.versions ? this.validationConfig.versions.join(', ') : 'toutes'}`, 'DEBUG');
logSh(` Critères: ${this.validationConfig.criteria.join(', ')}`, 'DEBUG');
logSh(` Max échantillons: ${this.validationConfig.maxSamples || 'tous'}`, 'DEBUG');
}
/**
* Exécute une validation complète
* @param {Object|String} config - Configuration validation (preset name ou config object)
* @param {Object} pipelineConfig - Configuration pipeline
* @param {number} rowNumber - Numéro de ligne Google Sheets
* @returns {Object} - Résultats validation
*/
async runValidation(config, pipelineConfig, rowNumber) {
return tracer.run('ValidatorCore.runValidation', async () => {
try {
this.status = 'running';
this.validationId = uuidv4();
// ✅ NOUVEAU: Appliquer configuration
this.applyConfig(config);
logSh(`🚀 Démarrage validation: ${this.validationId}`, 'INFO');
logSh(` Pipeline: ${pipelineConfig.name} | Row: ${rowNumber}`, 'INFO');
logSh(` Mode: ${this.validationConfig.name}`, 'INFO');
// ========================================
// PHASE 1: SETUP
// ========================================
this.updateProgress('setup', 'Création structure dossiers...', 5);
await this.setupValidationStructure();
// ========================================
// PHASE 2: EXÉCUTION PIPELINE
// ========================================
this.updateProgress('pipeline', 'Exécution pipeline avec sauvegarde versions...', 10);
const pipelineResult = await this.runPipeline(pipelineConfig, rowNumber);
if (!pipelineResult.success) {
throw new Error('Échec exécution pipeline');
}
this.updateProgress('pipeline', 'Pipeline terminé avec succès', 40);
// ========================================
// PHASE 3: ÉCHANTILLONNAGE
// ========================================
this.updateProgress('sampling', 'Extraction échantillons représentatifs...', 50);
const samplesResult = await this.runSampling(pipelineResult.versionPaths);
this.updateProgress('sampling', `Échantillonnage terminé: ${samplesResult.summary.totalSamples} échantillons`, 60);
// ========================================
// PHASE 4: ÉVALUATION LLM (✅ NOUVEAU)
// ========================================
this.updateProgress('evaluation', 'Évaluation LLM des échantillons...', 65);
const evaluationsResult = await this.runEvaluations(samplesResult.samples, this.csvData);
this.updateProgress('evaluation', `Évaluations terminées: ${Object.keys(evaluationsResult).length} échantillons évalués`, 85);
// ========================================
// PHASE 5: SAUVEGARDE CONFIGURATION ET RÉSULTATS
// ========================================
this.updateProgress('saving', 'Sauvegarde configuration et métadonnées...', 88);
await this.saveConfiguration(pipelineConfig, rowNumber, pipelineResult);
await this.saveSamplesData(samplesResult);
await this.saveEvaluationsData(evaluationsResult);
// ========================================
// PHASE 6: GÉNÉRATION RAPPORT
// ========================================
this.updateProgress('report', 'Génération rapport validation...', 95);
const report = await this.generateReport(pipelineResult, samplesResult, evaluationsResult);
this.updateProgress('completed', 'Validation terminée avec succès', 100);
this.status = 'completed';
logSh(`✅ Validation ${this.validationId} terminée avec succès`, 'INFO');
return {
success: true,
validationId: this.validationId,
validationDir: this.validationDir,
report,
versionPaths: pipelineResult.versionPaths,
samples: samplesResult
};
} catch (error) {
this.status = 'error';
this.updateProgress('error', `Erreur: ${error.message}`, 0);
logSh(`❌ Validation ${this.validationId} échouée: ${error.message}`, 'ERROR');
return {
success: false,
validationId: this.validationId,
error: error.message,
status: this.status
};
}
}, { validationId: this.validationId });
}
/**
* Crée la structure de dossiers pour la validation
*/
async setupValidationStructure() {
this.validationDir = path.join(process.cwd(), 'validations', this.validationId);
const dirs = [
this.validationDir,
path.join(this.validationDir, 'versions'),
path.join(this.validationDir, 'samples'),
path.join(this.validationDir, 'results')
];
for (const dir of dirs) {
await fs.mkdir(dir, { recursive: true });
}
logSh(`📁 Structure validation créée: ${this.validationDir}`, 'DEBUG');
}
/**
* Exécute le pipeline avec sauvegarde toutes versions
*/
async runPipeline(pipelineConfig, rowNumber) {
logSh(`▶ Exécution pipeline: ${pipelineConfig.name}`, 'INFO');
const versionsDir = path.join(this.validationDir, 'versions');
const result = await this.executor.execute(pipelineConfig, rowNumber, {
saveAllVersions: true,
outputDir: versionsDir,
stopOnError: true
});
// ✅ Stocker csvData pour contexte évaluations
this.csvData = this.executor.csvData;
logSh(`✓ Pipeline exécuté: ${result.versionPaths.length} versions sauvegardées`, 'INFO');
return result;
}
/**
* Exécute l'échantillonnage
* MODIFIÉ: Filtre versions et limite échantillons selon config
*/
async runSampling(versionPaths) {
logSh(`▶ Échantillonnage: ${versionPaths.length} versions`, 'INFO');
// ✅ Filtrer versions si config spécifie une liste
let filteredPaths = versionPaths;
if (this.validationConfig.versions) {
filteredPaths = versionPaths.filter(vp => {
const versionName = path.basename(vp, '.json');
return this.validationConfig.versions.includes(versionName);
});
logSh(` Versions filtrées: ${filteredPaths.map(vp => path.basename(vp, '.json')).join(', ')}`, 'DEBUG');
}
const samplesResult = await this.samplingEngine.extractSamples(filteredPaths);
// ✅ Limiter nombre échantillons si config le spécifie
if (this.validationConfig.maxSamples && this.validationConfig.maxSamples < Object.keys(samplesResult.samples).length) {
const allTags = Object.keys(samplesResult.samples);
const selectedTags = allTags.slice(0, this.validationConfig.maxSamples);
const limitedSamples = {};
selectedTags.forEach(tag => {
limitedSamples[tag] = samplesResult.samples[tag];
});
samplesResult.samples = limitedSamples;
samplesResult.summary.totalSamples = selectedTags.length;
logSh(` Échantillons limités à ${this.validationConfig.maxSamples}`, 'DEBUG');
}
// Sauvegarder les échantillons
const samplesPath = path.join(this.validationDir, 'samples', 'all-samples.json');
await this.samplingEngine.saveSamples(samplesResult, samplesPath);
logSh(`✓ Échantillons extraits et sauvegardés`, 'INFO');
return samplesResult;
}
/**
* MODIFIÉ: Exécute les évaluations LLM avec filtrage critères
*/
async runEvaluations(samples, csvData) {
logSh(`▶ Évaluation LLM: ${Object.keys(samples).length} échantillons`, 'INFO');
logSh(` Critères actifs: ${this.validationConfig.criteria.join(', ')}`, 'DEBUG');
// Préparer contexte pour évaluations
const context = {
mc0: csvData?.mc0 || '',
t0: csvData?.t0 || '',
personality: csvData?.personality || {}
};
// ✅ Passer la liste des critères à évaluer
const evaluations = await this.criteriaEvaluator.evaluateBatch(
samples,
context,
3, // maxConcurrent
this.validationConfig.criteria // ✅ NOUVEAU: critères filtrés
);
// Calculer scores agrégés
const aggregated = this.criteriaEvaluator.aggregateScores(evaluations);
logSh(`✓ Évaluations terminées: ${Object.keys(evaluations).length} échantillons`, 'INFO');
logSh(` Score global moyen: ${aggregated.overall.avgScore}/10`, 'INFO');
return {
evaluations,
aggregated
};
}
/**
* Sauvegarde la configuration utilisée
*/
async saveConfiguration(pipelineConfig, rowNumber, pipelineResult) {
const configPath = path.join(this.validationDir, 'config.json');
const configData = {
validationId: this.validationId,
timestamp: new Date().toISOString(),
pipeline: pipelineConfig,
rowNumber,
personality: pipelineResult.metadata.personality,
executionLog: pipelineResult.executionLog
};
await fs.writeFile(configPath, JSON.stringify(configData, null, 2), 'utf8');
logSh(`💾 Configuration sauvegardée: ${configPath}`, 'DEBUG');
}
/**
* Sauvegarde les données d'échantillons
*/
async saveSamplesData(samplesResult) {
const samplesPath = path.join(this.validationDir, 'samples', 'summary.json');
await fs.writeFile(samplesPath, JSON.stringify(samplesResult.summary, null, 2), 'utf8');
logSh(`💾 Résumé échantillons sauvegardé: ${samplesPath}`, 'DEBUG');
}
/**
* NOUVEAU: Sauvegarde les données d'évaluations
*/
async saveEvaluationsData(evaluationsResult) {
const evaluationsPath = path.join(this.validationDir, 'results', 'evaluations.json');
await fs.writeFile(evaluationsPath, JSON.stringify(evaluationsResult, null, 2), 'utf8');
logSh(`💾 Évaluations sauvegardées: ${evaluationsPath}`, 'DEBUG');
}
/**
* MODIFIÉ: Génère le rapport de validation (avec évaluations LLM)
*/
async generateReport(pipelineResult, samplesResult, evaluationsResult = null) {
const reportPath = path.join(this.validationDir, 'report.json');
const report = {
validationId: this.validationId,
timestamp: new Date().toISOString(),
status: 'completed',
pipeline: {
name: pipelineResult.metadata.pipelineName,
totalSteps: pipelineResult.metadata.totalSteps,
successfulSteps: pipelineResult.metadata.successfulSteps,
totalDuration: pipelineResult.metadata.totalDuration
},
versions: {
count: pipelineResult.versionPaths.length,
paths: pipelineResult.versionPaths
},
samples: {
total: samplesResult.summary.totalSamples,
titles: samplesResult.summary.titles,
content: samplesResult.summary.content,
faqs: samplesResult.summary.faqs
},
evaluations: evaluationsResult ? {
totalEvaluations: evaluationsResult.aggregated.overall.totalEvaluations,
overallScore: evaluationsResult.aggregated.overall.avgScore,
byVersion: evaluationsResult.aggregated.byVersion,
byCriteria: evaluationsResult.aggregated.byCriteria,
details: `Voir ${path.join('results', 'evaluations.json')} pour détails complets`
} : null
};
await fs.writeFile(reportPath, JSON.stringify(report, null, 2), 'utf8');
logSh(`📊 Rapport validation généré: ${reportPath}`, 'INFO');
if (evaluationsResult) {
logSh(` 🎯 Score global: ${evaluationsResult.aggregated.overall.avgScore}/10`, 'INFO');
}
return report;
}
/**
* Met à jour le statut de progression
* PHASE 3: Avec broadcast WebSocket
*/
updateProgress(phase, message, percentage) {
this.progress = {
phase,
message,
percentage
};
logSh(`📈 [${percentage}%] ${phase}: ${message}`, 'INFO');
// ✅ PHASE 3: Broadcast via WebSocket si disponible
if (this.broadcastCallback) {
try {
this.broadcastCallback({
type: 'validation_progress',
validationId: this.validationId,
status: this.status,
progress: {
phase,
message,
percentage
},
timestamp: new Date().toISOString()
});
} catch (error) {
logSh(`⚠️ Erreur broadcast WebSocket: ${error.message}`, 'WARN');
}
}
}
/**
* Obtient le statut actuel
*/
getStatus() {
return {
validationId: this.validationId,
status: this.status,
progress: this.progress
};
}
/**
* Reset l'état
*/
reset() {
this.validationId = null;
this.validationDir = null;
this.csvData = null; // ✅ NOUVEAU: Reset csvData
this.executor.reset();
this.samplingEngine.reset();
this.criteriaEvaluator.resetCache(); // ✅ NOUVEAU: Reset cache évaluations
this.status = 'idle';
this.progress = {
phase: null,
message: '',
percentage: 0
};
}
}
module.exports = { ValidatorCore, VALIDATION_PRESETS };

35
package-lock.json generated
View File

@ -21,6 +21,7 @@
"pino": "^9.9.0",
"pino-pretty": "^13.1.1",
"undici": "^7.15.0",
"uuid": "^13.0.0",
"ws": "^8.18.3"
},
"devDependencies": {
@ -810,6 +811,19 @@
}
}
},
"node_modules/gaxios/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/gcp-metadata": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz",
@ -1017,6 +1031,19 @@
"node": ">=14.0.0"
}
},
"node_modules/googleapis-common/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/googleapis/node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
@ -2194,16 +2221,16 @@
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/vary": {

View File

@ -50,6 +50,7 @@
"pino": "^9.9.0",
"pino-pretty": "^13.1.1",
"undici": "^7.15.0",
"uuid": "^13.0.0",
"ws": "^8.18.3"
},
"devDependencies": {

View File

@ -140,6 +140,31 @@
margin-right: 10px;
}
.card.wip {
opacity: 0.7;
cursor: not-allowed;
position: relative;
}
.card.wip:hover {
transform: none;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.wip-badge {
position: absolute;
top: 20px;
right: 20px;
background: var(--warning);
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.75em;
font-weight: 700;
letter-spacing: 1px;
box-shadow: 0 2px 8px rgba(237, 137, 54, 0.4);
}
.stats-panel {
background: white;
border-radius: 15px;
@ -255,6 +280,20 @@
<li>Auto-refresh toutes les 30 secondes</li>
</ul>
</div>
<!-- Card 4: Pipeline Validator -->
<div class="card wip">
<span class="wip-badge">WIP</span>
<div class="card-icon">🧪</div>
<h2>Pipeline Validator</h2>
<p>Valider et comparer la qualité du contenu généré objectivement</p>
<ul>
<li>Évaluation LLM sur 5 critères universels</li>
<li>Monitoring temps réel via WebSocket</li>
<li>Graphiques comparatifs par version</li>
<li>Export rapports JSON détaillés</li>
</ul>
</div>
</div>
<div class="stats-panel">

View File

@ -19,6 +19,7 @@ const state = {
modules: [],
templates: [],
llmProviders: [],
personalities: [], // ✅ NOUVEAU: Liste des personnalités disponibles
nextStepNumber: 1
};
@ -30,6 +31,7 @@ window.onload = async function() {
await loadModules();
await loadTemplates();
await loadLLMProviders();
await loadPersonalities(); // ✅ NOUVEAU: Charger personnalités
updatePreview();
};
@ -86,6 +88,22 @@ async function loadLLMProviders() {
}
}
// ✅ NOUVEAU: Load personalities from API
async function loadPersonalities() {
try {
const response = await fetch('/api/personalities');
const data = await response.json();
if (data.success && data.personalities) {
state.personalities = data.personalities;
console.log(`${state.personalities.length} personnalités chargées`);
}
} catch (error) {
console.error('Erreur chargement personnalités:', error);
state.personalities = [];
}
}
// ====================
// RENDERING
// ====================
@ -244,11 +262,28 @@ function renderModuleParameters(step, index, module) {
</div>
`;
// Autres paramètres du module (sauf llmProvider qui est déjà affiché)
// ✅ NOUVEAU: Afficher dropdown personnalité pour SmartTouch
if (step.module === 'smarttouch' && state.personalities.length > 0) {
const currentPersonality = step.parameters?.personalityName || '';
html += `
<div class="config-row">
<label>Personnalité:</label>
<select onchange="updateStepParameter(${index}, 'personalityName', this.value)">
<option value="">Auto (from csvData)</option>
${state.personalities.map(personality =>
`<option value="${personality.nom}" ${personality.nom === currentPersonality ? 'selected' : ''}>${personality.nom} (${personality.style})</option>`
).join('')}
</select>
</div>
`;
}
// Autres paramètres du module (sauf llmProvider et personalityName qui sont déjà affichés)
if (module.parameters && Object.keys(module.parameters).length > 0) {
Object.entries(module.parameters).forEach(([paramName, paramConfig]) => {
// Skip llmProvider car déjà affiché ci-dessus
if (paramName === 'llmProvider') return;
// Skip llmProvider et personalityName car déjà affichés ci-dessus
if (paramName === 'llmProvider' || paramName === 'personalityName') return;
const value = step.parameters?.[paramName] || paramConfig.default || '';

File diff suppressed because it is too large Load Diff

71
test.txt Normal file
View File

@ -0,0 +1,71 @@
"Gamme Toutenplaque : plaques couleur à personnaliser
Gamme Toutenplaque : plaque numero de maison personnalisable couleur
La plaque numero de maison de la gamme Toutenplaque offre une personnalisation couleur avancée, alliant lisibilité et durabilité, adaptée aux exigences esthétiques et techniques des façades modernes. Il convient de noter que ces plaques optimisent la signalétique in situ tout en respectant les spécifications normatives et les contraintes environnementales. Pour découvrir loffre complète, consultez Plaques et numéros de rue personnalisés pour maison, portail et boîte aux lettres, /plaques-numeros-rue
Plaque numéro de rue personnalisée: plaques couleur et personnalisation avancée
H3_2 Plaque de maison imprimé: gamme toutenplaque, personnalisation couleur
H3_2 Plaque de maison imprimé: la gamme Toutenplaque offre une personnalisation couleur précise, combinant résistance PVC et procédés numériques avancés pour une plaque de maison imprimé durable et personnalisable.
Plaque de rue en aluminium: Gamme Toutenplaque couleur personnalisable
La Plaque de rue en aluminium de la gamme Toutenplaque offre des couleurs personnalisables durables, selon des spécifications UV élevées et des tolérances de fixation optimisées pour terrain urbain.
Numéro de maison exemples installation - Gamme Toutenplaque colorées
Numéro de maison exemples installation: découvrez comment la gamme Toutenplaque colorées optimise lisibilité et durabilité, en harmonisant contraste, matériaux PVC et performance signaleétique adaptée à chaque façade.
Gamme Toutenplaque: Résistance aux intempéries personnalisable
Gamme Toutenplaque offre une résistance aux intempéries personnalisable, adaptable selon UV, épaisseur et procédés décoratifs, pour des applications extérieures durables et conformes aux spécifications industrielles.
Plaque en acier inoxydable: gamme toutenplaque, couleurs personnalisables
La Plaque en acier inoxydable de notre gamme Toutenplaque associe robustesse, esthétique et durabilité; couleurs personnalisables, procédés dimpression avancés, et conformité normes industrielles garanties, par conséquent sécurité et lisibilité optimisées.
Plaque numéro de rue personnalisée: gamme Toutenplaque couleur durable et sûre
La Plaque numéro de rue personnalisée sinscrit dans une logique technique précise au sein de la Gamme Toutenplaque, où la couleur et la durabilité se conjuguent pour des performances optimisées en usure urbaine. Il convient de noter que, grâce à des substrats de type Dibond et à des finitions résistantes UV, les plaques conservent une lisibilité et une stabilité dimensionnelle même après exposition prolongée aux intempéries. La Plaque numéro de rue personnalisée offre une personnalisation couleur calibrée, assurant une signalétique uniforme et conforme aux normes industrielles, tout en garantissant une excellente résistance aux chocs et une durabilité accrue sur le long terme. Par conséquent, les options de couleur et les encres robustes permettent doptimiser le contraste et la visibilité nocturne, réduisant les risques derreur didentification. Pour approfondir les choix techniques, configurations et exigences, il convient de cliquer sur le lien dédié et découvrir la page fille associée à la Gamme Toutenplaque Plaque Numero TOUTENPLAQUEPlaque numéro de rue personnalisée/plaque-numero-personnalise.
Matériaux innovants pour plaques de maison: gamme Toutenplaque rupture créative
La Gamme Toutenplaque Plaque Toutenplaque Motif Classique ouvre une voie de rupture créative dans le domaine des plaques de maison imprimé, en associant des matériaux innovants à des procédés dimpression haute fidélité et durables. Dans ce cadre, les plaques bénéficient dun substrat technique polymère renforcé, offrant une résistance accrue aux intempéries et à la décoloration, tout en conservant une stabilité dimensionnelle sous contraintes thermiques et UV fréquentes. Par conséquent, lintégration de pigments organiques et inorganiques en phase avec des capacités de sublimation et de solution-plexage confère une palette chromatique homogène et conforme aux normes esthétiques les plus exigeantes. Les choix matériels visent une durabilité classe V0 et une résistance aux chocs optimisée, garantissant des plaques de maison imprimé qui restent lisibles et résistantes aux rayures même après plusieurs années dexposition. Pour cette raison, découvrir Plaque de maison imprimé/plaque-toutenplaque-motif-classique savère pertinent afin dappréhender les bénéfices matériels et procédés de cette offre innovante et crédible. La page suivante détaille les attributs techniques et les possibilités de personnalisation qui favoriseront votre choix éclairé.
Personnalisation avancée de plaques numérotées: gamme Toutenplaque, performance couleur
Il convient de noter que la personnalisation avancée de plaques numérotées sappuie sur la Gamme Toutenplaque pour offrir une cohérence technique entre performance couleur et durabilité matérielle. La Plaque Toutenplaque Style Fantaisie, spécifiquement conçue pour les environnements urbains, intègre des pigments UV stables et des liants résistants à la corrosion, garantissant une couleur homogène et pérenne sur plaquettes et plaques de rue en aluminium. Cette approche selon les procédés de fabrication avancés optimise lalignement des teintes tout en préservant les caractéristiques optiques face à lexposition solaire et aux intempéries. Il convient de noter que la personnalisation va au-delà du simple choix chromatique: elle porte sur la densité détiquetage, la précision des gravures et la compatibilité des couches de finition avec les marquages numerotés. Pour comprendre comment la Gamme Toutenplaque assure une performance couleur durable et une lisibilité optimisée, consultez la page dédiée à Plaque de rue en aluminium.
Résistance aux intempéries des plaques extérieures : Gamme Toutenplaque
La Résistance aux intempéries des plaques extérieures constitue un critère déterminant dans le choix de la Gamme Toutenplaque, car elle conditionne non seulement la durabilité esthétique mais aussi la performance mécanique sur le long terme. Dans ce cadre, la Gamme Toutenplaque Plaque Toutenplaque Exemples Installation offre des solutions conçues pour résister aux cycles féroces de gel-dégel, à lexposition solaire et aux atmosphères urbaines agressives, tout en conservant une lisibilité et une couleur stables. Les plaques, réalisées avec des substrats hautes performances et des traitements anti-UV, affichent une rigidité accrue et une résistance accrue à la microfissuration, essentielle pour les numéros de maison exposés aux variations climatiques. Il convient de noter que les performances dépendent des procédés de fabrication et des caractéristiques des revêtements colorés en liaison avec les normes de durabilité. Pour approfondir les innovations spécifiques et des exemples concrets dinstallation, Numéro de maison exemples installation est à explorer dans le cadre de la page dédiée. Numéro de maison exemples installation vous mènera directement vers des cas dusage pertinents et illustratifs.
Plaque en acier inoxydable anti-corrosion Gamme Toutenplaque personnalisable santé
La sélection dune Plaque en acier inoxydable brossé et de la gamme Toutenplaque sinscrit dans une démarche qualité orientée santé et durabilité, où les performances matière et les spécifications de finition guident le choix. En matière danti-corrosion, linox AISI 304 ou 316, selon lexposition, offre une résistance adaptée aux environnements industriels et hospitaliers, tout en préservant une esthétique homogène et pérenne. La personnalisation de la gamme Toutenplaque permet dintégrer des coloris et des gravures spécifiques, tout en garantissant une tenue chromatique et une résistance mécanique conformes aux exigences normatives. Dans ce contexte, la Résistance aux intempéries demeure un paramètre clé, car elle conditionne la durabilité des pièces en extérieur ou en zones exposées. Pour ce thème Santé, privilégier des traitements de surface compatibles avec les contraintes de stérilisation et de nettoyage est crucial. Sintéresser à la Résistance aux intempéries, de ce fait, ouvre une voie fiable pour cliquer et approfondir la page associée sur Plaque en acier inoxydable brossé et découvrir les choix optimisés pour votre application.
Options écologiques pour plaques de numéro: choix durables et performances optimisées
Les options écologiques pour plaques de numéro exigent une approche lisible et techniquement robuste, particulièrement lorsque lon vise des choix durables et des performances optimisées. Ainsi, la personnalisation avancée des plaques peut sappuyer sur des matériaux composites à base de fibres recyclées et de matrices thermodurcissables ou thermoplastiques recyclées, réduisant lempreinte carbone tout en conservant une résistance mécanique élevée et une stabilité dimensionnelle assurée. Plaque en acier inoxydable, utilisée comme référence de robustesse, illustre comment des variantes hybrides peuvent conjuguer durabilité et poids maîtrisé, par conséquent optimisant lefficacité énergétique des systèmes de signalisation. Le recours à des procédés de fabrication écoresponsables, comme le contrôle précis des consommations énergétiques et la réduction des rejets, soutient des performances optiques et mécaniques constantes même dans des environnements extérieurs agressifs. En somme, ces options crédibilisent un choix durable sans compromis sur la lisibilité et la longévité, et incitent à explorer plus loin la Plaque rétroéclairée en matériau composite. Plaque rétroéclairée en matériau composite
La plaque numero de maison est un élément essentiel pour lidentification rapide des adresses, notamment dans les environnements urbains et industriels, où la lisibilité et la durabilité conditionnent les temps dintervention. Par conséquent, explorez les réponses suivantes pour comprendre les critères de choix et dinstallation.
Les plaques numéro de maison de la Gamme Toutenplaque se distinguent par une combinaison optimale entre durabilité et personnalisation, ce qui en fait une solution adaptée aux environnements extérieurs exigeants. De ce fait, les couleurs sont produites selon des formulations UV-stables, garantissant une atténuation minimale des teintes après 5 à 7 ans dexposition solaire, tout en conservant une lisibilité maximale. Par conséquent, lépaisseur choisie (généralement 2,5 à 3,2 mm selon les références) assure une rigidité suffisante face aux variations climatiques et limite les risques de déformation. Les propriétés mécaniques permettent également une installation fiable sur différents supports (murs, bois, métal) sans dilatation indésirable, ce qui réduit les coûts de maintenance. Enfin, ces plaques offrent une personnalisation flexible en termes de reliefs et de finitions, permettant dadapter la plaque numero de maison à lesthétique architecturale tout en respectant les normes industrielles applicables.
Quelle est la durée de vie d'une plaque en aluminium utilisée en signalétique ?
Les plaques en aluminium présentent une durabilité remarquable en extérieur: Les plaques en aluminium peuvent durer jusqu'à 20 ans en extérieur, grâce à leur résistance à la corrosion et aux intempéries. Il convient de noter que cette longévité dépend toutefois de facteurs tels que lépaisseur choisie, le type de traitement de surface (anodisation, peintures et polymères, etc.) et lexposition aux agents chimiques ou salins. De ce fait, il est conseillé dopter pour des finitions UV-stables et des percorres compatibles avec les conditions environnementales spécifiques; ces choix influent directement sur les caractéristiques mécaniques et la résistance au Tarnissement. Si vous envisagez une utilisation extérieure durable, privilégiez une épaisseur adaptée et une finition conforme aux normes industrielles en vigueur.
Comment entretenir une plaque de maison en métal pour une durabilité optimale?
Pour la gamme Toutenplaque, il convient de suivre une routine minimale de maintenance afin dassurer une durabilité maximale sans compromettre les propriétés esthétiques. Il suffit de nettoyer régulièrement avec de l'eau savonneuse et d'appliquer un protecteur pour métal une fois par an pour assurer une durabilité maximale, ce qui permet déliminer les dépôts superficiels et de préserver les propriétés anti-corrosion. En pratique, une rinçage à leau claire, essuyage immédiat et réapplication du film protecteur à intervalle annuel suffisent pour maintenir les performances dans les environnements industriels normés. Il est conseillé de documenter cette intervention pour traçabilité et conformité des spécifications.
Quelles finitions sont disponibles pour les plaques personnalisées chez Toutenplaque ?
Les plaques peuvent être disponibles en finitions mates, brillantes, ou texturées, selon le matériau choisi et les préférences esthétiques, et ce choix influence directement les performances esthétiques et ladhérence des couleurs. Il convient de noter que les finitions mates restent généralement plus sensibles aux traces et nécessitent des procédés de pré-polissage et de vernis de protection adaptés, tandis que les finitions brillantes offrent un effet visuel plus prononcé mais peuvent nécessiter un contrôle accru des rayures. Préconisons dévaluer les exigences dexposition, les contraintes de nettoyage et les procédés de fixation afin de sélectionner la finition optimale pour chaque application.
Les plaques personnalisées sont-elles résistantes aux UV et à la décoloration?
Oui, la plupart des matériaux utilisés pour les plaques de maison, comme le PVC et l'aluminium, sont traités pour résister aux rayons UV, empêchant la décoloration prématurée. Précisons que ces traitements, tels que lajout dadditifs stabilisants et des revêtements de surface, améliorent les propriétés dexposition longue durée et assurent une tenue colorimétrique stable sur des années. De ce fait, les gammes colorées de notre offre Garantissent une durabilité visuelle conforme aux exigences normatives et aux usages extérieurs. Il est conseillé de sélectionner des épaisseurs et finitions adaptées à lenvironnement dimplantation pour optimiser les performances."