401 lines
12 KiB
JavaScript
401 lines
12 KiB
JavaScript
// ========================================
|
|
// ÉTAPE 3: ENHANCEMENT TRANSITIONS
|
|
// Responsabilité: Améliorer la fluidité avec Gemini
|
|
// LLM: Gemini (température 0.6)
|
|
// ========================================
|
|
|
|
const { callLLM } = require('../LLMManager');
|
|
const { logSh } = require('../ErrorReporting');
|
|
const { tracer } = require('../trace');
|
|
|
|
/**
|
|
* MAIN ENTRY POINT - ENHANCEMENT TRANSITIONS
|
|
* Input: { content: {}, csvData: {}, context: {} }
|
|
* Output: { content: {}, stats: {}, debug: {} }
|
|
*/
|
|
async function enhanceTransitions(input) {
|
|
return await tracer.run('TransitionEnhancement.enhanceTransitions()', async () => {
|
|
const { content, csvData, context = {} } = input;
|
|
|
|
await tracer.annotate({
|
|
step: '3/4',
|
|
llmProvider: 'gemini',
|
|
elementsCount: Object.keys(content).length,
|
|
mc0: csvData.mc0
|
|
});
|
|
|
|
const startTime = Date.now();
|
|
logSh(`🔗 ÉTAPE 3/4: Enhancement transitions (Gemini)`, 'INFO');
|
|
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'INFO');
|
|
|
|
try {
|
|
// 1. Analyser quels éléments ont besoin d'amélioration transitions
|
|
const elementsNeedingTransitions = analyzeTransitionNeeds(content);
|
|
|
|
logSh(` 📋 Analyse: ${elementsNeedingTransitions.length}/${Object.keys(content).length} éléments nécessitent fluidité`, 'INFO');
|
|
|
|
if (elementsNeedingTransitions.length === 0) {
|
|
logSh(`✅ ÉTAPE 3/4: Transitions déjà optimales`, 'INFO');
|
|
return {
|
|
content,
|
|
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
|
|
debug: { llmProvider: 'gemini', step: 3, enhancementsApplied: [] }
|
|
};
|
|
}
|
|
|
|
// 2. Améliorer en chunks pour Gemini
|
|
const improvedResults = await improveTransitionsInChunks(elementsNeedingTransitions, csvData);
|
|
|
|
// 3. Merger avec contenu original
|
|
const finalContent = { ...content };
|
|
let actuallyImproved = 0;
|
|
|
|
Object.keys(improvedResults).forEach(tag => {
|
|
if (improvedResults[tag] !== content[tag]) {
|
|
finalContent[tag] = improvedResults[tag];
|
|
actuallyImproved++;
|
|
}
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
const stats = {
|
|
processed: Object.keys(content).length,
|
|
enhanced: actuallyImproved,
|
|
candidate: elementsNeedingTransitions.length,
|
|
duration
|
|
};
|
|
|
|
logSh(`✅ ÉTAPE 3/4 TERMINÉE: ${stats.enhanced} éléments fluidifiés (${duration}ms)`, 'INFO');
|
|
|
|
await tracer.event(`Enhancement transitions terminé`, stats);
|
|
|
|
return {
|
|
content: finalContent,
|
|
stats,
|
|
debug: {
|
|
llmProvider: 'gemini',
|
|
step: 3,
|
|
enhancementsApplied: Object.keys(improvedResults),
|
|
transitionIssues: elementsNeedingTransitions.map(e => e.issues)
|
|
}
|
|
};
|
|
|
|
} catch (error) {
|
|
const duration = Date.now() - startTime;
|
|
logSh(`❌ ÉTAPE 3/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
|
|
|
|
// Fallback: retourner contenu original si Gemini indisponible
|
|
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
|
|
return {
|
|
content,
|
|
stats: { processed: Object.keys(content).length, enhanced: 0, duration },
|
|
debug: { llmProvider: 'gemini', step: 3, error: error.message, fallback: true }
|
|
};
|
|
}
|
|
}, input);
|
|
}
|
|
|
|
/**
|
|
* Analyser besoin d'amélioration transitions
|
|
*/
|
|
function analyzeTransitionNeeds(content) {
|
|
const elementsNeedingTransitions = [];
|
|
|
|
Object.keys(content).forEach(tag => {
|
|
const text = content[tag];
|
|
|
|
// Filtrer les éléments longs (>150 chars) qui peuvent bénéficier d'améliorations
|
|
if (text.length > 150) {
|
|
const needsTransitions = evaluateTransitionQuality(text);
|
|
|
|
if (needsTransitions.needsImprovement) {
|
|
elementsNeedingTransitions.push({
|
|
tag,
|
|
content: text,
|
|
issues: needsTransitions.issues,
|
|
score: needsTransitions.score
|
|
});
|
|
|
|
logSh(` 🔍 [${tag}]: Score=${needsTransitions.score.toFixed(2)}, Issues: ${needsTransitions.issues.join(', ')}`, 'DEBUG');
|
|
}
|
|
} else {
|
|
logSh(` ⏭️ [${tag}]: Trop court (${text.length}c), ignoré`, 'DEBUG');
|
|
}
|
|
});
|
|
|
|
// Trier par score (plus problématique en premier)
|
|
elementsNeedingTransitions.sort((a, b) => a.score - b.score);
|
|
|
|
return elementsNeedingTransitions;
|
|
}
|
|
|
|
/**
|
|
* Évaluer qualité transitions d'un texte
|
|
*/
|
|
function evaluateTransitionQuality(text) {
|
|
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
|
|
|
|
if (sentences.length < 2) {
|
|
return { needsImprovement: false, score: 1.0, issues: [] };
|
|
}
|
|
|
|
const issues = [];
|
|
let score = 1.0; // Score parfait = 1.0, problématique = 0.0
|
|
|
|
// Analyse 1: Connecteurs répétitifs
|
|
const repetitiveConnectors = analyzeRepetitiveConnectors(text);
|
|
if (repetitiveConnectors > 0.3) {
|
|
issues.push('connecteurs_répétitifs');
|
|
score -= 0.3;
|
|
}
|
|
|
|
// Analyse 2: Transitions abruptes
|
|
const abruptTransitions = analyzeAbruptTransitions(sentences);
|
|
if (abruptTransitions > 0.4) {
|
|
issues.push('transitions_abruptes');
|
|
score -= 0.4;
|
|
}
|
|
|
|
// Analyse 3: Manque de variété dans longueurs
|
|
const sentenceVariety = analyzeSentenceVariety(sentences);
|
|
if (sentenceVariety < 0.3) {
|
|
issues.push('phrases_uniformes');
|
|
score -= 0.2;
|
|
}
|
|
|
|
// Analyse 4: Trop formel ou trop familier
|
|
const formalityIssues = analyzeFormalityBalance(text);
|
|
if (formalityIssues > 0.5) {
|
|
issues.push('formalité_déséquilibrée');
|
|
score -= 0.1;
|
|
}
|
|
|
|
return {
|
|
needsImprovement: score < 0.6,
|
|
score: Math.max(0, score),
|
|
issues
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Améliorer transitions en chunks
|
|
*/
|
|
async function improveTransitionsInChunks(elementsNeedingTransitions, csvData) {
|
|
logSh(`🔄 Amélioration transitions: ${elementsNeedingTransitions.length} éléments`, 'DEBUG');
|
|
|
|
const results = {};
|
|
const chunks = chunkArray(elementsNeedingTransitions, 6); // Chunks plus petits pour Gemini
|
|
|
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
const chunk = chunks[chunkIndex];
|
|
|
|
try {
|
|
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
|
|
|
|
const improvementPrompt = createTransitionImprovementPrompt(chunk, csvData);
|
|
|
|
const improvedResponse = await callLLM('gemini', improvementPrompt, {
|
|
temperature: 0.6,
|
|
maxTokens: 2500
|
|
}, csvData.personality);
|
|
|
|
const chunkResults = parseTransitionResponse(improvedResponse, chunk);
|
|
Object.assign(results, chunkResults);
|
|
|
|
logSh(` ✅ Chunk ${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 ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
|
|
|
|
// Fallback: garder contenu original pour ce chunk
|
|
chunk.forEach(element => {
|
|
results[element.tag] = element.content;
|
|
});
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Créer prompt amélioration transitions
|
|
*/
|
|
function createTransitionImprovementPrompt(chunk, csvData) {
|
|
const personality = csvData.personality;
|
|
|
|
let prompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.
|
|
|
|
CONTEXTE: Article SEO ${csvData.mc0}
|
|
PERSONNALITÉ: ${personality?.nom} (${personality?.style} web professionnel)
|
|
CONNECTEURS PRÉFÉRÉS: ${personality?.connecteursPref}
|
|
|
|
CONTENUS À FLUIDIFIER:
|
|
|
|
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
|
|
PROBLÈMES: ${item.issues.join(', ')}
|
|
CONTENU: "${item.content}"`).join('\n\n')}
|
|
|
|
OBJECTIFS:
|
|
- Connecteurs plus naturels et variés: ${personality?.connecteursPref}
|
|
- Transitions fluides entre idées
|
|
- ÉVITE répétitions excessives ("du coup", "franchement", "par ailleurs")
|
|
- Style ${personality?.style} mais professionnel web
|
|
|
|
CONSIGNES STRICTES:
|
|
- NE CHANGE PAS le fond du message
|
|
- GARDE même structure et longueur
|
|
- Améliore SEULEMENT la fluidité
|
|
- RESPECTE le style ${personality?.nom}
|
|
|
|
FORMAT RÉPONSE:
|
|
[1] Contenu avec transitions améliorées
|
|
[2] Contenu avec transitions améliorées
|
|
etc...`;
|
|
|
|
return prompt;
|
|
}
|
|
|
|
/**
|
|
* Parser réponse amélioration transitions
|
|
*/
|
|
function parseTransitionResponse(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 improvedContent = match[2].trim();
|
|
const element = chunk[index];
|
|
|
|
// Nettoyer le contenu amélioré
|
|
improvedContent = cleanImprovedContent(improvedContent);
|
|
|
|
if (improvedContent && improvedContent.length > 10) {
|
|
results[element.tag] = improvedContent;
|
|
logSh(`✅ Improved [${element.tag}]: "${improvedContent.substring(0, 100)}..."`, 'DEBUG');
|
|
} else {
|
|
results[element.tag] = element.content;
|
|
logSh(`⚠️ Fallback [${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;
|
|
}
|
|
|
|
// ============= HELPER FUNCTIONS =============
|
|
|
|
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 hasConnector = hasTransitionWord(current);
|
|
|
|
if (!hasConnector && current.length > 30) {
|
|
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;
|
|
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
|
|
const stdDev = Math.sqrt(variance);
|
|
|
|
return Math.min(1, stdDev / avgLength);
|
|
}
|
|
|
|
function analyzeFormalityBalance(content) {
|
|
const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois'];
|
|
const casualIndicators = ['du coup', 'bon', 'franchement', 'nickel'];
|
|
|
|
let formalCount = 0;
|
|
let casualCount = 0;
|
|
|
|
formalIndicators.forEach(indicator => {
|
|
if (content.toLowerCase().includes(indicator)) formalCount++;
|
|
});
|
|
|
|
casualIndicators.forEach(indicator => {
|
|
if (content.toLowerCase().includes(indicator)) casualCount++;
|
|
});
|
|
|
|
const total = formalCount + casualCount;
|
|
if (total === 0) return 0;
|
|
|
|
// Déséquilibre si trop d'un côté
|
|
const balance = Math.abs(formalCount - casualCount) / total;
|
|
return balance;
|
|
}
|
|
|
|
function hasTransitionWord(sentence) {
|
|
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite', 'puis', 'également', 'aussi'];
|
|
return connectors.some(connector => sentence.toLowerCase().includes(connector));
|
|
}
|
|
|
|
function cleanImprovedContent(content) {
|
|
if (!content) return content;
|
|
|
|
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?/, '');
|
|
content = content.replace(/\s{2,}/g, ' ');
|
|
content = content.trim();
|
|
|
|
return content;
|
|
}
|
|
|
|
function chunkArray(array, size) {
|
|
const chunks = [];
|
|
for (let i = 0; i < array.length; i += size) {
|
|
chunks.push(array.slice(i, i + size));
|
|
}
|
|
return chunks;
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
module.exports = {
|
|
enhanceTransitions, // ← MAIN ENTRY POINT
|
|
analyzeTransitionNeeds,
|
|
evaluateTransitionQuality,
|
|
improveTransitionsInChunks,
|
|
createTransitionImprovementPrompt,
|
|
parseTransitionResponse
|
|
}; |