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:
parent
74bf1b0f38
commit
9a2ef7da2b
@ -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 };
|
||||
@ -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(',')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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',
|
||||
|
||||
181
lib/human-simulation/HumanSimulationTracker.js
Normal file
181
lib/human-simulation/HumanSimulationTracker.js
Normal 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
|
||||
};
|
||||
@ -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);
|
||||
|
||||
312
lib/human-simulation/SpellingErrors.js
Normal file
312
lib/human-simulation/SpellingErrors.js
Normal 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
|
||||
};
|
||||
@ -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
|
||||
};
|
||||
219
lib/human-simulation/error-profiles/ErrorGrave.js
Normal file
219
lib/human-simulation/error-profiles/ErrorGrave.js
Normal 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
|
||||
};
|
||||
245
lib/human-simulation/error-profiles/ErrorLegere.js
Normal file
245
lib/human-simulation/error-profiles/ErrorLegere.js
Normal 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
|
||||
};
|
||||
253
lib/human-simulation/error-profiles/ErrorMoyenne.js
Normal file
253
lib/human-simulation/error-profiles/ErrorMoyenne.js
Normal 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
|
||||
};
|
||||
258
lib/human-simulation/error-profiles/ErrorProfiles.js
Normal file
258
lib/human-simulation/error-profiles/ErrorProfiles.js
Normal 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
|
||||
};
|
||||
161
lib/human-simulation/error-profiles/ErrorSelector.js
Normal file
161
lib/human-simulation/error-profiles/ErrorSelector.js
Normal 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
|
||||
};
|
||||
@ -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');
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
309
lib/selective-smart-touch/GlobalBudgetManager.js
Normal file
309
lib/selective-smart-touch/GlobalBudgetManager.js
Normal 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 };
|
||||
@ -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 };
|
||||
|
||||
@ -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 ? `
|
||||
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
477
lib/validation/CriteriaEvaluator.js
Normal file
477
lib/validation/CriteriaEvaluator.js
Normal 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
429
lib/validation/README.md
Normal 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
|
||||
175
lib/validation/SamplingEngine.js
Normal file
175
lib/validation/SamplingEngine.js
Normal 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 };
|
||||
495
lib/validation/ValidatorCore.js
Normal file
495
lib/validation/ValidatorCore.js
Normal 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
35
package-lock.json
generated
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 || '';
|
||||
|
||||
|
||||
1403
public/validation-dashboard.html
Normal file
1403
public/validation-dashboard.html
Normal file
File diff suppressed because it is too large
Load Diff
71
test.txt
Normal file
71
test.txt
Normal 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 l’offre 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 d’impression 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 s’inscrit 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 d’optimiser le contraste et la visibilité nocturne, réduisant les risques d’erreur d’identification. 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 d’impression haute fidélité et durables. Dans ce cadre, les plaques bénéficient d’un 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, l’inté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 d’exposition. Pour cette raison, découvrir Plaque de maison imprimé/plaque-toutenplaque-motif-classique s’avère pertinent afin d’appré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 s’appuie 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 l’alignement des teintes tout en préservant les caractéristiques optiques face à l’exposition 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, à l’exposition 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 d’installation, 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 d’usage pertinents et illustratifs.
|
||||
|
||||
Plaque en acier inoxydable anti-corrosion – Gamme Toutenplaque personnalisable santé
|
||||
|
||||
La sélection d’une Plaque en acier inoxydable brossé et de la gamme Toutenplaque s’inscrit 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 d’anti-corrosion, l’inox AISI 304 ou 316, selon l’exposition, 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 d’inté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. S’inté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 l’on vise des choix durables et des performances optimisées. Ainsi, la personnalisation avancée des plaques peut s’appuyer sur des matériaux composites à base de fibres recyclées et de matrices thermodurcissables ou thermoplastiques recyclées, réduisant l’empreinte 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 l’efficacité é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 l’identification rapide des adresses, notamment dans les environnements urbains et industriels, où la lisibilité et la durabilité conditionnent les temps d’intervention. Par conséquent, explorez les réponses suivantes pour comprendre les critères de choix et d’installation.
|
||||
|
||||
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 d’exposition 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 d’adapter la plaque numero de maison à l’esthé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 l’exposition aux agents chimiques ou salins. De ce fait, il est conseillé d’opter 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 d’assurer 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 à l’eau 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 l’adhé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 d’exposition, 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 l’ajout d’additifs stabilisants et des revêtements de surface, améliorent les propriétés d’exposition 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 à l’environnement d’implantation pour optimiser les performances."
|
||||
Loading…
Reference in New Issue
Block a user