seo-generator-server/lib/selective-enhancement/TechnicalLayer.js

443 lines
14 KiB
JavaScript

// ========================================
// TECHNICAL LAYER - COUCHE TECHNIQUE MODULAIRE
// Responsabilité: Amélioration technique modulaire réutilisable
// LLM: GPT-4o-mini (précision technique optimale)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { chunkArray, sleep } = require('./SelectiveUtils');
/**
* COUCHE TECHNIQUE MODULAIRE
*/
class TechnicalLayer {
constructor() {
this.name = 'TechnicalEnhancement';
this.defaultLLM = 'openai';
this.priority = 1; // Haute priorité - appliqué en premier généralement
}
/**
* MAIN METHOD - Appliquer amélioration technique
*/
async apply(content, config = {}) {
return await tracer.run('TechnicalLayer.apply()', async () => {
const {
llmProvider = this.defaultLLM,
intensity = 1.0, // 0.0-2.0 intensité d'amélioration
analysisMode = true, // Analyser avant d'appliquer
csvData = null,
preserveStructure = true,
targetTerms = null // Termes techniques ciblés
} = config;
await tracer.annotate({
technicalLayer: true,
llmProvider,
intensity,
elementsCount: Object.keys(content).length,
mc0: csvData?.mc0
});
const startTime = Date.now();
logSh(`⚙️ TECHNICAL LAYER: Amélioration technique (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Intensité: ${intensity}`, 'INFO');
try {
let enhancedContent = {};
let elementsProcessed = 0;
let elementsEnhanced = 0;
if (analysisMode) {
// 1. Analyser éléments nécessitant amélioration technique
const analysis = await this.analyzeTechnicalNeeds(content, csvData, targetTerms);
logSh(` 📋 Analyse: ${analysis.candidates.length}/${Object.keys(content).length} éléments candidats`, 'DEBUG');
if (analysis.candidates.length === 0) {
logSh(`✅ TECHNICAL LAYER: Aucune amélioration nécessaire`, 'INFO');
return {
content,
stats: {
processed: Object.keys(content).length,
enhanced: 0,
analysisSkipped: true,
duration: Date.now() - startTime
}
};
}
// 2. Améliorer les éléments sélectionnés
const improvedResults = await this.enhanceTechnicalElements(
analysis.candidates,
csvData,
{ llmProvider, intensity, preserveStructure }
);
// 3. Merger avec contenu original
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = analysis.candidates.length;
} else {
// Mode direct : améliorer tous les éléments
enhancedContent = await this.enhanceAllElementsDirect(
content,
csvData,
{ llmProvider, intensity, preserveStructure }
);
elementsProcessed = Object.keys(content).length;
elementsEnhanced = this.countDifferences(content, enhancedContent);
}
const duration = Date.now() - startTime;
const stats = {
processed: elementsProcessed,
enhanced: elementsEnhanced,
total: Object.keys(content).length,
enhancementRate: (elementsEnhanced / Math.max(elementsProcessed, 1)) * 100,
duration,
llmProvider,
intensity
};
logSh(`✅ TECHNICAL LAYER TERMINÉE: ${elementsEnhanced}/${elementsProcessed} améliorés (${duration}ms)`, 'INFO');
await tracer.event('Technical layer appliquée', stats);
return { content: enhancedContent, stats };
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ TECHNICAL LAYER ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw error;
}
}, { content: Object.keys(content), config });
}
/**
* ANALYSER BESOINS TECHNIQUES
*/
async analyzeTechnicalNeeds(content, csvData, targetTerms = null) {
logSh(`🔍 Analyse besoins techniques`, 'DEBUG');
const analysis = {
candidates: [],
technicalTermsFound: [],
missingTerms: [],
globalScore: 0
};
// Définir termes techniques selon contexte
const contextualTerms = this.getContextualTechnicalTerms(csvData?.mc0, targetTerms);
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
const elementAnalysis = this.analyzeTechnicalElement(text, contextualTerms, csvData);
if (elementAnalysis.needsImprovement) {
analysis.candidates.push({
tag,
content: text,
technicalTerms: elementAnalysis.foundTerms,
missingTerms: elementAnalysis.missingTerms,
score: elementAnalysis.score,
improvements: elementAnalysis.improvements
});
analysis.globalScore += elementAnalysis.score;
}
analysis.technicalTermsFound.push(...elementAnalysis.foundTerms);
});
analysis.globalScore = analysis.globalScore / Math.max(Object.keys(content).length, 1);
analysis.technicalTermsFound = [...new Set(analysis.technicalTermsFound)];
logSh(` 📊 Score global technique: ${analysis.globalScore.toFixed(2)}`, 'DEBUG');
return analysis;
}
/**
* AMÉLIORER ÉLÉMENTS TECHNIQUES SÉLECTIONNÉS
*/
async enhanceTechnicalElements(candidates, csvData, config) {
logSh(`🛠️ Amélioration ${candidates.length} éléments techniques`, 'DEBUG');
logSh(`🔍 Candidates reçus: ${JSON.stringify(candidates.map(c => c.tag))}`, 'DEBUG');
const results = {};
const chunks = chunkArray(candidates, 4); // Chunks de 4 pour GPT-4
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk technique ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const enhancementPrompt = this.createTechnicalEnhancementPrompt(chunk, csvData, config);
const response = await callLLM(config.llmProvider, enhancementPrompt, {
temperature: 0.4, // Précision technique
maxTokens: 3000
}, csvData?.personality);
const chunkResults = this.parseTechnicalResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk technique ${chunkIndex + 1}: ${Object.keys(chunkResults).length} améliorés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk technique ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: conserver contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* AMÉLIORER TOUS ÉLÉMENTS MODE DIRECT
*/
async enhanceAllElementsDirect(content, csvData, config) {
const allElements = Object.entries(content).map(([tag, text]) => ({
tag,
content: text,
technicalTerms: [],
improvements: ['amélioration_générale_technique'],
missingTerms: [] // Ajout de la propriété manquante
}));
return await this.enhanceTechnicalElements(allElements, csvData, config);
}
// ============= HELPER METHODS =============
/**
* Analyser élément technique individuel
*/
analyzeTechnicalElement(text, contextualTerms, csvData) {
let score = 0;
const foundTerms = [];
const missingTerms = [];
const improvements = [];
// 1. Détecter termes techniques présents
contextualTerms.forEach(term => {
if (text.toLowerCase().includes(term.toLowerCase())) {
foundTerms.push(term);
} else if (text.length > 100) { // Seulement pour textes longs
missingTerms.push(term);
}
});
// 2. Évaluer manque de précision technique
if (foundTerms.length === 0 && text.length > 80) {
score += 0.4;
improvements.push('ajout_termes_techniques');
}
// 3. Détecter vocabulaire trop générique
const genericWords = ['produit', 'solution', 'service', 'offre', 'article'];
const genericCount = genericWords.filter(word =>
text.toLowerCase().includes(word)
).length;
if (genericCount > 1) {
score += 0.3;
improvements.push('spécialisation_vocabulaire');
}
// 4. Manque de données techniques (dimensions, etc.)
if (text.length > 50 && !(/\d+\s*(mm|cm|m|%|°|kg|g)/.test(text))) {
score += 0.2;
improvements.push('ajout_données_techniques');
}
// 5. Contexte métier spécifique
if (csvData?.mc0 && !text.toLowerCase().includes(csvData.mc0.toLowerCase().split(' ')[0])) {
score += 0.1;
improvements.push('intégration_contexte_métier');
}
return {
needsImprovement: score > 0.3,
score,
foundTerms,
missingTerms: missingTerms.slice(0, 3), // Limiter à 3 termes manquants
improvements
};
}
/**
* Obtenir termes techniques contextuels
*/
getContextualTechnicalTerms(mc0, targetTerms) {
// Termes de base signalétique
const baseTerms = [
'dibond', 'aluminium', 'PMMA', 'acrylique', 'plexiglas',
'impression', 'gravure', 'découpe', 'fraisage', 'perçage',
'adhésif', 'fixation', 'visserie', 'support'
];
// Termes spécifiques selon contexte
const contextualTerms = [];
if (mc0) {
const mc0Lower = mc0.toLowerCase();
if (mc0Lower.includes('plaque')) {
contextualTerms.push('épaisseur 3mm', 'format standard', 'finition brossée', 'anodisation');
}
if (mc0Lower.includes('signalétique')) {
contextualTerms.push('norme ISO', 'pictogramme', 'contraste visuel', 'lisibilité');
}
if (mc0Lower.includes('personnalisée')) {
contextualTerms.push('découpe forme', 'impression numérique', 'quadrichromie', 'pantone');
}
}
// Ajouter termes ciblés si fournis
if (targetTerms && Array.isArray(targetTerms)) {
contextualTerms.push(...targetTerms);
}
return [...baseTerms, ...contextualTerms];
}
/**
* Créer prompt amélioration technique
*/
createTechnicalEnhancementPrompt(chunk, csvData, config) {
const personality = csvData?.personality;
let prompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces contenus.
CONTEXTE: ${csvData?.mc0 || 'Signalétique personnalisée'} - Secteur: impression/signalétique
${personality ? `PERSONNALITÉ: ${personality.nom} (${personality.style})` : ''}
INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif)
ÉLÉMENTS À AMÉLIORER TECHNIQUEMENT:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
CONTENU: "${item.content}"
AMÉLIORATIONS: ${item.improvements.join(', ')}
${item.missingTerms.length > 0 ? `TERMES À INTÉGRER: ${item.missingTerms.join(', ')}` : ''}`).join('\n\n')}
CONSIGNES TECHNIQUES:
- GARDE exactement le même message et ton${personality ? ` ${personality.style}` : ''}
- AJOUTE précision technique naturelle et vocabulaire spécialisé
- INTÈGRE termes métier : matériaux, procédés, normes, dimensions
- REMPLACE vocabulaire générique par termes techniques appropriés
- ÉVITE jargon incompréhensible, reste accessible
- PRESERVE longueur approximative (±15%)
VOCABULAIRE TECHNIQUE RECOMMANDÉ:
- Matériaux: dibond, aluminium anodisé, PMMA coulé, PVC expansé
- Procédés: impression UV, gravure laser, découpe numérique, fraisage CNC
- Finitions: brossé, poli, texturé, laqué
- Fixations: perçage, adhésif double face, vis inox, plots de fixation
FORMAT RÉPONSE:
[1] Contenu avec amélioration technique précise
[2] Contenu avec amélioration technique précise
etc...
IMPORTANT: Réponse DIRECTE par les contenus améliorés, pas d'explication.`;
return prompt;
}
/**
* Parser réponse technique
*/
parseTechnicalResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let technicalContent = match[2].trim();
const element = chunk[index];
// Nettoyer contenu technique
technicalContent = this.cleanTechnicalContent(technicalContent);
if (technicalContent && technicalContent.length > 10) {
results[element.tag] = technicalContent;
logSh(`✅ Amélioré technique [${element.tag}]: "${technicalContent.substring(0, 60)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content; // Fallback
logSh(`⚠️ Fallback technique [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu technique généré
*/
cleanTechnicalContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(voici\s+)?le\s+contenu\s+amélioré\s*[:.]?\s*/gi, '');
content = content.replace(/^(avec\s+)?amélioration\s+technique\s*[:.]?\s*/gi, '');
content = content.replace(/^(bon,?\s*)?(alors,?\s*)?pour\s+/gi, '');
// Nettoyer formatage
content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
content = content.replace(/\s{2,}/g, ' '); // Espaces multiples
content = content.trim();
return content;
}
/**
* Compter différences entre contenus
*/
countDifferences(original, enhanced) {
let count = 0;
Object.keys(original).forEach(tag => {
if (enhanced[tag] && enhanced[tag] !== original[tag]) {
count++;
}
});
return count;
}
}
module.exports = { TechnicalLayer };