seogeneratorserver/lib/selective-enhancement/SelectiveCore.js
StillHammer dbf1a3de8c Add technical plan for multi-format export system
Added plan.md with complete architecture for format-agnostic content generation:
- Support for Markdown, HTML, Plain Text, JSON formats
- New FormatExporter module with neutral data structure
- Integration strategy with existing ContentAssembly and ArticleStorage
- Bonus features: SEO metadata generation, readability scoring, WordPress Gutenberg format
- Implementation roadmap with 4 phases (6h total estimated)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 16:14:29 +08:00

422 lines
13 KiB
JavaScript

// ========================================
// SELECTIVE CORE - MOTEUR MODULAIRE
// Responsabilité: Moteur selective enhancement réutilisable sur tout contenu
// Architecture: Couches applicables à la demande
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - APPLICATION COUCHE SELECTIVE ENHANCEMENT
* Input: contenu existant + configuration selective
* Output: contenu avec couche selective appliquée
*/
async function applySelectiveLayer(existingContent, config = {}) {
return await tracer.run('SelectiveCore.applySelectiveLayer()', async () => {
const {
layerType = 'technical', // 'technical' | 'transitions' | 'style' | 'all'
llmProvider = 'auto', // 'claude' | 'gpt4' | 'gemini' | 'mistral' | 'auto'
analysisMode = true, // Analyser avant d'appliquer
preserveStructure = true,
csvData = null,
context = {}
} = config;
await tracer.annotate({
selectiveLayer: true,
layerType,
llmProvider,
analysisMode,
elementsCount: Object.keys(existingContent).length
});
const startTime = Date.now();
logSh(`🔧 APPLICATION COUCHE SELECTIVE: ${layerType} (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(existingContent).length} éléments | Mode: ${analysisMode ? 'analysé' : 'direct'}`, 'INFO');
try {
let enhancedContent = {};
let layerStats = {};
// Sélection automatique du LLM si 'auto'
const selectedLLM = selectOptimalLLM(layerType, llmProvider);
// Application selon type de couche
switch (layerType) {
case 'technical':
const technicalResult = await applyTechnicalEnhancement(existingContent, { ...config, llmProvider: selectedLLM });
enhancedContent = technicalResult.content;
layerStats = technicalResult.stats;
break;
case 'transitions':
const transitionResult = await applyTransitionEnhancement(existingContent, { ...config, llmProvider: selectedLLM });
enhancedContent = transitionResult.content;
layerStats = transitionResult.stats;
break;
case 'style':
const styleResult = await applyStyleEnhancement(existingContent, { ...config, llmProvider: selectedLLM });
enhancedContent = styleResult.content;
layerStats = styleResult.stats;
break;
case 'all':
const allResult = await applyAllSelectiveLayers(existingContent, config);
enhancedContent = allResult.content;
layerStats = allResult.stats;
break;
default:
throw new Error(`Type de couche selective inconnue: ${layerType}`);
}
const duration = Date.now() - startTime;
const stats = {
layerType,
llmProvider: selectedLLM,
elementsProcessed: Object.keys(existingContent).length,
elementsEnhanced: countEnhancedElements(existingContent, enhancedContent),
duration,
...layerStats
};
logSh(`✅ COUCHE SELECTIVE APPLIQUÉE: ${stats.elementsEnhanced}/${stats.elementsProcessed} améliorés (${duration}ms)`, 'INFO');
await tracer.event('Couche selective appliquée', stats);
return {
content: enhancedContent,
stats,
original: existingContent,
config: { ...config, llmProvider: selectedLLM }
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ COUCHE SELECTIVE ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content: existingContent,
stats: { fallback: true, duration },
original: existingContent,
config,
error: error.message
};
}
}, { existingContent: Object.keys(existingContent), config });
}
/**
* APPLICATION TECHNIQUE MODULAIRE
*/
async function applyTechnicalEnhancement(content, config = {}) {
const { TechnicalLayer } = require('./TechnicalLayer');
const layer = new TechnicalLayer();
return await layer.apply(content, config);
}
/**
* APPLICATION TRANSITIONS MODULAIRE
*/
async function applyTransitionEnhancement(content, config = {}) {
const { TransitionLayer } = require('./TransitionLayer');
const layer = new TransitionLayer();
return await layer.apply(content, config);
}
/**
* APPLICATION STYLE MODULAIRE
*/
async function applyStyleEnhancement(content, config = {}) {
const { StyleLayer } = require('./StyleLayer');
const layer = new StyleLayer();
return await layer.apply(content, config);
}
/**
* APPLICATION TOUTES COUCHES SÉQUENTIELLES
*/
async function applyAllSelectiveLayers(content, config = {}) {
logSh(`🔄 Application séquentielle toutes couches selective`, 'DEBUG');
let currentContent = content;
const allStats = {
steps: [],
totalDuration: 0,
totalEnhancements: 0
};
const steps = [
{ name: 'technical', llm: 'gpt4' },
{ name: 'transitions', llm: 'gemini' },
{ name: 'style', llm: 'mistral' }
];
for (const step of steps) {
try {
logSh(` 🔧 Étape: ${step.name} (${step.llm})`, 'DEBUG');
const stepResult = await applySelectiveLayer(currentContent, {
...config,
layerType: step.name,
llmProvider: step.llm
});
currentContent = stepResult.content;
allStats.steps.push({
name: step.name,
llm: step.llm,
...stepResult.stats
});
allStats.totalDuration += stepResult.stats.duration;
allStats.totalEnhancements += stepResult.stats.elementsEnhanced;
} catch (error) {
logSh(` ❌ Étape ${step.name} échouée: ${error.message}`, 'ERROR');
allStats.steps.push({
name: step.name,
llm: step.llm,
error: error.message,
duration: 0,
elementsEnhanced: 0
});
}
}
return {
content: currentContent,
stats: allStats
};
}
/**
* ANALYSE BESOIN D'ENHANCEMENT
*/
async function analyzeEnhancementNeeds(content, config = {}) {
logSh(`🔍 Analyse besoins selective enhancement`, 'DEBUG');
const analysis = {
technical: { needed: false, score: 0, elements: [] },
transitions: { needed: false, score: 0, elements: [] },
style: { needed: false, score: 0, elements: [] },
recommendation: 'none'
};
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
// Analyse technique (termes techniques manquants)
const technicalNeed = assessTechnicalNeed(text, config.csvData);
if (technicalNeed.score > 0.3) {
analysis.technical.needed = true;
analysis.technical.score += technicalNeed.score;
analysis.technical.elements.push({ tag, score: technicalNeed.score, reason: technicalNeed.reason });
}
// Analyse transitions (fluidité)
const transitionNeed = assessTransitionNeed(text);
if (transitionNeed.score > 0.4) {
analysis.transitions.needed = true;
analysis.transitions.score += transitionNeed.score;
analysis.transitions.elements.push({ tag, score: transitionNeed.score, reason: transitionNeed.reason });
}
// Analyse style (personnalité)
const styleNeed = assessStyleNeed(text, config.csvData?.personality);
if (styleNeed.score > 0.3) {
analysis.style.needed = true;
analysis.style.score += styleNeed.score;
analysis.style.elements.push({ tag, score: styleNeed.score, reason: styleNeed.reason });
}
});
// Normaliser scores
const elementCount = Object.keys(content).length;
analysis.technical.score = analysis.technical.score / elementCount;
analysis.transitions.score = analysis.transitions.score / elementCount;
analysis.style.score = analysis.style.score / elementCount;
// Recommandation
const scores = [
{ type: 'technical', score: analysis.technical.score },
{ type: 'transitions', score: analysis.transitions.score },
{ type: 'style', score: analysis.style.score }
].sort((a, b) => b.score - a.score);
if (scores[0].score > 0.6) {
analysis.recommendation = scores[0].type;
} else if (scores[0].score > 0.4) {
analysis.recommendation = 'light_' + scores[0].type;
}
logSh(` 📊 Analyse: Tech=${analysis.technical.score.toFixed(2)} | Trans=${analysis.transitions.score.toFixed(2)} | Style=${analysis.style.score.toFixed(2)}`, 'DEBUG');
logSh(` 💡 Recommandation: ${analysis.recommendation}`, 'DEBUG');
return analysis;
}
// ============= HELPER FUNCTIONS =============
/**
* Sélectionner LLM optimal selon type de couche
*/
function selectOptimalLLM(layerType, llmProvider) {
if (llmProvider !== 'auto') return llmProvider;
const optimalMapping = {
'technical': 'openai', // OpenAI GPT-4 excellent pour précision technique
'transitions': 'gemini', // Gemini bon pour fluidité
'style': 'mistral', // Mistral excellent pour style personnalité
'all': 'claude' // Claude polyvalent pour tout
};
return optimalMapping[layerType] || 'claude';
}
/**
* Compter éléments améliorés
*/
function countEnhancedElements(original, enhanced) {
let count = 0;
Object.keys(original).forEach(tag => {
if (enhanced[tag] && enhanced[tag] !== original[tag]) {
count++;
}
});
return count;
}
/**
* Évaluer besoin technique
*/
function assessTechnicalNeed(content, csvData) {
let score = 0;
let reason = [];
// Manque de termes techniques spécifiques
if (csvData?.mc0) {
const technicalTerms = ['dibond', 'pmma', 'aluminium', 'fraisage', 'impression', 'gravure', 'découpe'];
const contentLower = content.toLowerCase();
const foundTerms = technicalTerms.filter(term => contentLower.includes(term));
if (foundTerms.length === 0 && content.length > 100) {
score += 0.4;
reason.push('manque_termes_techniques');
}
}
// Vocabulaire trop générique
const genericWords = ['produit', 'solution', 'service', 'qualité', 'offre'];
const genericCount = genericWords.filter(word => content.toLowerCase().includes(word)).length;
if (genericCount > 2) {
score += 0.3;
reason.push('vocabulaire_générique');
}
// Manque de précision dimensionnelle/technique
if (content.length > 50 && !(/\d+\s*(mm|cm|m|%|°)/.test(content))) {
score += 0.2;
reason.push('manque_précision_technique');
}
return { score: Math.min(1, score), reason: reason.join(',') };
}
/**
* Évaluer besoin transitions
*/
function assessTransitionNeed(content) {
let score = 0;
let reason = [];
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) return { score: 0, reason: '' };
// Connecteurs répétitifs
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant'];
let repetitiveConnectors = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(connector, 'gi')) || []);
if (matches.length > 1) repetitiveConnectors++;
});
if (repetitiveConnectors > 1) {
score += 0.4;
reason.push('connecteurs_répétitifs');
}
// Transitions abruptes (phrases sans connecteurs logiques)
let abruptTransitions = 0;
for (let i = 1; i < sentences.length; i++) {
const sentence = sentences[i].trim().toLowerCase();
const hasConnector = connectors.some(conn => sentence.startsWith(conn)) ||
/^(puis|ensuite|également|aussi|donc|ainsi)/.test(sentence);
if (!hasConnector && sentence.length > 30) {
abruptTransitions++;
}
}
if (abruptTransitions / sentences.length > 0.6) {
score += 0.3;
reason.push('transitions_abruptes');
}
return { score: Math.min(1, score), reason: reason.join(',') };
}
/**
* Évaluer besoin style
*/
function assessStyleNeed(content, personality) {
let score = 0;
let reason = [];
if (!personality) {
score += 0.2;
reason.push('pas_personnalité');
return { score, reason: reason.join(',') };
}
// Style générique (pas de personnalité visible)
const personalityWords = (personality.vocabulairePref || '').toLowerCase().split(',');
const contentLower = content.toLowerCase();
const personalityFound = personalityWords.some(word =>
word.trim() && contentLower.includes(word.trim())
);
if (!personalityFound && content.length > 50) {
score += 0.4;
reason.push('style_générique');
}
// Niveau technique inadapté
if (personality.niveauTechnique === 'accessible' && /\b(optimisation|implémentation|méthodologie)\b/i.test(content)) {
score += 0.3;
reason.push('trop_technique');
}
return { score: Math.min(1, score), reason: reason.join(',') };
}
module.exports = {
applySelectiveLayer, // ← MAIN ENTRY POINT MODULAIRE
applyTechnicalEnhancement,
applyTransitionEnhancement,
applyStyleEnhancement,
applyAllSelectiveLayers,
analyzeEnhancementNeeds,
selectOptimalLLM
};