// ======================================== // FICHIER: SelectiveEnhancement.js - Node.js Version // Description: Enhancement par batch pour éviter timeouts // ======================================== const { callLLM } = require('./LLMManager'); const { logSh } = require('./ErrorReporting'); const { tracer } = require('./trace.js'); const { selectMultiplePersonalitiesWithAI, getPersonalities } = require('./BrainConfig'); // Utilitaire pour les délais function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * NOUVELLE APPROCHE - Multi-Personnalités Batch Enhancement * 4 personnalités différentes utilisées dans le pipeline pour maximum d'anti-détection */ async function generateWithBatchEnhancement(hierarchy, csvData) { const totalElements = Object.keys(hierarchy).length; // NOUVEAU: Sélection de 4 personnalités complémentaires const personalities = await tracer.run('SelectiveEnhancement.selectMultiplePersonalities()', async () => { const allPersonalities = await getPersonalities(); const selectedPersonalities = await selectMultiplePersonalitiesWithAI(csvData.mc0, csvData.t0, allPersonalities); await tracer.event(`4 personnalités sélectionnées: ${selectedPersonalities.map(p => p.nom).join(', ')}`); return selectedPersonalities; }, { mc0: csvData.mc0, t0: csvData.t0 }); await tracer.annotate({ totalElements, personalities: personalities.map(p => `${p.nom}(${p.style})`).join(', '), mc0: csvData.mc0 }); // ÉTAPE 1 : Génération base avec IA configurée + Personnalité 1 const baseContents = await tracer.run('SelectiveEnhancement.generateAllContentBase()', async () => { const csvDataWithPersonality1 = { ...csvData, personality: personalities[0] }; const aiProvider1 = personalities[0].aiEtape1Base; const result = await generateAllContentBase(hierarchy, csvDataWithPersonality1, aiProvider1); await tracer.event(`${Object.keys(result).length} éléments générés avec ${personalities[0].nom} via ${aiProvider1.toUpperCase()}`); return result; }, { hierarchyElements: Object.keys(hierarchy).length, personality1: personalities[0].nom, llmProvider: personalities[0].aiEtape1Base, mc0: csvData.mc0 }); // ÉTAPE 2 : Enhancement technique avec IA configurée + Personnalité 2 const technicalEnhanced = await tracer.run('SelectiveEnhancement.enhanceAllTechnicalTerms()', async () => { const csvDataWithPersonality2 = { ...csvData, personality: personalities[1] }; const aiProvider2 = personalities[1].aiEtape2Technique; const result = await enhanceAllTechnicalTerms(baseContents, csvDataWithPersonality2, aiProvider2); const enhancedCount = Object.keys(result).filter(k => result[k] !== baseContents[k]).length; await tracer.event(`${enhancedCount}/${Object.keys(result).length} éléments techniques améliorés avec ${personalities[1].nom} via ${aiProvider2.toUpperCase()}`); return result; }, { baseElements: Object.keys(baseContents).length, personality2: personalities[1].nom, llmProvider: personalities[1].aiEtape2Technique, mc0: csvData.mc0 }); // ÉTAPE 3 : Enhancement transitions avec IA configurée + Personnalité 3 const transitionsEnhanced = await tracer.run('SelectiveEnhancement.enhanceAllTransitions()', async () => { const csvDataWithPersonality3 = { ...csvData, personality: personalities[2] }; const aiProvider3 = personalities[2].aiEtape3Transitions; const result = await enhanceAllTransitions(technicalEnhanced, csvDataWithPersonality3, aiProvider3); const enhancedCount = Object.keys(result).filter(k => result[k] !== technicalEnhanced[k]).length; await tracer.event(`${enhancedCount}/${Object.keys(result).length} transitions fluidifiées avec ${personalities[2].nom} via ${aiProvider3.toUpperCase()}`); return result; }, { technicalElements: Object.keys(technicalEnhanced).length, personality3: personalities[2].nom, llmProvider: personalities[2].aiEtape3Transitions }); // ÉTAPE 4 : Enhancement style avec IA configurée + Personnalité 4 const finalContents = await tracer.run('SelectiveEnhancement.enhanceAllPersonalityStyle()', async () => { const csvDataWithPersonality4 = { ...csvData, personality: personalities[3] }; const aiProvider4 = personalities[3].aiEtape4Style; const result = await enhanceAllPersonalityStyle(transitionsEnhanced, csvDataWithPersonality4, aiProvider4); const enhancedCount = Object.keys(result).filter(k => result[k] !== transitionsEnhanced[k]).length; const avgWords = Math.round(Object.values(result).reduce((acc, content) => acc + content.split(' ').length, 0) / Object.keys(result).length); await tracer.event(`${enhancedCount}/${Object.keys(result).length} éléments stylisés avec ${personalities[3].nom} via ${aiProvider4.toUpperCase()}`, { avgWordsPerElement: avgWords }); return result; }, { transitionElements: Object.keys(transitionsEnhanced).length, personality4: personalities[3].nom, llmProvider: personalities[3].aiEtape4Style }); // Log final du DNA Mixing réussi avec IA configurables const aiChain = personalities.map((p, i) => `${p.aiEtape1Base || p.aiEtape2Technique || p.aiEtape3Transitions || p.aiEtape4Style}`.toUpperCase()).join(' → '); logSh(`✅ DNA MIXING MULTI-PERSONNALITÉS TERMINÉ:`, 'INFO'); logSh(` 🎭 4 personnalités utilisées: ${personalities.map(p => p.nom).join(' → ')}`, 'INFO'); logSh(` 🤖 IA configurées: ${personalities[0].aiEtape1Base.toUpperCase()} → ${personalities[1].aiEtape2Technique.toUpperCase()} → ${personalities[2].aiEtape3Transitions.toUpperCase()} → ${personalities[3].aiEtape4Style.toUpperCase()}`, 'INFO'); logSh(` 📝 ${Object.keys(finalContents).length} éléments avec style hybride généré`, 'INFO'); return finalContents; } /** * ÉTAPE 1 - Génération base TOUS éléments avec IA configurable */ async function generateAllContentBase(hierarchy, csvData, aiProvider) { logSh('🔍 === DEBUG GÉNÉRATION BASE ===', 'DEBUG'); // Debug: logger la hiérarchie complète logSh(`🔍 Hiérarchie reçue: ${Object.keys(hierarchy).length} sections`, 'DEBUG'); Object.keys(hierarchy).forEach((path, i) => { const section = hierarchy[path]; logSh(`🔍 Section ${i+1} [${path}]:`, 'DEBUG'); logSh(`🔍 - title: ${section.title ? section.title.originalElement?.originalTag : 'AUCUN'}`, 'DEBUG'); logSh(`🔍 - text: ${section.text ? section.text.originalElement?.originalTag : 'AUCUN'}`, 'DEBUG'); logSh(`🔍 - questions: ${section.questions?.length || 0}`, 'DEBUG'); }); const allElements = collectAllElements(hierarchy); logSh(`🔍 Éléments collectés: ${allElements.length}`, 'DEBUG'); // Debug: logger tous les éléments collectés allElements.forEach((element, i) => { logSh(`🔍 Élément ${i+1}: tag="${element.tag}", type="${element.type}"`, 'DEBUG'); }); // NOUVELLE LOGIQUE : SÉPARER PAIRES FAQ ET AUTRES ÉLÉMENTS const results = {}; logSh(`🔍 === GÉNÉRATION INTELLIGENTE DE ${allElements.length} ÉLÉMENTS ===`, 'DEBUG'); logSh(`🔍 Ordre respecté: ${allElements.map(el => el.tag.replace(/\|/g, '')).join(' → ')}`, 'DEBUG'); // 1. IDENTIFIER les paires FAQ const { faqPairs, otherElements } = separateFAQPairsAndOthers(allElements); logSh(`🔍 ${faqPairs.length} paires FAQ trouvées, ${otherElements.length} autres éléments`, 'INFO'); // 2. GÉNÉRER les autres éléments EN BATCH ORDONNÉ (titres d'abord, puis textes avec contexte) const groupedElements = groupElementsByType(otherElements); // ORDRE DE GÉNÉRATION : TITRES → TEXTES → INTRO → AUTRES const orderedTypes = ['titre', 'texte', 'intro']; for (const type of orderedTypes) { const elements = groupedElements[type]; if (!elements || elements.length === 0) continue; // DÉCOUPER EN CHUNKS DE MAX 4 ÉLÉMENTS POUR ÉVITER TIMEOUTS const chunks = chunkArray(elements, 4); logSh(`🚀 BATCH ${type.toUpperCase()}: ${elements.length} éléments en ${chunks.length} chunks`, 'INFO'); for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; logSh(` Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); try { // Passer les résultats déjà générés pour contexte (titres → textes) const batchPrompt = createBatchBasePrompt(chunk, type, csvData, results); const batchResponse = await callLLM(aiProvider, batchPrompt, { temperature: 0.7, maxTokens: 2000 * chunk.length }, csvData.personality); const batchResults = parseBatchResponse(batchResponse, chunk); Object.assign(results, batchResults); logSh(`✅ Chunk ${chunkIndex + 1}: ${Object.keys(batchResults).length}/${chunk.length} éléments générés`, 'INFO'); } catch (error) { logSh(`❌ FATAL: Chunk ${chunkIndex + 1} de ${type} échoué: ${error.message}`, 'ERROR'); throw new Error(`FATAL: Génération chunk ${chunkIndex + 1} de ${type} échouée - arrêt du workflow: ${error.message}`); } // Délai entre chunks pour éviter rate limiting if (chunkIndex < chunks.length - 1) { await sleep(1500); } } logSh(`✅ BATCH ${type.toUpperCase()} COMPLET: ${elements.length} éléments générés en ${chunks.length} chunks`, 'INFO'); } // TRAITER les types restants (autres que titre/texte/intro) for (const [type, elements] of Object.entries(groupedElements)) { if (orderedTypes.includes(type) || elements.length === 0) continue; // DÉCOUPER EN CHUNKS DE MAX 4 ÉLÉMENTS POUR ÉVITER TIMEOUTS const chunks = chunkArray(elements, 4); logSh(`🚀 BATCH ${type.toUpperCase()}: ${elements.length} éléments en ${chunks.length} chunks`, 'INFO'); for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; logSh(` Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); try { const batchPrompt = createBatchBasePrompt(chunk, type, csvData, results); const batchResponse = await callLLM(aiProvider, batchPrompt, { temperature: 0.7, maxTokens: 2000 * chunk.length }, csvData.personality); const batchResults = parseBatchResponse(batchResponse, chunk); Object.assign(results, batchResults); logSh(`✅ Chunk ${chunkIndex + 1}: ${Object.keys(batchResults).length}/${chunk.length} éléments générés`, 'INFO'); } catch (error) { logSh(`❌ FATAL: Chunk ${chunkIndex + 1} de ${type} échoué: ${error.message}`, 'ERROR'); throw new Error(`FATAL: Génération chunk ${chunkIndex + 1} de ${type} échouée - arrêt du workflow: ${error.message}`); } // Délai entre chunks if (chunkIndex < chunks.length - 1) { await sleep(1500); } } logSh(`✅ BATCH ${type.toUpperCase()} COMPLET: ${elements.length} éléments générés en ${chunks.length} chunks`, 'INFO'); } // 3. GÉNÉRER les paires FAQ ensemble (RESTAURÉ depuis .gs) if (faqPairs.length > 0) { logSh(`🔍 === GÉNÉRATION PAIRES FAQ (${faqPairs.length} paires) ===`, 'INFO'); const faqResults = await generateFAQPairsRestored(faqPairs, csvData, aiProvider); Object.assign(results, faqResults); } logSh(`🔍 === RÉSULTATS FINAUX GÉNÉRATION BASE ===`, 'DEBUG'); logSh(`🔍 Total généré: ${Object.keys(results).length} éléments`, 'DEBUG'); Object.keys(results).forEach(tag => { logSh(`🔍 [${tag}]: "${results[tag]}"`, 'DEBUG'); }); return results; } /** * ÉTAPE 2 - Enhancement technique ÉLÉMENT PAR ÉLÉMENT avec IA configurable * NOUVEAU : Traitement individuel pour fiabilité maximale et debug précis */ async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) { logSh('🔧 === DÉBUT ENHANCEMENT TECHNIQUE ===', 'INFO'); logSh('Enhancement technique BATCH TOTAL...', 'DEBUG'); const allElements = Object.keys(baseContents); if (allElements.length === 0) { logSh('⚠️ Aucun élément à analyser techniquement', 'WARNING'); return baseContents; } const analysisStart = Date.now(); logSh(`📊 Analyse démarrée: ${allElements.length} éléments à examiner`, 'INFO'); try { // ÉTAPE 1 : Extraction batch TOUS les termes techniques (1 seul appel) logSh(`🔍 Analyse technique batch: ${allElements.length} éléments`, 'INFO'); const technicalAnalysis = await extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider); const analysisEnd = Date.now(); // ÉTAPE 2 : Enhancement batch TOUS les éléments qui en ont besoin (1 seul appel) const elementsNeedingEnhancement = technicalAnalysis.filter(item => item.needsEnhancement); logSh(`📋 Analyse terminée (${analysisEnd - analysisStart}ms):`, 'INFO'); logSh(` • ${elementsNeedingEnhancement.length}/${allElements.length} éléments nécessitent enhancement`, 'INFO'); if (elementsNeedingEnhancement.length === 0) { logSh('✅ Aucun élément ne nécessite enhancement technique - contenu déjà optimal', 'INFO'); return baseContents; } // Log détaillé des éléments à améliorer elementsNeedingEnhancement.forEach((item, i) => { logSh(` ${i+1}. [${item.tag}]: ${item.technicalTerms.join(', ')}`, 'DEBUG'); }); const enhancementStart = Date.now(); logSh(`🔧 Enhancement technique: ${elementsNeedingEnhancement.length}/${allElements.length} éléments`, 'INFO'); const enhancedContents = await enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider); const enhancementEnd = Date.now(); // ÉTAPE 3 : Merger résultats const results = { ...baseContents }; let actuallyEnhanced = 0; Object.keys(enhancedContents).forEach(tag => { if (enhancedContents[tag] !== baseContents[tag]) { results[tag] = enhancedContents[tag]; actuallyEnhanced++; } }); logSh(`⚡ Enhancement terminé (${enhancementEnd - enhancementStart}ms):`, 'INFO'); logSh(` • ${actuallyEnhanced} éléments réellement améliorés`, 'INFO'); logSh(` • Termes intégrés: dibond, impression UV, fraisage, etc.`, 'DEBUG'); logSh(`✅ Enhancement technique terminé avec succès`, 'INFO'); return results; } catch (error) { const analysisTotal = Date.now() - analysisStart; logSh(`❌ FATAL: Enhancement technique échoué après ${analysisTotal}ms`, 'ERROR'); logSh(`❌ Message: ${error.message}`, 'ERROR'); throw new Error(`FATAL: Enhancement technique impossible - arrêt du workflow: ${error.message}`); } } /** * Analyser un seul élément pour détecter les termes techniques */ async function analyzeSingleElementTechnicalTerms(tag, content, csvData, aiProvider) { const prompt = `MISSION: Analyser ce contenu et déterminer s'il contient des termes techniques. CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression CONTENU À ANALYSER: TAG: ${tag} CONTENU: "${content}" CONSIGNES: - Cherche UNIQUEMENT des vrais termes techniques métier/industrie - Évite mots génériques (qualité, service, pratique, personnalisé, etc.) - Focus: matériaux, procédés, normes, dimensions, technologies spécifiques EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé, anodisation EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique, haute performance RÉPONSE REQUISE: - Si termes techniques trouvés: "OUI - termes: [liste des termes séparés par virgules]" - Si aucun terme technique: "NON" EXEMPLE: OUI - termes: aluminium composite, impression numérique, gravure laser`; try { const response = await callLLM(aiProvider, prompt, { temperature: 0.3 }); if (response.toUpperCase().startsWith('OUI')) { // Extraire les termes de la réponse const termsMatch = response.match(/termes:\s*(.+)/i); const terms = termsMatch ? termsMatch[1].trim() : ''; logSh(`✅ [${tag}] Termes techniques détectés: ${terms}`, 'DEBUG'); return true; } else { logSh(`⏭️ [${tag}] Pas de termes techniques`, 'DEBUG'); return false; } } catch (error) { logSh(`❌ ERREUR analyse ${tag}: ${error.message}`, 'ERROR'); return false; // En cas d'erreur, on skip l'enhancement } } /** * Enhancer un seul élément techniquement */ async function enhanceSingleElementTechnical(tag, content, csvData, aiProvider) { const prompt = `MISSION: Améliore ce contenu en intégrant des termes techniques précis. CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression CONTENU À AMÉLIORER: TAG: ${tag} CONTENU: "${content}" OBJECTIFS: - Remplace les termes génériques par des termes techniques précis - Ajoute des spécifications techniques réalistes - Maintient le même style et longueur - Intègre naturellement: matériaux (dibond, aluminium composite), procédés (impression UV, gravure laser), dimensions, normes EXEMPLE DE TRANSFORMATION: "matériaux haute performance" → "dibond 3mm ou aluminium composite" "impression moderne" → "impression UV haute définition" "fixation solide" → "fixation par chevilles inox Ø6mm" CONTRAINTES: - GARDE la même structure - MÊME longueur approximative - Style cohérent avec l'original - RÉPONDS DIRECTEMENT par le contenu amélioré, sans préfixe`; try { const enhancedContent = await callLLM(aiProvider, prompt, { temperature: 0.7 }); return enhancedContent.trim(); } catch (error) { logSh(`❌ ERREUR enhancement ${tag}: ${error.message}`, 'ERROR'); return content; // En cas d'erreur, on retourne le contenu original } } // ANCIENNES FONCTIONS BATCH SUPPRIMÉES - REMPLACÉES PAR TRAITEMENT INDIVIDUEL /** * NOUVELLE FONCTION : Enhancement batch TOUS les éléments */ // FONCTION SUPPRIMÉE : enhanceAllElementsTechnicalBatch() - Remplacée par traitement individuel /** * ÉTAPE 3 - Enhancement transitions BATCH avec IA configurable */ async function enhanceAllTransitions(baseContents, csvData, aiProvider) { logSh('🔗 === DÉBUT ENHANCEMENT TRANSITIONS ===', 'INFO'); logSh('Enhancement transitions batch...', 'DEBUG'); const transitionStart = Date.now(); const allElements = Object.keys(baseContents); logSh(`📊 Analyse transitions: ${allElements.length} éléments à examiner`, 'INFO'); // Sélectionner éléments longs qui bénéficient d'amélioration transitions const transitionElements = []; let analyzedCount = 0; Object.keys(baseContents).forEach(tag => { const content = baseContents[tag]; analyzedCount++; if (content.length > 150) { const needsTransitions = analyzeTransitionNeed(content); logSh(` [${tag}]: ${content.length}c, transitions=${needsTransitions ? '✅' : '❌'}`, 'DEBUG'); if (needsTransitions) { transitionElements.push({ tag: tag, content: content }); } } else { logSh(` [${tag}]: ${content.length}c - trop court, ignoré`, 'DEBUG'); } }); logSh(`📋 Analyse transitions terminée:`, 'INFO'); logSh(` • ${analyzedCount} éléments analysés`, 'INFO'); logSh(` • ${transitionElements.length} nécessitent amélioration`, 'INFO'); if (transitionElements.length === 0) { logSh('✅ Pas d\'éléments nécessitant enhancement transitions - fluidité déjà optimale', 'INFO'); return baseContents; } logSh(`${transitionElements.length} éléments à améliorer (transitions)`, 'INFO'); const chunks = chunkArray(transitionElements, 6); // Plus petit pour Gemini const results = { ...baseContents }; for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; try { logSh(`Chunk transitions ${chunkIndex + 1}/${chunks.length} (${chunk.length} éléments)`, 'DEBUG'); const batchTransitionsPrompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus. CONTEXTE: Article SEO professionnel pour site web commercial PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} adapté web) CONNECTEURS PRÉFÉRÉS: ${csvData.personality?.connecteursPref} CONTENUS: ${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} "${item.content}"`).join('\n\n')} OBJECTIFS: - Connecteurs plus naturels et variés issus de: ${csvData.personality?.connecteursPref} - Transitions fluides entre idées - ÉVITE répétitions excessives ("franchement", "du coup", "vraiment", "par ailleurs") - Style cohérent ${csvData.personality?.style} CONTRAINTES STRICTES: - NE CHANGE PAS le fond du message - GARDE la même structure et longueur approximative - Améliore SEULEMENT la fluidité des transitions - RESPECTE le style ${csvData.personality?.nom} - RÉPONDS DIRECTEMENT PAR LE CONTENU AMÉLIORÉ, sans préfixe ni tag XML FORMAT DE RÉPONSE: [1] Contenu avec transitions améliorées selon ${csvData.personality?.nom} [2] Contenu avec transitions améliorées selon ${csvData.personality?.nom} etc...`; const improved = await callLLM(aiProvider, batchTransitionsPrompt, { temperature: 0.6, maxTokens: 2500 }, csvData.personality); const parsedImprovements = parseTransitionsBatchResponse(improved, chunk); Object.keys(parsedImprovements).forEach(tag => { results[tag] = parsedImprovements[tag]; }); } catch (error) { logSh(`❌ Erreur chunk transitions ${chunkIndex + 1}: ${error.message}`, 'ERROR'); } if (chunkIndex < chunks.length - 1) { await sleep(1500); } } return results; } /** * ÉTAPE 4 - Enhancement style personnalité BATCH avec IA configurable */ async function enhanceAllPersonalityStyle(baseContents, csvData, aiProvider) { const personality = csvData.personality; if (!personality) { logSh('Pas de personnalité, skip enhancement style', 'DEBUG'); return baseContents; } logSh(`Enhancement style ${personality.nom} batch...`, 'DEBUG'); // Tous les éléments bénéficient de l'adaptation personnalité const styleElements = Object.keys(baseContents).map(tag => ({ tag: tag, content: baseContents[tag] })); const chunks = chunkArray(styleElements, 8); const results = { ...baseContents }; for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; try { logSh(`Chunk style ${chunkIndex + 1}/${chunks.length} (${chunk.length} éléments)`, 'DEBUG'); const batchStylePrompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}. CONTEXTE: Finalisation article SEO pour site e-commerce professionnel PERSONNALITÉ: ${personality.nom} DESCRIPTION: ${personality.description} STYLE CIBLE: ${personality.style} adapté au web professionnel VOCABULAIRE: ${personality.vocabulairePref} CONNECTEURS: ${personality.connecteursPref} NIVEAU TECHNIQUE: ${personality.niveauTechnique} LONGUEUR PHRASES: ${personality.longueurPhrases} CONTENUS À STYLISER: ${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} "${item.content}"`).join('\n\n')} CONSIGNES STRICTES: - GARDE le même contenu informatif et technique - Adapte SEULEMENT le ton, les expressions et le vocabulaire selon ${personality.nom} - RESPECTE la longueur approximative (même nombre de mots ±20%) - ÉVITE les répétitions excessives ("franchement", "du coup", "vraiment") - VARIE les expressions et connecteurs selon: ${personality.connecteursPref} - Style ${personality.nom} reconnaissable mais NATUREL - RÉPONDS DIRECTEMENT PAR LE CONTENU STYLISÉ, sans préfixe ni tag XML - PAS de messages d'excuse ou d'incapacité FORMAT DE RÉPONSE: [1] Contenu stylisé selon ${personality.nom} (${personality.style}) [2] Contenu stylisé selon ${personality.nom} (${personality.style}) etc...`; const styled = await callLLM(aiProvider, batchStylePrompt, { temperature: 0.8, maxTokens: 3000 }, personality); const parsedStyles = parseStyleBatchResponse(styled, chunk); Object.keys(parsedStyles).forEach(tag => { results[tag] = parsedStyles[tag]; }); } catch (error) { logSh(`❌ Erreur chunk style ${chunkIndex + 1}: ${error.message}`, 'ERROR'); } if (chunkIndex < chunks.length - 1) { await sleep(1500); } } return results; } // ============= HELPER FUNCTIONS ============= /** * Sleep function replacement for Utilities.sleep */ // FONCTION SUPPRIMÉE : sleep() dupliquée - déjà définie ligne 12 /** * RESTAURÉ DEPUIS .GS : Génération des paires FAQ cohérentes */ async function generateFAQPairsRestored(faqPairs, csvData, aiProvider) { logSh(`🔍 === GÉNÉRATION PAIRES FAQ (logique .gs restaurée) ===`, 'INFO'); if (faqPairs.length === 0) return {}; const batchPrompt = createBatchFAQPairsPrompt(faqPairs, csvData); logSh(`🔍 Prompt FAQ paires (${batchPrompt.length} chars): "${batchPrompt.substring(0, 300)}..."`, 'DEBUG'); try { const batchResponse = await callLLM(aiProvider, batchPrompt, { temperature: 0.8, maxTokens: 3000 // Plus large pour les paires }, csvData.personality); logSh(`🔍 Réponse FAQ paires reçue: ${batchResponse.length} caractères`, 'DEBUG'); logSh(`🔍 Début réponse: "${batchResponse.substring(0, 200)}..."`, 'DEBUG'); return parseFAQPairsResponse(batchResponse, faqPairs); } catch (error) { logSh(`❌ FATAL: Erreur génération paires FAQ: ${error.message}`, 'ERROR'); throw new Error(`FATAL: Génération paires FAQ échouée - arrêt du workflow: ${error.message}`); } } /** * RESTAURÉ DEPUIS .GS : Prompt pour paires FAQ cohérentes */ function createBatchFAQPairsPrompt(faqPairs, csvData) { const personality = csvData.personality; let prompt = `=== 1. CONTEXTE === Entreprise: Autocollant.fr - signalétique personnalisée Sujet: ${csvData.mc0} Section: FAQ pour article SEO commercial === 2. PERSONNALITÉ === Rédacteur: ${personality.nom} Style: ${personality.style} Ton: ${personality.description || 'professionnel'} === 3. RÈGLES GÉNÉRALES === - Questions naturelles de clients - Réponses expertes et rassurantes - Langage professionnel mais accessible - Textes rédigés humainement et de façon authentique - Couvrir: prix, livraison, personnalisation, installation, durabilité - IMPÉRATIF: Respecter strictement les contraintes XML === 4. PAIRES FAQ À GÉNÉRER === `; faqPairs.forEach((pair, index) => { const questionTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, ''); const answerTag = pair.answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, ''); prompt += `${index + 1}. [${questionTag}] + [${answerTag}] - Paire FAQ naturelle `; }); prompt += ` FORMAT DE RÉPONSE: PAIRE 1: [${faqPairs[0].question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}] Question client directe et naturelle sur ${csvData.mc0} ? [${faqPairs[0].answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}] Réponse utile et rassurante selon le style ${personality.style} de ${personality.nom}. `; if (faqPairs.length > 1) { prompt += `PAIRE 2: etc... `; } return prompt; } /** * RESTAURÉ DEPUIS .GS : Parser réponse paires FAQ */ function parseFAQPairsResponse(response, faqPairs) { const results = {}; logSh(`🔍 Parsing FAQ paires: "${response.substring(0, 300)}..."`, 'DEBUG'); // Parser avec regex [TAG] contenu const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\[|$)/gs; let match; const parsedItems = {}; while ((match = regex.exec(response)) !== null) { const tag = match[1].trim(); let content = match[2].trim().replace(/\n\s*\n/g, '\n').replace(/^\n+|\n+$/g, ''); // NOUVEAU: Appliquer le nettoyage XML pour FAQ aussi content = cleanXMLTagsFromContent(content); if (content && content.length > 0) { parsedItems[tag] = content; logSh(`🔍 Parsé [${tag}]: "${content.substring(0, 100)}..."`, 'DEBUG'); } } // Mapper aux vrais tags FAQ avec | let pairesCompletes = 0; faqPairs.forEach(pair => { const questionCleanTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, ''); const answerCleanTag = pair.answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, ''); const questionContent = parsedItems[questionCleanTag]; const answerContent = parsedItems[answerCleanTag]; if (questionContent && answerContent) { results[pair.question.tag] = questionContent; results[pair.answer.tag] = answerContent; pairesCompletes++; logSh(`✅ Paire FAQ ${pair.number} complète: Q="${questionContent}" R="${answerContent.substring(0, 50)}..."`, 'INFO'); } else { logSh(`⚠️ Paire FAQ ${pair.number} incomplète: Q=${!!questionContent} R=${!!answerContent}`, 'WARNING'); if (questionContent) results[pair.question.tag] = questionContent; if (answerContent) results[pair.answer.tag] = answerContent; } }); logSh(`📊 FAQ parsing: ${pairesCompletes}/${faqPairs.length} paires complètes`, 'INFO'); // FATAL si aucune paire complète (comme dans le .gs) if (pairesCompletes === 0 && faqPairs.length > 0) { logSh(`❌ FATAL: Aucune paire FAQ générée correctement`, 'ERROR'); throw new Error(`FATAL: Génération FAQ incomplète (0/${faqPairs.length} paires complètes) - arrêt du workflow`); } return results; } /** * RESTAURÉ DEPUIS .GS : Nettoyer instructions FAQ */ function cleanFAQInstructions(instructions, csvData) { if (!instructions) return ''; let cleanInstructions = instructions; // Remplacer variables cleanInstructions = cleanInstructions.replace(/\{\{T0\}\}/g, csvData.t0 || ''); cleanInstructions = cleanInstructions.replace(/\{\{MC0\}\}/g, csvData.mc0 || ''); cleanInstructions = cleanInstructions.replace(/\{\{T-1\}\}/g, csvData.tMinus1 || ''); cleanInstructions = cleanInstructions.replace(/\{\{L-1\}\}/g, csvData.lMinus1 || ''); // Variables multiples MC+1_X, T+1_X, L+1_X if (csvData.mcPlus1) { const mcPlus1 = csvData.mcPlus1.split(',').map(s => s.trim()); for (let i = 1; i <= 6; i++) { const mcValue = mcPlus1[i-1] || `[MC+1_${i} non défini]`; cleanInstructions = cleanInstructions.replace(new RegExp(`\\{\\{MC\\+1_${i}\\}\\}`, 'g'), mcValue); } } if (csvData.tPlus1) { const tPlus1 = csvData.tPlus1.split(',').map(s => s.trim()); for (let i = 1; i <= 6; i++) { const tValue = tPlus1[i-1] || `[T+1_${i} non défini]`; cleanInstructions = cleanInstructions.replace(new RegExp(`\\{\\{T\\+1_${i}\\}\\}`, 'g'), tValue); } } // Nettoyer HTML cleanInstructions = cleanInstructions.replace(/<\/?[^>]+>/g, ''); cleanInstructions = cleanInstructions.replace(/\s+/g, ' ').trim(); return cleanInstructions; } /** * Collecter tous les éléments dans l'ordre XML original * CORRECTION: Suit l'ordre séquentiel XML au lieu de grouper par section */ function collectAllElements(hierarchy) { const allElements = []; const tagToElementMap = {}; // 1. Créer un mapping de tous les éléments disponibles Object.keys(hierarchy).forEach(path => { const section = hierarchy[path]; if (section.title) { tagToElementMap[section.title.originalElement.originalTag] = { tag: section.title.originalElement.originalTag, element: section.title.originalElement, type: 'titre' }; } if (section.text) { tagToElementMap[section.text.originalElement.originalTag] = { tag: section.text.originalElement.originalTag, element: section.text.originalElement, type: 'texte' }; } section.questions.forEach(q => { tagToElementMap[q.originalElement.originalTag] = { tag: q.originalElement.originalTag, element: q.originalElement, type: q.originalElement.type }; }); }); // 2. Récupérer l'ordre XML original depuis le template global logSh(`🔍 Global XML Template disponible: ${!!global.currentXmlTemplate}`, 'DEBUG'); if (global.currentXmlTemplate && global.currentXmlTemplate.length > 0) { logSh(`🔍 Template XML: ${global.currentXmlTemplate.substring(0, 200)}...`, 'DEBUG'); const regex = /\|([^|]+)\|/g; let match; // Parcourir le XML dans l'ordre d'apparition while ((match = regex.exec(global.currentXmlTemplate)) !== null) { const fullMatch = match[1]; // Extraire le nom du tag (sans variables) const nameMatch = fullMatch.match(/^([^{]+)/); const tagName = nameMatch ? nameMatch[1].trim() : fullMatch.split('{')[0]; const pureTag = `|${tagName}|`; // Si cet élément existe dans notre mapping, l'ajouter dans l'ordre if (tagToElementMap[pureTag]) { allElements.push(tagToElementMap[pureTag]); logSh(`🔍 Ajouté dans l'ordre: ${pureTag}`, 'DEBUG'); delete tagToElementMap[pureTag]; // Éviter les doublons } else { logSh(`🔍 Tag XML non trouvé dans mapping: ${pureTag}`, 'DEBUG'); } } } // 3. Ajouter les éléments restants (sécurité) const remainingElements = Object.values(tagToElementMap); if (remainingElements.length > 0) { logSh(`🔍 Éléments restants ajoutés: ${remainingElements.map(el => el.tag).join(', ')}`, 'DEBUG'); remainingElements.forEach(element => { allElements.push(element); }); } logSh(`🔍 ORDRE FINAL: ${allElements.map(el => el.tag.replace(/\|/g, '')).join(' → ')}`, 'INFO'); return allElements; } /** * RESTAURÉ DEPUIS .GS : Séparer les paires FAQ des autres éléments */ function separateFAQPairsAndOthers(allElements) { const faqPairs = []; const otherElements = []; const faqQuestions = {}; const faqAnswers = {}; // 1. Collecter toutes les questions et réponses FAQ allElements.forEach(element => { if (element.type === 'faq_question') { // Extraire le numéro : |Faq_q_1| → 1 const numberMatch = element.tag.match(/(\d+)/); const faqNumber = numberMatch ? numberMatch[1] : '1'; faqQuestions[faqNumber] = element; logSh(`🔍 Question FAQ ${faqNumber} trouvée: ${element.tag}`, 'DEBUG'); } else if (element.type === 'faq_reponse') { // Extraire le numéro : |Faq_a_1| → 1 const numberMatch = element.tag.match(/(\d+)/); const faqNumber = numberMatch ? numberMatch[1] : '1'; faqAnswers[faqNumber] = element; logSh(`🔍 Réponse FAQ ${faqNumber} trouvée: ${element.tag}`, 'DEBUG'); } else { // Élément normal (titre, texte, intro, etc.) otherElements.push(element); } }); // 2. Créer les paires FAQ cohérentes Object.keys(faqQuestions).forEach(number => { const question = faqQuestions[number]; const answer = faqAnswers[number]; if (question && answer) { faqPairs.push({ number: number, question: question, answer: answer }); logSh(`✅ Paire FAQ ${number} créée: ${question.tag} + ${answer.tag}`, 'INFO'); } else if (question) { logSh(`⚠️ Question FAQ ${number} sans réponse correspondante`, 'WARNING'); otherElements.push(question); // Traiter comme élément individuel } else if (answer) { logSh(`⚠️ Réponse FAQ ${number} sans question correspondante`, 'WARNING'); otherElements.push(answer); // Traiter comme élément individuel } }); logSh(`🔍 Séparation terminée: ${faqPairs.length} paires FAQ, ${otherElements.length} autres éléments`, 'INFO'); return { faqPairs, otherElements }; } /** * Grouper éléments par type */ function groupElementsByType(elements) { const groups = {}; elements.forEach(element => { const type = element.type; if (!groups[type]) { groups[type] = []; } groups[type].push(element); }); return groups; } /** * Diviser array en chunks */ function chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } /** * Trouver le titre associé à un élément texte */ function findAssociatedTitle(textElement, existingResults) { const textName = textElement.element.name || textElement.tag; // STRATÉGIE 1: Correspondance directe (Txt_H2_1 → Titre_H2_1) const directMatch = textName.replace(/Txt_/, 'Titre_').replace(/Text_/, 'Titre_'); const directTitle = existingResults[`|${directMatch}|`] || existingResults[directMatch]; if (directTitle) return directTitle; // STRATÉGIE 2: Même niveau hiérarchique (H2, H3) const levelMatch = textName.match(/(H\d)_(\d+)/); if (levelMatch) { const [, level, number] = levelMatch; const titleTag = `Titre_${level}_${number}`; const levelTitle = existingResults[`|${titleTag}|`] || existingResults[titleTag]; if (levelTitle) return levelTitle; } // STRATÉGIE 3: Proximité dans l'ordre (texte suivant un titre) const allTitles = Object.entries(existingResults) .filter(([tag]) => tag.includes('Titre')) .sort(([a], [b]) => a.localeCompare(b)); if (allTitles.length > 0) { // Retourner le premier titre disponible comme contexte général return allTitles[0][1]; } return null; } /** * Créer prompt batch de base */ function createBatchBasePrompt(elements, type, csvData, existingResults = {}) { const personality = csvData.personality; let prompt = `=== 1. CONTEXTE === Entreprise: Autocollant.fr - signalétique personnalisée Sujet: ${csvData.mc0} Type d'article: SEO professionnel pour site commercial === 2. PERSONNALITÉ === Rédacteur: ${personality.nom} Style: ${personality.style} Ton: ${personality.description || 'professionnel'} === 3. RÈGLES GÉNÉRALES === - Contenu SEO optimisé - Langage naturel et fluide - Éviter répétitions - Pas de références techniques dans le contenu - Textes rédigés humainement et de façon authentique - IMPÉRATIF: Respecter strictement les contraintes XML (nombre de mots, etc.) === 4. ÉLÉMENTS À GÉNÉRER === `; // AJOUTER CONTEXTE DES TITRES POUR LES TEXTES if (type === 'texte' && Object.keys(existingResults).length > 0) { const generatedTitles = Object.entries(existingResults) .filter(([tag]) => tag.includes('Titre')) .map(([tag, title]) => `• ${tag.replace(/\|/g, '')}: "${title}"`) .slice(0, 5); // Limiter à 5 titres pour éviter surcharge if (generatedTitles.length > 0) { prompt += ` Titres existants pour contexte: ${generatedTitles.join('\n')} `; } } elements.forEach((elementInfo, index) => { const cleanTag = elementInfo.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, ''); prompt += `${index + 1}. [${cleanTag}] `; // INSTRUCTIONS PROPRES PAR ÉLÉMENT if (type === 'titre') { if (elementInfo.element.type === 'titre_h1') { prompt += `Titre principal accrocheur\n`; } else if (elementInfo.element.type === 'titre_h2') { prompt += `Titre de section engageant\n`; } else if (elementInfo.element.type === 'titre_h3') { prompt += `Sous-titre spécialisé\n`; } else { prompt += `Titre pertinent\n`; } } else if (type === 'texte') { prompt += `Paragraphe informatif\n`; // ASSOCIER LE TITRE CORRESPONDANT AUTOMATIQUEMENT const associatedTitle = findAssociatedTitle(elementInfo, existingResults); if (associatedTitle) { prompt += ` Contexte: "${associatedTitle}"\n`; } if (elementInfo.element.resolvedContent) { prompt += ` Angle: "${elementInfo.element.resolvedContent}"\n`; } } else if (type === 'intro') { prompt += `Introduction engageante\n`; } else { prompt += `Contenu pertinent\n`; } }); prompt += `\nSTYLE ${personality.nom.toUpperCase()} - ${personality.style}: - Vocabulaire: ${personality.vocabulairePref} - Connecteurs: ${personality.connecteursPref} - Phrases: ${personality.longueurPhrases} - Niveau technique: ${personality.niveauTechnique} CONSIGNES STRICTES POUR ARTICLE SEO: - CONTEXTE: Article professionnel pour site e-commerce, destiné aux clients potentiels - STYLE: ${personality.style} de ${personality.nom} mais ADAPTÉ au web professionnel - INTERDICTION ABSOLUE: expressions trop familières répétées ("du coup", "bon", "franchement", "nickel", "tip-top") - VOCABULAIRE: Mélange expertise technique + accessibilité client - SEO: Utilise naturellement "${csvData.mc0}" et termes associés - POUR LES TITRES: Titre SEO attractif UNIQUEMENT, JAMAIS "Titre_H1_1" ou "Titre_H2_7" - EXEMPLE TITRE: "Plaques personnalisées résistantes : guide complet 2024" - CONTENU: Informatif, rassurant, incite à l'achat SANS être trop commercial - RÉPONDS DIRECTEMENT par le contenu web demandé, SANS préfixe FORMAT DE RÉPONSE ${type === 'titre' ? '(TITRES UNIQUEMENT)' : ''}: [${elements[0].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}] ${type === 'titre' ? 'Titre réel et attractif (PAS "Titre_H1_1")' : 'Contenu rédigé selon le style ' + personality.nom} [${elements[1] ? elements[1].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '') : 'element2'}] ${type === 'titre' ? 'Titre réel et attractif (PAS "Titre_H2_1")' : 'Contenu rédigé selon le style ' + personality.nom} etc...`; return prompt; } /** * Parser réponse batch générique avec nettoyage des tags XML */ function parseBatchResponse(response, elements) { const results = {}; // Parser avec regex [TAG] contenu const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs; let match; const parsedItems = {}; while ((match = regex.exec(response)) !== null) { const tag = match[1].trim(); let content = match[2].trim(); // NOUVEAU: Nettoyer les tags XML qui peuvent apparaître dans le contenu content = cleanXMLTagsFromContent(content); parsedItems[tag] = content; } // Mapper aux vrais tags avec | elements.forEach(element => { const cleanTag = element.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, ''); if (parsedItems[cleanTag] && parsedItems[cleanTag].length > 10) { results[element.tag] = parsedItems[cleanTag]; logSh(`✅ Parsé [${cleanTag}]: "${parsedItems[cleanTag].substring(0, 100)}..."`, 'DEBUG'); } else { // Fallback si parsing échoue ou contenu trop court results[element.tag] = `Contenu professionnel pour ${element.element.name}`; logSh(`⚠️ Fallback [${cleanTag}]: parsing échoué ou contenu invalide`, 'WARNING'); } }); return results; } /** * NOUVELLE FONCTION: Nettoyer les tags XML du contenu généré */ function cleanXMLTagsFromContent(content) { if (!content) return content; // Supprimer les tags XML avec ** content = content.replace(/\*\*[^*]+\*\*/g, ''); // Supprimer les préfixes de titres indésirables content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?(voici\s+le\s+topo\s+pour\s+)?Titre_[HU]\d+_\d+[.,\s]*/gi, ''); content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?pour\s+Titre_[HU]\d+_\d+[.,\s]*/gi, ''); content = content.replace(/^(Bon,?\s*)?(donc,?\s*)?Titre_[HU]\d+_\d+[.,\s]*/gi, ''); // Supprimer les messages d'excuse content = content.replace(/Oh là là,?\s*je\s*(suis\s*)?(\w+\s*)?désolée?\s*,?\s*mais\s*je\s*n'ai\s*pas\s*l'information.*?(?=\.|$)/gi, ''); content = content.replace(/Bon,?\s*passons\s*au\s*suivant.*?(?=\.|$)/gi, ''); content = content.replace(/je\s*ne\s*sais\s*pas\s*quoi\s*vous\s*dire.*?(?=\.|$)/gi, ''); content = content.replace(/encore\s*un\s*point\s*où\s*je\s*n'ai\s*pas\s*l'information.*?(?=\.|$)/gi, ''); // Réduire les répétitions excessives d'expressions familières content = content.replace(/(du coup[,\s]+){3,}/gi, 'du coup '); content = content.replace(/(bon[,\s]+){3,}/gi, 'bon '); content = content.replace(/(franchement[,\s]+){3,}/gi, 'franchement '); content = content.replace(/(alors[,\s]+){3,}/gi, 'alors '); content = content.replace(/(nickel[,\s]+){2,}/gi, 'nickel '); content = content.replace(/(tip-top[,\s]+){2,}/gi, 'tip-top '); content = content.replace(/(costaud[,\s]+){2,}/gi, 'costaud '); // Nettoyer espaces multiples et retours ligne content = content.replace(/\s{2,}/g, ' '); content = content.replace(/\n{2,}/g, '\n'); content = content.trim(); return content; } // ============= PARSING FUNCTIONS ============= // FONCTION SUPPRIMÉE : parseAllTechnicalTermsResponse() - Parser batch défaillant remplacé par traitement individuel // FONCTIONS SUPPRIMÉES : parseTechnicalEnhancementBatchResponse() et parseTechnicalBatchResponse() - Remplacées par traitement individuel // Placeholder pour les fonctions de parsing conservées qui suivent function parseTransitionsBatchResponse(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 content = match[2].trim(); // Appliquer le nettoyage XML content = cleanXMLTagsFromContent(content); if (content && content.length > 10) { results[chunk[index].tag] = content; } else { // Fallback si contenu invalide results[chunk[index].tag] = chunk[index].content; // Garder contenu original } index++; } return results; } function parseStyleBatchResponse(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 content = match[2].trim(); // Appliquer le nettoyage XML content = cleanXMLTagsFromContent(content); if (content && content.length > 10) { results[chunk[index].tag] = content; } else { // Fallback si contenu invalide results[chunk[index].tag] = chunk[index].content; // Garder contenu original } index++; } return results; } // ============= ANALYSIS FUNCTIONS ============= /** * Analyser besoin d'amélioration transitions */ function analyzeTransitionNeed(content) { const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10); // Critères multiples d'analyse const metrics = { repetitiveConnectors: analyzeRepetitiveConnectors(content), abruptTransitions: analyzeAbruptTransitions(sentences), sentenceVariety: analyzeSentenceVariety(sentences), formalityLevel: analyzeFormalityLevel(content), overallLength: content.length }; // Score de besoin (0-1) let needScore = 0; needScore += metrics.repetitiveConnectors * 0.3; needScore += metrics.abruptTransitions * 0.4; needScore += (1 - metrics.sentenceVariety) * 0.2; needScore += metrics.formalityLevel * 0.1; // Seuil ajustable selon longueur const threshold = metrics.overallLength > 300 ? 0.4 : 0.6; logSh(`🔍 Analyse transitions: score=${needScore.toFixed(2)}, seuil=${threshold}`, 'DEBUG'); return needScore > threshold; } function analyzeRepetitiveConnectors(content) { const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc']; let totalConnectors = 0; let repetitions = 0; connectors.forEach(connector => { const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []); totalConnectors += matches.length; if (matches.length > 1) repetitions += matches.length - 1; }); return totalConnectors > 0 ? repetitions / totalConnectors : 0; } function analyzeAbruptTransitions(sentences) { if (sentences.length < 2) return 0; let abruptCount = 0; for (let i = 1; i < sentences.length; i++) { const current = sentences[i].trim(); const previous = sentences[i-1].trim(); const hasConnector = hasTransitionWord(current); const topicContinuity = calculateTopicContinuity(previous, current); // Transition abrupte = pas de connecteur + faible continuité thématique if (!hasConnector && topicContinuity < 0.3) { abruptCount++; } } return abruptCount / (sentences.length - 1); } function analyzeSentenceVariety(sentences) { if (sentences.length < 2) return 1; const lengths = sentences.map(s => s.trim().length); const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length; // Calculer variance des longueurs const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length; const stdDev = Math.sqrt(variance); // Score de variété (0-1) - plus la variance est élevée, plus c'est varié return Math.min(1, stdDev / avgLength); } function analyzeFormalityLevel(content) { const formalIndicators = [ 'il convient de', 'par conséquent', 'néanmoins', 'toutefois', 'de surcroît', 'en définitive', 'il s\'avère que', 'force est de constater' ]; let formalCount = 0; formalIndicators.forEach(indicator => { if (content.toLowerCase().includes(indicator)) formalCount++; }); const sentences = content.split(/[.!?]+/).length; return sentences > 0 ? formalCount / sentences : 0; } function calculateTopicContinuity(sentence1, sentence2) { const stopWords = ['les', 'des', 'une', 'sont', 'avec', 'pour', 'dans', 'cette', 'vous', 'peut', 'tout']; const words1 = extractSignificantWords(sentence1, stopWords); const words2 = extractSignificantWords(sentence2, stopWords); if (words1.length === 0 || words2.length === 0) return 0; const commonWords = words1.filter(word => words2.includes(word)); const semanticSimilarity = commonWords.length / Math.min(words1.length, words2.length); const technicalWords = ['plaque', 'dibond', 'aluminium', 'impression', 'signalétique']; const commonTechnical = commonWords.filter(word => technicalWords.includes(word)); const technicalBonus = commonTechnical.length * 0.2; return Math.min(1, semanticSimilarity + technicalBonus); } function extractSignificantWords(sentence, stopWords) { return sentence.toLowerCase() .match(/\b[a-zàâäéèêëïîôùûüÿç]{4,}\b/g) // Mots 4+ lettres avec accents ?.filter(word => !stopWords.includes(word)) || []; } function hasTransitionWord(sentence) { const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite', 'puis', 'également', 'aussi', 'toutefois', 'néanmoins', 'alors', 'enfin']; return connectors.some(connector => sentence.toLowerCase().includes(connector)); } /** * Instructions de style dynamiques */ function getPersonalityStyleInstructions(personality) { // CORRECTION: Utilisation des VRAIS champs Google Sheets au lieu du hardcodé if (!personality) return "Style professionnel standard"; const instructions = `STYLE ${personality.nom.toUpperCase()} (${personality.style}): - Description: ${personality.description} - Vocabulaire préféré: ${personality.vocabulairePref || 'professionnel, qualité'} - Connecteurs préférés: ${personality.connecteursPref || 'par ailleurs, en effet'} - Mots-clés secteurs: ${personality.motsClesSecteurs || 'technique, qualité'} - Longueur phrases: ${personality.longueurPhrases || 'Moyennes (15-25 mots)'} - Niveau technique: ${personality.niveauTechnique || 'Accessible'} - Style CTA: ${personality.ctaStyle || 'Professionnel'} - Défauts simulés: ${personality.defautsSimules || 'Aucun'} - Erreurs typiques à éviter: ${personality.erreursTypiques || 'Répétitions, généralités'}`; return instructions; } /** * Créer prompt pour élément (fonction de base nécessaire) */ function createPromptForElement(element, csvData) { const personality = csvData.personality; const styleContext = `Rédige dans le style ${personality.style} de ${personality.nom} (${personality.description}).`; switch (element.type) { case 'titre_h1': return `${styleContext} MISSION: Crée un titre H1 accrocheur pour: ${csvData.mc0} Référence: ${csvData.t0} CONSIGNES: 10 mots maximum, direct et impactant, optimisé SEO. RÉPONDS UNIQUEMENT PAR LE TITRE, sans introduction.`; case 'titre_h2': return `${styleContext} MISSION: Crée un titre H2 optimisé SEO pour: ${csvData.mc0} CONSIGNES: Intègre naturellement le mot-clé, 8 mots maximum. RÉPONDS UNIQUEMENT PAR LE TITRE, sans introduction.`; case 'intro': if (element.instructions) { return `${styleContext} MISSION: ${element.instructions} Données contextuelles: - MC0: ${csvData.mc0} - T-1: ${csvData.tMinus1} - L-1: ${csvData.lMinus1} RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`; } return `${styleContext} MISSION: Rédige une introduction de 100 mots pour ${csvData.mc0}. RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`; case 'texte': if (element.instructions) { return `${styleContext} MISSION: ${element.instructions} RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`; } return `${styleContext} MISSION: Rédige un paragraphe de 150 mots sur ${csvData.mc0}. RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`; case 'faq_question': if (element.instructions) { return `${styleContext} MISSION: ${element.instructions} CONTEXTE: ${csvData.mc0} - ${csvData.t0} STYLE: Question ${csvData.personality?.style} de ${csvData.personality?.nom} CONSIGNES: - Vraie question que se poserait un client intéressé par ${csvData.mc0} - Commence par "Comment", "Quel", "Pourquoi", "Où", "Quand" ou "Est-ce que" - Maximum 15 mots, pratique et concrète - Vocabulaire: ${csvData.personality?.vocabulairePref || 'accessible'} RÉPONDS UNIQUEMENT PAR LA QUESTION, sans guillemets ni introduction.`; } return `${styleContext} MISSION: Génère une vraie question FAQ client sur ${csvData.mc0}. CONSIGNES: - Question pratique et concrète qu'un client se poserait - Commence par "Comment", "Quel", "Pourquoi", "Combien", "Où" ou "Est-ce que" - Maximum 15 mots, style ${csvData.personality?.style} - Vocabulaire: ${csvData.personality?.vocabulairePref || 'accessible'} RÉPONDS UNIQUEMENT PAR LA QUESTION, sans guillemets ni introduction.`; case 'faq_reponse': if (element.instructions) { return `${styleContext} MISSION: ${element.instructions} CONTEXTE: ${csvData.mc0} - ${csvData.t0} STYLE: Réponse ${csvData.personality?.style} de ${csvData.personality?.nom} CONSIGNES: - Réponse utile et rassurante - 50-80 mots, ton ${csvData.personality?.style} - Vocabulaire: ${csvData.personality?.vocabulairePref} - Connecteurs: ${csvData.personality?.connecteursPref} RÉPONDS UNIQUEMENT PAR LA RÉPONSE, sans introduction.`; } return `${styleContext} MISSION: Réponds à une question client sur ${csvData.mc0}. CONSIGNES: - Réponse utile, claire et rassurante - 50-80 mots, ton ${csvData.personality?.style} de ${csvData.personality?.nom} - Vocabulaire: ${csvData.personality?.vocabulairePref || 'professionnel'} - Connecteurs: ${csvData.personality?.connecteursPref || 'par ailleurs'} RÉPONDS UNIQUEMENT PAR LA RÉPONSE, sans introduction.`; default: return `${styleContext} MISSION: Génère du contenu pertinent pour ${csvData.mc0}. RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`; } } /** * NOUVELLE FONCTION : Extraction batch TOUS les termes techniques */ async function extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider) { const contentEntries = Object.keys(baseContents); const batchAnalysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques. CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression CONTENUS À ANALYSER: ${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag} CONTENU: "${baseContents[tag]}"`).join('\n\n')} CONSIGNES: - Identifie UNIQUEMENT les vrais termes techniques métier/industrie - Évite mots génériques (qualité, service, pratique, personnalisé, etc.) - Focus: matériaux, procédés, normes, dimensions, technologies - Si aucun terme technique → "AUCUN" EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique FORMAT RÉPONSE EXACT: [1] dibond, impression UV, 3mm OU AUCUN [2] aluminium, fraisage CNC OU AUCUN [3] AUCUN etc... (${contentEntries.length} lignes total)`; try { const analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, { temperature: 0.3, maxTokens: 2000 }, csvData.personality); return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries); } catch (error) { logSh(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR'); throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`); } } /** * NOUVELLE FONCTION : Enhancement batch TOUS les éléments */ async function enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider) { if (elementsNeedingEnhancement.length === 0) return {}; const batchEnhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces ${elementsNeedingEnhancement.length} contenus. CONTEXTE: Article SEO pour site e-commerce de signalétique PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} web professionnel) SUJET: ${csvData.mc0} - Secteur: Signalétique/impression VOCABULAIRE PRÉFÉRÉ: ${csvData.personality?.vocabulairePref} CONTENUS + TERMES À AMÉLIORER: ${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag} CONTENU ACTUEL: "${item.content}" TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')} CONSIGNES STRICTES: - Améliore UNIQUEMENT la précision technique, garde le style ${csvData.personality?.nom} - GARDE la même longueur, structure et ton - Intègre naturellement les termes techniques listés - NE CHANGE PAS le fond du message ni le style personnel - Utilise un vocabulaire expert mais accessible - ÉVITE les répétitions excessives - RESPECTE le niveau technique: ${csvData.personality?.niveauTechnique} - Termes techniques secteur: dibond, aluminium, impression UV, fraisage, épaisseur, PMMA FORMAT RÉPONSE: [1] Contenu avec amélioration technique selon ${csvData.personality?.nom} [2] Contenu avec amélioration technique selon ${csvData.personality?.nom} etc... (${elementsNeedingEnhancement.length} éléments total)`; try { const enhanced = await callLLM(aiProvider, batchEnhancementPrompt, { temperature: 0.4, maxTokens: 5000 // Plus large pour batch total }, csvData.personality); return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement); } catch (error) { logSh(`❌ FATAL: Enhancement technique batch échoué: ${error.message}`, 'ERROR'); throw new Error(`FATAL: Enhancement technique batch impossible - arrêt du workflow: ${error.message}`); } } /** * Parser réponse extraction termes */ function parseAllTechnicalTermsResponse(response, baseContents, contentEntries) { const results = []; const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs; let match; const parsedItems = {}; // Parser la réponse while ((match = regex.exec(response)) !== null) { const index = parseInt(match[1]) - 1; // Convertir en 0-indexé const termsText = match[2].trim(); parsedItems[index] = termsText; } // Mapper aux éléments contentEntries.forEach((tag, index) => { const termsText = parsedItems[index] || 'AUCUN'; const hasTerms = !termsText.toUpperCase().includes('AUCUN'); const technicalTerms = hasTerms ? termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) : []; results.push({ tag: tag, content: baseContents[tag], technicalTerms: technicalTerms, needsEnhancement: hasTerms && technicalTerms.length > 0 }); logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'pas de termes techniques'}`, 'DEBUG'); }); const enhancementCount = results.filter(r => r.needsEnhancement).length; logSh(`📊 Analyse terminée: ${enhancementCount}/${contentEntries.length} éléments ont besoin d'enhancement`, 'INFO'); return results; } /** * Parser réponse enhancement technique */ function parseTechnicalEnhancementBatchResponse(response, elementsNeedingEnhancement) { const results = {}; const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs; let match; let index = 0; while ((match = regex.exec(response)) && index < elementsNeedingEnhancement.length) { let content = match[2].trim(); const element = elementsNeedingEnhancement[index]; // NOUVEAU: Appliquer le nettoyage XML content = cleanXMLTagsFromContent(content); if (content && content.length > 10) { results[element.tag] = content; logSh(`✅ Enhanced [${element.tag}]: "${content.substring(0, 100)}..."`, 'DEBUG'); } else { // Fallback si contenu invalide après nettoyage results[element.tag] = element.content; logSh(`⚠️ Fallback [${element.tag}]: contenu invalide après nettoyage`, 'WARNING'); } index++; } // Vérifier si on a bien tout parsé if (Object.keys(results).length < elementsNeedingEnhancement.length) { logSh(`⚠️ Parsing partiel: ${Object.keys(results).length}/${elementsNeedingEnhancement.length}`, 'WARNING'); // Compléter avec contenu original pour les manquants elementsNeedingEnhancement.forEach(element => { if (!results[element.tag]) { results[element.tag] = element.content; } }); } return results; } // ============= EXPORTS ============= module.exports = { generateWithBatchEnhancement, generateAllContentBase, enhanceAllTechnicalTerms, enhanceAllTransitions, enhanceAllPersonalityStyle, collectAllElements, groupElementsByType, chunkArray, createBatchBasePrompt, parseBatchResponse, cleanXMLTagsFromContent, analyzeTransitionNeed, getPersonalityStyleInstructions, createPromptForElement, sleep, separateFAQPairsAndOthers, generateFAQPairsRestored, createBatchFAQPairsPrompt, parseFAQPairsResponse, cleanFAQInstructions, extractAllTechnicalTermsBatch, enhanceAllElementsTechnicalBatch, parseAllTechnicalTermsResponse, parseTechnicalEnhancementBatchResponse };