// ======================================== // 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 = 'gpt4'; 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'); 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'] })); 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 };