557 lines
19 KiB
JavaScript
557 lines
19 KiB
JavaScript
// ========================================
|
|
// VALIDATEUR DE PERSONNALITÉ
|
|
// Validation de la cohérence avec le profil de personnalité sélectionné
|
|
// ========================================
|
|
|
|
// Import du LLMManager pour validation IA
|
|
let LLMManager;
|
|
try {
|
|
const llmModule = require('../../lib/LLMManager');
|
|
if (llmModule && typeof llmModule.callLLM === 'function') {
|
|
LLMManager = llmModule;
|
|
}
|
|
} catch (error) {
|
|
console.warn('LLMManager non disponible pour PersonalityValidator:', error.message);
|
|
}
|
|
|
|
/**
|
|
* Validateur spécialisé dans l'évaluation de la cohérence de personnalité
|
|
*/
|
|
class PersonalityValidator {
|
|
|
|
// Base de données des indicateurs de personnalité
|
|
static PERSONALITY_INDICATORS = {
|
|
'Marc': {
|
|
style: 'technique',
|
|
vocabulary: ['technique', 'précision', 'optimisé', 'performance', 'efficace'],
|
|
tone: 'professionnel',
|
|
structure: 'logique',
|
|
markers: ['il est recommandé', 'la solution optimale', 'techniquement']
|
|
},
|
|
'Sophie': {
|
|
style: 'décoratif',
|
|
vocabulary: ['élégant', 'harmonieux', 'esthétique', 'raffinement', 'style'],
|
|
tone: 'inspirant',
|
|
structure: 'descriptive',
|
|
markers: ['créer une ambiance', 'apporter du charme', 'sublimer']
|
|
},
|
|
'Laurent': {
|
|
style: 'commercial',
|
|
vocabulary: ['opportunité', 'avantage', 'investissement', 'rentable', 'performant'],
|
|
tone: 'persuasif',
|
|
structure: 'argumentative',
|
|
markers: ['bénéficier de', 'maximiser', 'opportunité unique']
|
|
},
|
|
'Julie': {
|
|
style: 'architectural',
|
|
vocabulary: ['structure', 'conception', 'aménagement', 'fonctionnel', 'ergonomie'],
|
|
tone: 'expert',
|
|
structure: 'méthodique',
|
|
markers: ['concevoir', 'structurer', 'aménager']
|
|
},
|
|
'Kévin': {
|
|
style: 'terrain',
|
|
vocabulary: ['pratique', 'solide', 'durable', 'fiable', 'résistant'],
|
|
tone: 'direct',
|
|
structure: 'concrète',
|
|
markers: ['sur le terrain', 'concrètement', 'en pratique']
|
|
},
|
|
'Amara': {
|
|
style: 'ingénierie',
|
|
vocabulary: ['innovation', 'technologie', 'développement', 'recherche', 'avancée'],
|
|
tone: 'scientifique',
|
|
structure: 'analytique',
|
|
markers: ['développer', 'innover', 'technologies avancées']
|
|
},
|
|
'Mamadou': {
|
|
style: 'artisan',
|
|
vocabulary: ['artisanal', 'savoir-faire', 'tradition', 'minutieux', 'authentique'],
|
|
tone: 'passionné',
|
|
structure: 'narrative',
|
|
markers: ['avec passion', 'savoir-faire artisanal', 'tradition']
|
|
},
|
|
'Émilie': {
|
|
style: 'digital',
|
|
vocabulary: ['numérique', 'connecté', 'innovation', 'tendance', 'moderne'],
|
|
tone: 'dynamique',
|
|
structure: 'concise',
|
|
markers: ['solutions digitales', 'innovation numérique', 'connecté']
|
|
},
|
|
'Pierre-Henri': {
|
|
style: 'patrimoine',
|
|
vocabulary: ['héritage', 'tradition', 'authentique', 'noble', 'raffinement'],
|
|
tone: 'distingué',
|
|
structure: 'élaborée',
|
|
markers: ['héritage culturel', 'tradition séculaire', 'authentique']
|
|
},
|
|
'Yasmine': {
|
|
style: 'éco',
|
|
vocabulary: ['durable', 'écologique', 'responsable', 'environnement', 'naturel'],
|
|
tone: 'engagé',
|
|
structure: 'consciente',
|
|
markers: ['respectueux de l\'environnement', 'démarche durable', 'écologique']
|
|
},
|
|
'Fabrice': {
|
|
style: 'métallurgie',
|
|
vocabulary: ['métallique', 'robuste', 'industriel', 'résistance', 'qualité'],
|
|
tone: 'technique',
|
|
structure: 'précise',
|
|
markers: ['qualité métallurgique', 'résistance optimale', 'fabrication']
|
|
},
|
|
'Chloé': {
|
|
style: 'contenu',
|
|
vocabulary: ['créatif', 'expressif', 'original', 'inspirant', 'captivant'],
|
|
tone: 'créatif',
|
|
structure: 'fluide',
|
|
markers: ['créer du contenu', 'inspiration créative', 'expression']
|
|
},
|
|
'Linh': {
|
|
style: 'fabrication',
|
|
vocabulary: ['production', 'qualité', 'processus', 'fabrication', 'contrôle'],
|
|
tone: 'rigoureux',
|
|
structure: 'processuelle',
|
|
markers: ['processus de fabrication', 'contrôle qualité', 'production']
|
|
},
|
|
'Minh': {
|
|
style: 'design',
|
|
vocabulary: ['design', 'esthétique', 'forme', 'beauté', 'élégance'],
|
|
tone: 'artistique',
|
|
structure: 'visuelle',
|
|
markers: ['design innovant', 'esthétique soignée', 'beauté']
|
|
},
|
|
'Thierry': {
|
|
style: 'créole',
|
|
vocabulary: ['chaleur', 'convivial', 'authentique', 'tradition', 'accueil'],
|
|
tone: 'chaleureux',
|
|
structure: 'conviviale',
|
|
markers: ['accueil chaleureux', 'tradition créole', 'convivialité']
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Validation principale de cohérence personnalité
|
|
*/
|
|
static async validatePersonality(content, targetPersonality, context = {}) {
|
|
// Récupération des indicateurs de la personnalité cible
|
|
const indicators = this.PERSONALITY_INDICATORS[targetPersonality.nom] ||
|
|
this.PERSONALITY_INDICATORS[targetPersonality];
|
|
|
|
if (!indicators) {
|
|
return {
|
|
score: 50,
|
|
confidence: 0.1,
|
|
feedback: `Personnalité "${targetPersonality.nom || targetPersonality}" inconnue`,
|
|
breakdown: {}
|
|
};
|
|
}
|
|
|
|
// Analyse statique
|
|
const staticAnalysis = {
|
|
vocabularyMatch: this.analyzeVocabularyMatch(content, indicators.vocabulary),
|
|
toneConsistency: this.analyzeToneConsistency(content, indicators.tone),
|
|
structureAlignment: this.analyzeStructureAlignment(content, indicators.structure),
|
|
markerPresence: this.analyzeMarkerPresence(content, indicators.markers),
|
|
overallCoherence: 0
|
|
};
|
|
|
|
// Calcul du score statique global
|
|
staticAnalysis.overallCoherence = Math.round(
|
|
staticAnalysis.vocabularyMatch.score * 0.3 +
|
|
staticAnalysis.toneConsistency.score * 0.25 +
|
|
staticAnalysis.structureAlignment.score * 0.25 +
|
|
staticAnalysis.markerPresence.score * 0.2
|
|
);
|
|
|
|
// Validation IA complémentaire (si LLM disponible)
|
|
let aiAnalysis = null;
|
|
if (LLMManager && context.enableLLM !== false) {
|
|
try {
|
|
const personalityName = targetPersonality.nom || targetPersonality;
|
|
aiAnalysis = await this.validatePersonalityWithLLM(content, personalityName, indicators);
|
|
|
|
// Fusion des scores (moyenne pondérée)
|
|
const combinedScore = Math.round(staticAnalysis.overallCoherence * 0.6 + aiAnalysis.score * 0.4);
|
|
|
|
return {
|
|
score: combinedScore,
|
|
confidence: Math.max(this.calculateConfidence(staticAnalysis), aiAnalysis.confidence),
|
|
feedback: aiAnalysis.feedback || this.generatePersonalityFeedback(staticAnalysis, indicators),
|
|
breakdown: staticAnalysis,
|
|
aiAnalysis: aiAnalysis,
|
|
targetPersonality: personalityName,
|
|
indicators: indicators
|
|
};
|
|
|
|
} catch (error) {
|
|
console.warn('Erreur validation LLM personnalité:', error.message);
|
|
}
|
|
}
|
|
|
|
return {
|
|
score: staticAnalysis.overallCoherence,
|
|
confidence: this.calculateConfidence(staticAnalysis),
|
|
feedback: this.generatePersonalityFeedback(staticAnalysis, indicators),
|
|
breakdown: staticAnalysis,
|
|
targetPersonality: targetPersonality.nom || targetPersonality,
|
|
indicators: indicators
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validation avec LLM pour analyse qualitative de personnalité
|
|
*/
|
|
static async validatePersonalityWithLLM(content, personalityName, indicators) {
|
|
const prompt = `
|
|
MISSION: Évalue si ce contenu correspond au style et à la personnalité de "${personalityName}".
|
|
|
|
PROFIL PERSONNALITÉ CIBLE - ${personalityName}:
|
|
- Style: ${indicators.style}
|
|
- Ton: ${indicators.tone}
|
|
- Structure: ${indicators.structure}
|
|
- Vocabulaire attendu: ${indicators.vocabulary.join(', ')}
|
|
- Marqueurs spécifiques: ${indicators.markers.join(', ')}
|
|
|
|
CONTENU À ANALYSER:
|
|
---
|
|
${content.substring(0, 1200)}${content.length > 1200 ? '...[TRONQUÉ]' : ''}
|
|
---
|
|
|
|
CRITÈRES D'ÉVALUATION:
|
|
1. VOCABULAIRE: Utilisation du vocabulaire spécialisé attendu
|
|
2. TON: Cohérence avec le ton caractéristique de la personnalité
|
|
3. STRUCTURE: Alignement avec le style de rédaction
|
|
4. AUTHENTICITÉ: Le contenu sonne-t-il vraiment comme cette personnalité?
|
|
|
|
RÉPONSE JSON STRICTE:
|
|
{
|
|
"score": 75,
|
|
"personalityMatch": "good",
|
|
"strengths": ["éléments bien alignés"],
|
|
"weaknesses": ["éléments à améliorer"],
|
|
"feedback": "Analyse personnalisée en 2-3 phrases",
|
|
"confidence": 0.8
|
|
}
|
|
|
|
SCORE: 0-100 (0=pas du tout cette personnalité, 100=parfaitement aligné)`;
|
|
|
|
const response = await LLMManager.callLLM('openai', prompt, {
|
|
temperature: 0.1,
|
|
max_tokens: 400
|
|
});
|
|
|
|
try {
|
|
// Parse JSON de la réponse
|
|
let jsonStr = response.trim();
|
|
const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
|
|
if (jsonMatch) {
|
|
jsonStr = jsonMatch[0];
|
|
}
|
|
|
|
const parsed = JSON.parse(jsonStr);
|
|
|
|
return {
|
|
score: Math.max(0, Math.min(100, parsed.score || 50)),
|
|
match: parsed.personalityMatch || 'medium',
|
|
strengths: parsed.strengths || [],
|
|
weaknesses: parsed.weaknesses || [],
|
|
feedback: parsed.feedback || 'Analyse non disponible',
|
|
confidence: Math.max(0, Math.min(1, parsed.confidence || 0.5)),
|
|
source: 'llm'
|
|
};
|
|
|
|
} catch (error) {
|
|
console.warn('Erreur parsing réponse LLM personnalité:', error.message);
|
|
return {
|
|
score: 50,
|
|
feedback: `Erreur parsing: ${error.message}`,
|
|
confidence: 0.1,
|
|
source: 'fallback'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Analyse de correspondance du vocabulaire
|
|
*/
|
|
static analyzeVocabularyMatch(content, targetVocabulary) {
|
|
const contentWords = this.extractSignificantWords(content);
|
|
const contentLower = content.toLowerCase();
|
|
|
|
let matchCount = 0;
|
|
let totalRelevance = 0;
|
|
const foundWords = [];
|
|
|
|
targetVocabulary.forEach(word => {
|
|
const occurrences = (contentLower.match(new RegExp(word, 'g')) || []).length;
|
|
if (occurrences > 0) {
|
|
matchCount += occurrences;
|
|
foundWords.push({ word, count: occurrences });
|
|
totalRelevance += occurrences * 10; // 10 points par occurrence
|
|
}
|
|
});
|
|
|
|
// Score basé sur la densité de vocabulaire spécialisé
|
|
const vocabularyDensity = matchCount / (contentWords.length / 100); // Pour 100 mots
|
|
const score = Math.min(totalRelevance + vocabularyDensity * 10, 100);
|
|
|
|
return {
|
|
score: Math.round(score),
|
|
matchCount,
|
|
foundWords,
|
|
density: vocabularyDensity
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Analyse de cohérence du ton
|
|
*/
|
|
static analyzeToneConsistency(content, targetTone) {
|
|
const toneIndicators = {
|
|
'professionnel': {
|
|
positive: ['recommandé', 'conseillé', 'expertise', 'spécialisé'],
|
|
negative: ['super', 'génial', 'carrément', 'franchement']
|
|
},
|
|
'inspirant': {
|
|
positive: ['créer', 'transformer', 'magnifier', 'sublimer', 'imaginer'],
|
|
negative: ['technique', 'spécification', 'paramètre']
|
|
},
|
|
'persuasif': {
|
|
positive: ['bénéficier', 'avantage', 'opportunité', 'maximiser'],
|
|
negative: ['peut-être', 'probablement', 'incertain']
|
|
},
|
|
'direct': {
|
|
positive: ['directement', 'simplement', 'clairement', 'concrètement'],
|
|
negative: ['sophistiqué', 'complexe', 'nuancé']
|
|
},
|
|
'chaleureux': {
|
|
positive: ['accueillant', 'convivial', 'chaleureux', 'bienveillant'],
|
|
negative: ['froid', 'technique', 'rigide']
|
|
},
|
|
'scientifique': {
|
|
positive: ['recherche', 'analyse', 'étude', 'développement'],
|
|
negative: ['ressenti', 'intuition', 'approximatif']
|
|
}
|
|
};
|
|
|
|
const indicators = toneIndicators[targetTone] || { positive: [], negative: [] };
|
|
const contentLower = content.toLowerCase();
|
|
|
|
let positiveScore = 0;
|
|
let negativeScore = 0;
|
|
|
|
indicators.positive.forEach(word => {
|
|
const count = (contentLower.match(new RegExp(word, 'g')) || []).length;
|
|
positiveScore += count * 15;
|
|
});
|
|
|
|
indicators.negative.forEach(word => {
|
|
const count = (contentLower.match(new RegExp(word, 'g')) || []).length;
|
|
negativeScore += count * 10;
|
|
});
|
|
|
|
const toneScore = Math.max(0, Math.min(100, 50 + positiveScore - negativeScore));
|
|
|
|
return {
|
|
score: Math.round(toneScore),
|
|
positiveMatches: positiveScore / 15,
|
|
negativeMatches: negativeScore / 10
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Analyse d'alignement de structure
|
|
*/
|
|
static analyzeStructureAlignment(content, targetStructure) {
|
|
const structureFeatures = {
|
|
'logique': {
|
|
check: content => {
|
|
const hasNumberedList = /\d+\.\s/.test(content);
|
|
const hasLogicalConnectors = /(donc|ainsi|par conséquent)/gi.test(content);
|
|
const hasMethodicalFlow = /(premièrement|deuxièmement|enfin)/gi.test(content);
|
|
|
|
return (hasNumberedList ? 30 : 0) +
|
|
(hasLogicalConnectors ? 40 : 0) +
|
|
(hasMethodicalFlow ? 30 : 0);
|
|
}
|
|
},
|
|
'descriptive': {
|
|
check: content => {
|
|
const hasDescriptiveWords = /(élégant|beau|harmonieux|raffinement)/gi.test(content);
|
|
const hasFlowingStructure = !/(1\.|2\.|3\.)/g.test(content);
|
|
const hasAdjectives = (content.match(/\b\w+ment\b/g) || []).length;
|
|
|
|
return (hasDescriptiveWords ? 40 : 0) +
|
|
(hasFlowingStructure ? 30 : 0) +
|
|
Math.min(hasAdjectives * 5, 30);
|
|
}
|
|
},
|
|
'argumentative': {
|
|
check: content => {
|
|
const hasArguments = /(avantage|bénéfice|preuve|démontrer)/gi.test(content);
|
|
const hasCounterpoints = /(cependant|néanmoins|mais)/gi.test(content);
|
|
const hasConclusion = /(en conclusion|finalement|donc)/gi.test(content);
|
|
|
|
return (hasArguments ? 40 : 0) +
|
|
(hasCounterpoints ? 30 : 0) +
|
|
(hasConclusion ? 30 : 0);
|
|
}
|
|
},
|
|
'narrative': {
|
|
check: content => {
|
|
const hasStoryElements = /(histoire|tradition|expérience|passion)/gi.test(content);
|
|
const hasPersonalTouch = /(nous|notre|mon|ma)/gi.test(content);
|
|
const hasTemporalMarkers = /(depuis|aujourd'hui|maintenant)/gi.test(content);
|
|
|
|
return (hasStoryElements ? 40 : 0) +
|
|
(hasPersonalTouch ? 30 : 0) +
|
|
(hasTemporalMarkers ? 30 : 0);
|
|
}
|
|
}
|
|
};
|
|
|
|
const feature = structureFeatures[targetStructure];
|
|
const score = feature ? feature.check(content) : 50;
|
|
|
|
return {
|
|
score: Math.min(100, Math.round(score)),
|
|
targetStructure
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Analyse de présence des marqueurs spécifiques
|
|
*/
|
|
static analyzeMarkerPresence(content, targetMarkers) {
|
|
const contentLower = content.toLowerCase();
|
|
let foundMarkers = 0;
|
|
let totalScore = 0;
|
|
const detectedMarkers = [];
|
|
|
|
targetMarkers.forEach(marker => {
|
|
if (contentLower.includes(marker.toLowerCase())) {
|
|
foundMarkers++;
|
|
totalScore += 20; // 20 points par marqueur trouvé
|
|
detectedMarkers.push(marker);
|
|
}
|
|
});
|
|
|
|
// Bonus si plusieurs marqueurs présents
|
|
if (foundMarkers > 1) {
|
|
totalScore += foundMarkers * 5;
|
|
}
|
|
|
|
return {
|
|
score: Math.min(100, totalScore),
|
|
foundCount: foundMarkers,
|
|
totalCount: targetMarkers.length,
|
|
detectedMarkers
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calcul de la confiance dans l'évaluation
|
|
*/
|
|
static calculateConfidence(analysis) {
|
|
// Confiance basée sur la cohérence des différents indicateurs
|
|
const scores = [
|
|
analysis.vocabularyMatch.score,
|
|
analysis.toneConsistency.score,
|
|
analysis.structureAlignment.score,
|
|
analysis.markerPresence.score
|
|
];
|
|
|
|
const mean = scores.reduce((a, b) => a + b) / scores.length;
|
|
const variance = scores.reduce((sum, score) => sum + Math.pow(score - mean, 2), 0) / scores.length;
|
|
const stdDev = Math.sqrt(variance);
|
|
|
|
// Confiance élevée si peu de variance entre les scores
|
|
const confidence = Math.max(0.1, 1 - (stdDev / 50));
|
|
|
|
return Math.round(confidence * 100) / 100;
|
|
}
|
|
|
|
/**
|
|
* Génération du feedback personnalisé
|
|
*/
|
|
static generatePersonalityFeedback(analysis, indicators) {
|
|
const feedback = [];
|
|
|
|
if (analysis.vocabularyMatch.score >= 70) {
|
|
feedback.push(`✅ Vocabulaire bien aligné avec le style ${indicators.style}`);
|
|
} else {
|
|
feedback.push(`⚠️ Vocabulaire à enrichir (mots-clés: ${indicators.vocabulary.join(', ')})`);
|
|
}
|
|
|
|
if (analysis.toneConsistency.score >= 70) {
|
|
feedback.push(`✅ Ton ${indicators.tone} respecté`);
|
|
} else {
|
|
feedback.push(`⚠️ Ton à ajuster vers plus ${indicators.tone}`);
|
|
}
|
|
|
|
if (analysis.structureAlignment.score >= 70) {
|
|
feedback.push(`✅ Structure ${indicators.structure} appropriée`);
|
|
} else {
|
|
feedback.push(`⚠️ Structure à adapter au style ${indicators.structure}`);
|
|
}
|
|
|
|
if (analysis.markerPresence.foundCount > 0) {
|
|
feedback.push(`✅ ${analysis.markerPresence.foundCount} marqueur(s) spécifique(s) détecté(s)`);
|
|
} else {
|
|
feedback.push(`⚠️ Aucun marqueur spécifique détecté`);
|
|
}
|
|
|
|
return feedback.join(' | ');
|
|
}
|
|
|
|
/**
|
|
* Utilitaires
|
|
*/
|
|
static extractSignificantWords(content) {
|
|
return content
|
|
.toLowerCase()
|
|
.replace(/[^\w\s]/g, ' ')
|
|
.split(/\s+/)
|
|
.filter(word => word.length > 3);
|
|
}
|
|
|
|
/**
|
|
* Validation rapide avec recommandations
|
|
*/
|
|
static quickValidation(content, personality) {
|
|
const validation = this.validatePersonality(content, personality);
|
|
|
|
return {
|
|
score: validation.score,
|
|
level: validation.score >= 75 ? 'GOOD' : validation.score >= 50 ? 'MODERATE' : 'POOR',
|
|
recommendations: this.generateRecommendations(validation),
|
|
confidence: validation.confidence
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Génération de recommandations d'amélioration
|
|
*/
|
|
static generateRecommendations(validation) {
|
|
const recs = [];
|
|
|
|
if (validation.breakdown.vocabularyMatch.score < 60) {
|
|
recs.push(`Intégrer plus de vocabulaire spécialisé: ${validation.indicators.vocabulary.slice(0, 3).join(', ')}`);
|
|
}
|
|
|
|
if (validation.breakdown.toneConsistency.score < 60) {
|
|
recs.push(`Adapter le ton pour qu'il soit plus ${validation.indicators.tone}`);
|
|
}
|
|
|
|
if (validation.breakdown.structureAlignment.score < 60) {
|
|
recs.push(`Réorganiser selon une structure plus ${validation.indicators.structure}`);
|
|
}
|
|
|
|
if (validation.breakdown.markerPresence.foundCount === 0) {
|
|
recs.push(`Utiliser des marqueurs spécifiques: ${validation.indicators.markers.slice(0, 2).join(', ')}`);
|
|
}
|
|
|
|
return recs;
|
|
}
|
|
}
|
|
|
|
module.exports = { PersonalityValidator }; |