## 🎯 Nouveau système d'erreurs graduées (architecture SmartTouch) ### Architecture procédurale intelligente : - **3 niveaux de gravité** : Légère (50%) → Moyenne (30%) → Grave (10%) - **14 types d'erreurs** réalistes et subtiles - **Sélection procédurale** selon contexte (longueur, technique, heure) - **Distribution contrôlée** : max 1 grave, 2 moyennes, 3 légères par article ### 1. Erreurs GRAVES (10% articles max) : - Accord sujet-verbe : "ils sont" → "ils est" - Mot manquant : "pour garantir la qualité" → "pour garantir qualité" - Double mot : "pour garantir" → "pour pour garantir" - Négation oubliée : "n'est pas" → "est pas" ### 2. Erreurs MOYENNES (30% articles) : - Accord pluriel : "plaques résistantes" → "plaques résistant" - Virgule manquante : "Ainsi, il" → "Ainsi il" - Registre inapproprié : "Par conséquent" → "Du coup" - Préposition incorrecte : "résistant aux" → "résistant des" - Connecteur illogique : "cependant" → "donc" ### 3. Erreurs LÉGÈRES (50% articles) : - Double espace : "de votre" → "de votre" - Trait d'union : "c'est-à-dire" → "c'est à dire" - Espace ponctuation : "qualité ?" → "qualité?" - Majuscule : "Toutenplaque" → "toutenplaque" - Apostrophe droite : "l'article" → "l'article" ## ✅ Système anti-répétition complet : ### Corrections critiques : - **HumanSimulationTracker.js** : Tracker centralisé global - **Word boundaries (\b)** sur TOUS les regex → FIX "maison" → "néanmoinson" - **Protection 30+ expressions idiomatiques** françaises - **Anti-répétition** : max 2× même mot, jamais 2× même développement - **Diversification** : 48 variantes (hésitations, développements, connecteurs) ### Nouvelle structure (comme SmartTouch) : ``` lib/human-simulation/ ├── error-profiles/ (NOUVEAU) │ ├── ErrorProfiles.js (définitions + probabilités) │ ├── ErrorGrave.js (10% articles) │ ├── ErrorMoyenne.js (30% articles) │ ├── ErrorLegere.js (50% articles) │ └── ErrorSelector.js (sélection procédurale) ├── HumanSimulationCore.js (orchestrateur) ├── HumanSimulationTracker.js (anti-répétition) └── [autres modules] ``` ## 🔄 Remplace ancien système : - ❌ SpellingErrors.js (basique, répétitif, "et" → "." × 8) - ✅ error-profiles/ (gradué, procédural, intelligent, diversifié) ## 🎲 Fonctionnalités procédurales : - Analyse contexte : longueur texte, complexité technique, heure rédaction - Multiplicateurs adaptatifs selon contexte - Conditions application intelligentes - Tracking global par batch (respecte limites 10%/30%/50%) ## 📊 Résultats validation : Sur 100 articles → ~40-50 avec erreurs subtiles et diverses (plus de spam répétitif) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
390 lines
12 KiB
JavaScript
390 lines
12 KiB
JavaScript
/**
|
||
* PipelineDefinition.js
|
||
*
|
||
* Schemas et validation pour les pipelines modulaires flexibles.
|
||
* Permet de définir des workflows custom avec n'importe quelle combinaison de modules.
|
||
*/
|
||
|
||
const { logSh } = require('../ErrorReporting');
|
||
const { getLLMProvidersList } = require('../LLMManager');
|
||
|
||
/**
|
||
* Providers LLM disponibles (source unique depuis LLMManager)
|
||
*/
|
||
const AVAILABLE_LLM_PROVIDERS = getLLMProvidersList();
|
||
|
||
/**
|
||
* Modules disponibles dans le pipeline
|
||
*/
|
||
const AVAILABLE_MODULES = {
|
||
generation: {
|
||
name: 'Generation',
|
||
description: 'Génération initiale du contenu',
|
||
modes: ['simple'],
|
||
defaultIntensity: 1.0,
|
||
defaultLLM: 'claude-sonnet-4-5',
|
||
parameters: {
|
||
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'claude-sonnet-4-5' }
|
||
}
|
||
},
|
||
selective: {
|
||
name: 'Selective Enhancement',
|
||
description: 'Amélioration sélective par couches',
|
||
modes: [
|
||
'lightEnhancement',
|
||
'standardEnhancement',
|
||
'fullEnhancement',
|
||
'personalityFocus',
|
||
'fluidityFocus',
|
||
'adaptive'
|
||
],
|
||
defaultIntensity: 1.0,
|
||
defaultLLM: 'gpt-4o-mini',
|
||
parameters: {
|
||
layers: { type: 'array', description: 'Couches spécifiques à appliquer' },
|
||
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'gpt-4o-mini' }
|
||
}
|
||
},
|
||
smarttouch: {
|
||
name: 'SmartTouch (Analyse→Ciblé)',
|
||
description: 'Analyse intelligente puis améliorations précises ciblées (nouvelle génération)',
|
||
modes: [
|
||
'full', // Analyse + Technical + Style + Readability
|
||
'analysis_only', // Analyse uniquement sans amélioration
|
||
'technical_only', // Améliorations techniques ciblées uniquement
|
||
'style_only', // Améliorations style ciblées uniquement
|
||
'readability_only' // Améliorations lisibilité ciblées uniquement
|
||
],
|
||
defaultIntensity: 1.0,
|
||
defaultLLM: 'gpt-4o-mini',
|
||
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' },
|
||
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: {
|
||
name: 'Adversarial Generation',
|
||
description: 'Techniques anti-détection',
|
||
modes: ['none', 'light', 'standard', 'heavy', 'adaptive'],
|
||
defaultIntensity: 1.0,
|
||
defaultLLM: 'gemini-pro',
|
||
parameters: {
|
||
detector: { type: 'string', enum: ['general', 'gptZero', 'originality'], default: 'general' },
|
||
method: { type: 'string', enum: ['enhancement', 'regeneration', 'hybrid'], default: 'regeneration' },
|
||
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'gemini-pro' }
|
||
}
|
||
},
|
||
human: {
|
||
name: 'Human Simulation',
|
||
description: 'Simulation comportement humain',
|
||
modes: [
|
||
'none',
|
||
'lightSimulation',
|
||
'standardSimulation',
|
||
'heavySimulation',
|
||
'adaptiveSimulation',
|
||
'personalityFocus',
|
||
'temporalFocus'
|
||
],
|
||
defaultIntensity: 1.0,
|
||
defaultLLM: 'mistral-small',
|
||
parameters: {
|
||
fatigueLevel: { type: 'number', min: 0, max: 1, default: 0.5 },
|
||
errorRate: { type: 'number', min: 0, max: 1, default: 0.3 },
|
||
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'mistral-small' }
|
||
}
|
||
},
|
||
pattern: {
|
||
name: 'Pattern Breaking',
|
||
description: 'Cassage patterns LLM',
|
||
modes: [
|
||
'none',
|
||
'lightPatternBreaking',
|
||
'standardPatternBreaking',
|
||
'heavyPatternBreaking',
|
||
'adaptivePatternBreaking',
|
||
'syntaxFocus',
|
||
'connectorsFocus'
|
||
],
|
||
defaultIntensity: 1.0,
|
||
defaultLLM: 'deepseek-chat',
|
||
parameters: {
|
||
focus: { type: 'string', enum: ['syntax', 'connectors', 'both'], default: 'both' },
|
||
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'deepseek-chat' }
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Schema d'une étape de pipeline
|
||
*/
|
||
const STEP_SCHEMA = {
|
||
step: { type: 'number', required: true, description: 'Numéro séquentiel de l\'étape' },
|
||
module: { type: 'string', required: true, enum: Object.keys(AVAILABLE_MODULES), description: 'Module à exécuter' },
|
||
mode: { type: 'string', required: true, description: 'Mode du module' },
|
||
intensity: { type: 'number', required: false, min: 0.1, max: 2.0, default: 1.0, description: 'Intensité d\'application' },
|
||
parameters: { type: 'object', required: false, default: {}, description: 'Paramètres spécifiques au module' },
|
||
saveCheckpoint: { type: 'boolean', required: false, default: false, description: 'Sauvegarder checkpoint après cette étape' },
|
||
enabled: { type: 'boolean', required: false, default: true, description: 'Activer/désactiver l\'étape' }
|
||
};
|
||
|
||
/**
|
||
* Schema complet d'un pipeline
|
||
*/
|
||
const PIPELINE_SCHEMA = {
|
||
name: { type: 'string', required: true, minLength: 3, maxLength: 100 },
|
||
description: { type: 'string', required: false, maxLength: 500 },
|
||
pipeline: { type: 'array', required: true, minLength: 1, maxLength: 20 },
|
||
metadata: {
|
||
type: 'object',
|
||
required: false,
|
||
properties: {
|
||
author: { type: 'string' },
|
||
created: { type: 'string' },
|
||
version: { type: 'string' },
|
||
tags: { type: 'array' }
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Classe PipelineDefinition
|
||
*/
|
||
class PipelineDefinition {
|
||
constructor(definition = null) {
|
||
this.definition = definition;
|
||
}
|
||
|
||
/**
|
||
* Valide un pipeline complet
|
||
*/
|
||
static validate(pipeline) {
|
||
const errors = [];
|
||
|
||
// Validation schema principal
|
||
if (!pipeline.name || typeof pipeline.name !== 'string' || pipeline.name.length < 3) {
|
||
errors.push('Le nom du pipeline doit contenir au moins 3 caractères');
|
||
}
|
||
|
||
if (!Array.isArray(pipeline.pipeline) || pipeline.pipeline.length === 0) {
|
||
errors.push('Le pipeline doit contenir au moins une étape');
|
||
}
|
||
|
||
if (pipeline.pipeline && pipeline.pipeline.length > 20) {
|
||
errors.push('Le pipeline ne peut pas contenir plus de 20 étapes');
|
||
}
|
||
|
||
// Validation des étapes
|
||
if (Array.isArray(pipeline.pipeline)) {
|
||
pipeline.pipeline.forEach((step, index) => {
|
||
const stepErrors = PipelineDefinition.validateStep(step, index);
|
||
errors.push(...stepErrors);
|
||
});
|
||
|
||
// Vérifier séquence des steps
|
||
const steps = pipeline.pipeline.map(s => s.step).sort((a, b) => a - b);
|
||
for (let i = 0; i < steps.length; i++) {
|
||
if (steps[i] !== i + 1) {
|
||
errors.push(`Numérotation des étapes incorrecte: attendu ${i + 1}, trouvé ${steps[i]}`);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (errors.length > 0) {
|
||
logSh(`❌ Pipeline validation failed: ${errors.join(', ')}`, 'ERROR');
|
||
return { valid: false, errors };
|
||
}
|
||
|
||
logSh(`✅ Pipeline "${pipeline.name}" validé: ${pipeline.pipeline.length} étapes`, 'DEBUG');
|
||
return { valid: true, errors: [] };
|
||
}
|
||
|
||
/**
|
||
* Valide une étape individuelle
|
||
*/
|
||
static validateStep(step, index) {
|
||
const errors = [];
|
||
|
||
// Step number
|
||
if (typeof step.step !== 'number' || step.step < 1) {
|
||
errors.push(`Étape ${index}: 'step' doit être un nombre >= 1`);
|
||
}
|
||
|
||
// Module
|
||
if (!step.module || !AVAILABLE_MODULES[step.module]) {
|
||
errors.push(`Étape ${index}: module '${step.module}' inconnu. Disponibles: ${Object.keys(AVAILABLE_MODULES).join(', ')}`);
|
||
return errors; // Stop si module invalide
|
||
}
|
||
|
||
const moduleConfig = AVAILABLE_MODULES[step.module];
|
||
|
||
// Mode
|
||
if (!step.mode) {
|
||
errors.push(`Étape ${index}: 'mode' requis pour module ${step.module}`);
|
||
} else if (!moduleConfig.modes.includes(step.mode)) {
|
||
errors.push(`Étape ${index}: mode '${step.mode}' invalide pour ${step.module}. Disponibles: ${moduleConfig.modes.join(', ')}`);
|
||
}
|
||
|
||
// Intensity
|
||
if (step.intensity !== undefined) {
|
||
if (typeof step.intensity !== 'number' || step.intensity < 0.1 || step.intensity > 2.0) {
|
||
errors.push(`Étape ${index}: intensity doit être entre 0.1 et 2.0`);
|
||
}
|
||
}
|
||
|
||
// Parameters (validation basique)
|
||
if (step.parameters && typeof step.parameters !== 'object') {
|
||
errors.push(`Étape ${index}: parameters doit être un objet`);
|
||
}
|
||
|
||
return errors;
|
||
}
|
||
|
||
/**
|
||
* Crée une étape de pipeline valide
|
||
*/
|
||
static createStep(stepNumber, module, mode, options = {}) {
|
||
const moduleConfig = AVAILABLE_MODULES[module];
|
||
if (!moduleConfig) {
|
||
throw new Error(`Module inconnu: ${module}`);
|
||
}
|
||
|
||
if (!moduleConfig.modes.includes(mode)) {
|
||
throw new Error(`Mode ${mode} invalide pour module ${module}`);
|
||
}
|
||
|
||
return {
|
||
step: stepNumber,
|
||
module,
|
||
mode,
|
||
intensity: options.intensity ?? moduleConfig.defaultIntensity,
|
||
parameters: options.parameters ?? {},
|
||
saveCheckpoint: options.saveCheckpoint ?? false,
|
||
enabled: options.enabled ?? true
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Crée un pipeline vide
|
||
*/
|
||
static createEmpty(name, description = '') {
|
||
return {
|
||
name,
|
||
description,
|
||
pipeline: [],
|
||
metadata: {
|
||
author: 'system',
|
||
created: new Date().toISOString(),
|
||
version: '1.0',
|
||
tags: []
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Clone un pipeline
|
||
*/
|
||
static clone(pipeline, newName = null) {
|
||
const cloned = JSON.parse(JSON.stringify(pipeline));
|
||
if (newName) {
|
||
cloned.name = newName;
|
||
}
|
||
cloned.metadata = {
|
||
...cloned.metadata,
|
||
created: new Date().toISOString(),
|
||
clonedFrom: pipeline.name
|
||
};
|
||
return cloned;
|
||
}
|
||
|
||
/**
|
||
* Estime la durée d'un pipeline
|
||
*/
|
||
static estimateDuration(pipeline) {
|
||
// Durées moyennes par module (en secondes)
|
||
const DURATIONS = {
|
||
generation: 15,
|
||
selective: 20,
|
||
smarttouch: 25, // ✅ AJOUTÉ: smarttouch (analyse + améliorations ciblées)
|
||
adversarial: 25,
|
||
human: 15,
|
||
pattern: 18
|
||
};
|
||
|
||
let totalSeconds = 0;
|
||
pipeline.pipeline.forEach(step => {
|
||
if (!step.enabled) return;
|
||
|
||
const baseDuration = DURATIONS[step.module] || 20;
|
||
const intensityFactor = step.intensity || 1.0;
|
||
totalSeconds += baseDuration * intensityFactor;
|
||
});
|
||
|
||
return {
|
||
seconds: Math.round(totalSeconds),
|
||
formatted: PipelineDefinition.formatDuration(totalSeconds)
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Formate une durée en secondes
|
||
*/
|
||
static formatDuration(seconds) {
|
||
if (seconds < 60) return `${seconds}s`;
|
||
const minutes = Math.floor(seconds / 60);
|
||
const secs = seconds % 60;
|
||
return `${minutes}m ${secs}s`;
|
||
}
|
||
|
||
/**
|
||
* Obtient les infos d'un module
|
||
*/
|
||
static getModuleInfo(moduleName) {
|
||
return AVAILABLE_MODULES[moduleName] || null;
|
||
}
|
||
|
||
/**
|
||
* Liste tous les modules disponibles
|
||
*/
|
||
static listModules() {
|
||
return Object.entries(AVAILABLE_MODULES).map(([key, config]) => ({
|
||
id: key,
|
||
...config
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Génère un résumé lisible du pipeline
|
||
*/
|
||
static getSummary(pipeline) {
|
||
const enabledSteps = pipeline.pipeline.filter(s => s.enabled !== false);
|
||
const moduleCount = {};
|
||
|
||
enabledSteps.forEach(step => {
|
||
moduleCount[step.module] = (moduleCount[step.module] || 0) + 1;
|
||
});
|
||
|
||
const summary = Object.entries(moduleCount)
|
||
.map(([module, count]) => `${module}×${count}`)
|
||
.join(' → ');
|
||
|
||
return {
|
||
totalSteps: enabledSteps.length,
|
||
summary,
|
||
duration: PipelineDefinition.estimateDuration(pipeline)
|
||
};
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
PipelineDefinition,
|
||
AVAILABLE_MODULES,
|
||
AVAILABLE_LLM_PROVIDERS,
|
||
PIPELINE_SCHEMA,
|
||
STEP_SCHEMA
|
||
};
|