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>
422 lines
13 KiB
JavaScript
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
|
|
}; |