From 9a2ef7da2b071ec2f0b549335e8c2f0b96c4877a Mon Sep 17 00:00:00 2001 From: StillHammer Date: Tue, 14 Oct 2025 01:06:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(human-simulation):=20Syst=C3=A8me=20d'erre?= =?UTF-8?q?urs=20gradu=C3=A9es=20proc=C3=A9durales=20+=20anti-r=C3=A9p?= =?UTF-8?q?=C3=A9tition=20complet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 🎯 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 --- lib/APIController.js | 409 +++++ lib/BrainConfig.js | 60 +- lib/LLMManager.js | 8 +- lib/adversarial-generation/AdversarialCore.js | 435 ++++- .../AdversarialLayers.js | 19 +- lib/human-simulation/FatiguePatterns.js | 101 +- lib/human-simulation/HumanSimulationCore.js | 69 +- lib/human-simulation/HumanSimulationLayers.js | 23 +- .../HumanSimulationTracker.js | 181 +++ lib/human-simulation/PersonalityErrors.js | 48 +- lib/human-simulation/SpellingErrors.js | 312 ++++ lib/human-simulation/TemporalStyles.js | 221 ++- .../error-profiles/ErrorGrave.js | 219 +++ .../error-profiles/ErrorLegere.js | 245 +++ .../error-profiles/ErrorMoyenne.js | 253 +++ .../error-profiles/ErrorProfiles.js | 258 +++ .../error-profiles/ErrorSelector.js | 161 ++ lib/modes/ManualServer.js | 30 + lib/pipeline/PipelineDefinition.js | 4 +- lib/pipeline/PipelineExecutor.js | 73 +- .../GlobalBudgetManager.js | 309 ++++ lib/selective-smart-touch/SmartStyleLayer.js | 64 +- .../SmartTechnicalLayer.js | 19 +- lib/selective-smart-touch/SmartTouchCore.js | 116 +- lib/validation/CriteriaEvaluator.js | 477 ++++++ lib/validation/README.md | 429 +++++ lib/validation/SamplingEngine.js | 175 ++ lib/validation/ValidatorCore.js | 495 ++++++ package-lock.json | 35 +- package.json | 1 + public/index.html | 39 + public/pipeline-builder.js | 41 +- public/validation-dashboard.html | 1403 +++++++++++++++++ test.txt | 71 + 34 files changed, 6552 insertions(+), 251 deletions(-) create mode 100644 lib/human-simulation/HumanSimulationTracker.js create mode 100644 lib/human-simulation/SpellingErrors.js create mode 100644 lib/human-simulation/error-profiles/ErrorGrave.js create mode 100644 lib/human-simulation/error-profiles/ErrorLegere.js create mode 100644 lib/human-simulation/error-profiles/ErrorMoyenne.js create mode 100644 lib/human-simulation/error-profiles/ErrorProfiles.js create mode 100644 lib/human-simulation/error-profiles/ErrorSelector.js create mode 100644 lib/selective-smart-touch/GlobalBudgetManager.js create mode 100644 lib/validation/CriteriaEvaluator.js create mode 100644 lib/validation/README.md create mode 100644 lib/validation/SamplingEngine.js create mode 100644 lib/validation/ValidatorCore.js create mode 100644 public/validation-dashboard.html create mode 100644 test.txt diff --git a/lib/APIController.js b/lib/APIController.js index f61c764..dcfd489 100644 --- a/lib/APIController.js +++ b/lib/APIController.js @@ -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 }; \ No newline at end of file diff --git a/lib/BrainConfig.js b/lib/BrainConfig.js index 93ae382..91544fd 100644 --- a/lib/BrainConfig.js +++ b/lib/BrainConfig.js @@ -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(',') diff --git a/lib/LLMManager.js b/lib/LLMManager.js index 6f3baba..a4bc510 100644 --- a/lib/LLMManager.js +++ b/lib/LLMManager.js @@ -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 diff --git a/lib/adversarial-generation/AdversarialCore.js b/lib/adversarial-generation/AdversarialCore.js index 0ade63c..9256706 100644 --- a/lib/adversarial-generation/AdversarialCore.js +++ b/lib/adversarial-generation/AdversarialCore.js @@ -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 + } + }; } /** diff --git a/lib/adversarial-generation/AdversarialLayers.js b/lib/adversarial-generation/AdversarialLayers.js index b2ff5cd..a2ea96a 100644 --- a/lib/adversarial-generation/AdversarialLayers.js +++ b/lib/adversarial-generation/AdversarialLayers.js @@ -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); diff --git a/lib/human-simulation/FatiguePatterns.js b/lib/human-simulation/FatiguePatterns.js index 38a716c..5e1cab1 100644 --- a/lib/human-simulation/FatiguePatterns.js +++ b/lib/human-simulation/FatiguePatterns.js @@ -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 diff --git a/lib/human-simulation/HumanSimulationCore.js b/lib/human-simulation/HumanSimulationCore.js index 68e4721..f113f38 100644 --- a/lib/human-simulation/HumanSimulationCore.js +++ b/lib/human-simulation/HumanSimulationCore.js @@ -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 */ diff --git a/lib/human-simulation/HumanSimulationLayers.js b/lib/human-simulation/HumanSimulationLayers.js index f67c1d1..7946ed0 100644 --- a/lib/human-simulation/HumanSimulationLayers.js +++ b/lib/human-simulation/HumanSimulationLayers.js @@ -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', diff --git a/lib/human-simulation/HumanSimulationTracker.js b/lib/human-simulation/HumanSimulationTracker.js new file mode 100644 index 0000000..a50050b --- /dev/null +++ b/lib/human-simulation/HumanSimulationTracker.js @@ -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 +}; diff --git a/lib/human-simulation/PersonalityErrors.js b/lib/human-simulation/PersonalityErrors.js index 4ef1f42..6601178 100644 --- a/lib/human-simulation/PersonalityErrors.js +++ b/lib/human-simulation/PersonalityErrors.js @@ -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); diff --git a/lib/human-simulation/SpellingErrors.js b/lib/human-simulation/SpellingErrors.js new file mode 100644 index 0000000..a78c3f4 --- /dev/null +++ b/lib/human-simulation/SpellingErrors.js @@ -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 +}; diff --git a/lib/human-simulation/TemporalStyles.js b/lib/human-simulation/TemporalStyles.js index 721ff09..05ad0a9 100644 --- a/lib/human-simulation/TemporalStyles.js +++ b/lib/human-simulation/TemporalStyles.js @@ -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 }; \ No newline at end of file diff --git a/lib/human-simulation/error-profiles/ErrorGrave.js b/lib/human-simulation/error-profiles/ErrorGrave.js new file mode 100644 index 0000000..98a43d9 --- /dev/null +++ b/lib/human-simulation/error-profiles/ErrorGrave.js @@ -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 +}; diff --git a/lib/human-simulation/error-profiles/ErrorLegere.js b/lib/human-simulation/error-profiles/ErrorLegere.js new file mode 100644 index 0000000..41ea56a --- /dev/null +++ b/lib/human-simulation/error-profiles/ErrorLegere.js @@ -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 +}; diff --git a/lib/human-simulation/error-profiles/ErrorMoyenne.js b/lib/human-simulation/error-profiles/ErrorMoyenne.js new file mode 100644 index 0000000..5bcbe1d --- /dev/null +++ b/lib/human-simulation/error-profiles/ErrorMoyenne.js @@ -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 +}; diff --git a/lib/human-simulation/error-profiles/ErrorProfiles.js b/lib/human-simulation/error-profiles/ErrorProfiles.js new file mode 100644 index 0000000..48ebaa8 --- /dev/null +++ b/lib/human-simulation/error-profiles/ErrorProfiles.js @@ -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 +}; diff --git a/lib/human-simulation/error-profiles/ErrorSelector.js b/lib/human-simulation/error-profiles/ErrorSelector.js new file mode 100644 index 0000000..0bad64d --- /dev/null +++ b/lib/human-simulation/error-profiles/ErrorSelector.js @@ -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 +}; diff --git a/lib/modes/ManualServer.js b/lib/modes/ManualServer.js index c762c8d..5daf635 100644 --- a/lib/modes/ManualServer.js +++ b/lib/modes/ManualServer.js @@ -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'); diff --git a/lib/pipeline/PipelineDefinition.js b/lib/pipeline/PipelineDefinition.js index 1a8e86f..070e9d0 100644 --- a/lib/pipeline/PipelineDefinition.js +++ b/lib/pipeline/PipelineDefinition.js @@ -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: { diff --git a/lib/pipeline/PipelineExecutor.js b/lib/pipeline/PipelineExecutor.js index ae97964..c075958 100644 --- a/lib/pipeline/PipelineExecutor.js +++ b/lib/pipeline/PipelineExecutor.js @@ -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 */ diff --git a/lib/selective-smart-touch/GlobalBudgetManager.js b/lib/selective-smart-touch/GlobalBudgetManager.js new file mode 100644 index 0000000..672ada3 --- /dev/null +++ b/lib/selective-smart-touch/GlobalBudgetManager.js @@ -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 }; diff --git a/lib/selective-smart-touch/SmartStyleLayer.js b/lib/selective-smart-touch/SmartStyleLayer.js index 0d32f76..f85eb12 100644 --- a/lib/selective-smart-touch/SmartStyleLayer.js +++ b/lib/selective-smart-touch/SmartStyleLayer.js @@ -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 }; diff --git a/lib/selective-smart-touch/SmartTechnicalLayer.js b/lib/selective-smart-touch/SmartTechnicalLayer.js index 8db9409..d7c92a8 100644 --- a/lib/selective-smart-touch/SmartTechnicalLayer.js +++ b/lib/selective-smart-touch/SmartTechnicalLayer.js @@ -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 ? ` diff --git a/lib/selective-smart-touch/SmartTouchCore.js b/lib/selective-smart-touch/SmartTouchCore.js index 93ec0c8..52b258f 100644 --- a/lib/selective-smart-touch/SmartTouchCore.js +++ b/lib/selective-smart-touch/SmartTouchCore.js @@ -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 + }); } } diff --git a/lib/validation/CriteriaEvaluator.js b/lib/validation/CriteriaEvaluator.js new file mode 100644 index 0000000..185c2b8 --- /dev/null +++ b/lib/validation/CriteriaEvaluator.js @@ -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 }; diff --git a/lib/validation/README.md b/lib/validation/README.md new file mode 100644 index 0000000..9f8fa92 --- /dev/null +++ b/lib/validation/README.md @@ -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 diff --git a/lib/validation/SamplingEngine.js b/lib/validation/SamplingEngine.js new file mode 100644 index 0000000..e1f39ab --- /dev/null +++ b/lib/validation/SamplingEngine.js @@ -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} 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} 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 }; diff --git a/lib/validation/ValidatorCore.js b/lib/validation/ValidatorCore.js new file mode 100644 index 0000000..25626ff --- /dev/null +++ b/lib/validation/ValidatorCore.js @@ -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 }; diff --git a/package-lock.json b/package-lock.json index 6bac893..94addeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index da2459d..f292b72 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/public/index.html b/public/index.html index 1e3433d..c819e0a 100644 --- a/public/index.html +++ b/public/index.html @@ -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 @@
  • Auto-refresh toutes les 30 secondes
  • + + +
    + WIP +
    đŸ§Ș
    +

    Pipeline Validator

    +

    Valider et comparer la qualité du contenu généré objectivement

    +
      +
    • Évaluation LLM sur 5 critĂšres universels
    • +
    • Monitoring temps rĂ©el via WebSocket
    • +
    • Graphiques comparatifs par version
    • +
    • Export rapports JSON dĂ©taillĂ©s
    • +
    +
    diff --git a/public/pipeline-builder.js b/public/pipeline-builder.js index cf44ab7..432db05 100644 --- a/public/pipeline-builder.js +++ b/public/pipeline-builder.js @@ -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) {
    `; - // 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 += ` +
    + + +
    + `; + } + + // 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 || ''; diff --git a/public/validation-dashboard.html b/public/validation-dashboard.html new file mode 100644 index 0000000..258bc3c --- /dev/null +++ b/public/validation-dashboard.html @@ -0,0 +1,1403 @@ + + + + + + Pipeline Validator - SEO Generator + + + + +
    +
    +

    đŸ§Ș Pipeline Validator

    + ← Retour Accueil +
    + + +
    + + + +
    + + +
    +
    +

    📂 Configuration

    + +
    + + +
    + +
    +

    +

    + +
    +
    +
    Étapes
    +
    -
    +
    +
    +
    Durée Estimée
    +
    -
    +
    +
    +
    + +
    + + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + + +
    +
    + + +
    +
    +

    ⚡ Validations en cours

    +
    +
    +
    đŸ’€
    +

    Aucune validation active

    +
    +
    +
    + +
    +

    📜 Historique

    +
    +
    +
    📭
    +

    Aucune validation terminée

    +
    +
    +
    +
    + + +
    +
    +

    📊 SĂ©lection

    +
    + + +
    +
    + + +
    +
    + + + + diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..bbd4b42 --- /dev/null +++ b/test.txt @@ -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." \ No newline at end of file