seo-generator-server/lib/pipeline/PipelineDefinition.js
StillHammer 9a2ef7da2b feat(human-simulation): Système d'erreurs graduées procédurales + anti-répétition complet
## 🎯 Nouveau système d'erreurs graduées (architecture SmartTouch)

### Architecture procédurale intelligente :
- **3 niveaux de gravité** : Légère (50%) → Moyenne (30%) → Grave (10%)
- **14 types d'erreurs** réalistes et subtiles
- **Sélection procédurale** selon contexte (longueur, technique, heure)
- **Distribution contrôlée** : max 1 grave, 2 moyennes, 3 légères par article

### 1. Erreurs GRAVES (10% articles max) :
- Accord sujet-verbe : "ils sont" → "ils est"
- Mot manquant : "pour garantir la qualité" → "pour garantir qualité"
- Double mot : "pour garantir" → "pour pour garantir"
- Négation oubliée : "n'est pas" → "est pas"

### 2. Erreurs MOYENNES (30% articles) :
- Accord pluriel : "plaques résistantes" → "plaques résistant"
- Virgule manquante : "Ainsi, il" → "Ainsi il"
- Registre inapproprié : "Par conséquent" → "Du coup"
- Préposition incorrecte : "résistant aux" → "résistant des"
- Connecteur illogique : "cependant" → "donc"

### 3. Erreurs LÉGÈRES (50% articles) :
- Double espace : "de votre" → "de  votre"
- Trait d'union : "c'est-à-dire" → "c'est à dire"
- Espace ponctuation : "qualité ?" → "qualité?"
- Majuscule : "Toutenplaque" → "toutenplaque"
- Apostrophe droite : "l'article" → "l'article"

##  Système anti-répétition complet :

### Corrections critiques :
- **HumanSimulationTracker.js** : Tracker centralisé global
- **Word boundaries (\b)** sur TOUS les regex → FIX "maison" → "néanmoinson"
- **Protection 30+ expressions idiomatiques** françaises
- **Anti-répétition** : max 2× même mot, jamais 2× même développement
- **Diversification** : 48 variantes (hésitations, développements, connecteurs)

### Nouvelle structure (comme SmartTouch) :
```
lib/human-simulation/
├── error-profiles/                (NOUVEAU)
│   ├── ErrorProfiles.js          (définitions + probabilités)
│   ├── ErrorGrave.js             (10% articles)
│   ├── ErrorMoyenne.js           (30% articles)
│   ├── ErrorLegere.js            (50% articles)
│   └── ErrorSelector.js          (sélection procédurale)
├── HumanSimulationCore.js         (orchestrateur)
├── HumanSimulationTracker.js      (anti-répétition)
└── [autres modules]
```

## 🔄 Remplace ancien système :
-  SpellingErrors.js (basique, répétitif, "et" → "." × 8)
-  error-profiles/ (gradué, procédural, intelligent, diversifié)

## 🎲 Fonctionnalités procédurales :
- Analyse contexte : longueur texte, complexité technique, heure rédaction
- Multiplicateurs adaptatifs selon contexte
- Conditions application intelligentes
- Tracking global par batch (respecte limites 10%/30%/50%)

## 📊 Résultats validation :
Sur 100 articles → ~40-50 avec erreurs subtiles et diverses (plus de spam répétitif)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 01:06:28 +08:00

390 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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
};