feat: Améliore système de génération avec personnalités enrichies et détection intelligente
Ajouts majeurs: - Détection intelligente des contraintes de longueur dans les instructions (évite conflits entre instruction et type) - Contexte de personnalité enrichi: colonnes B, D, E, F, H, I, J du Google Sheet - Randomisation des éléments de personnalité (max 2 par catégorie pour variabilité) - Injection du titre associé dans le prompt des textes/intros pour cohérence - Exclusion de personnalité pour questions FAQ (seulement réponses) Corrections: - Fix détection type: vérification préfixes (Intro_, Titre_, Txt_) avant suffixes (_title, _text) - Fix parsing vocabulairePref pour gérer arrays et strings - Logs détaillés sur détection de contraintes Impact: - Meilleure cohérence titre→texte - Plus de variabilité anti-détection - Respect strict des contraintes de longueur spécifiées - Personnalités mieux contextualisées dans les prompts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
acb993cde4
commit
602d06ba21
@ -15,7 +15,7 @@ const { chunkArray, sleep } = require('../selective-enhancement/SelectiveUtils')
|
||||
class InitialGenerationLayer {
|
||||
constructor() {
|
||||
this.name = 'InitialGeneration';
|
||||
this.defaultLLM = 'claude';
|
||||
this.defaultLLM = 'claude-sonnet-4-5';
|
||||
this.priority = 0; // Priorité maximale - appliqué en premier
|
||||
}
|
||||
|
||||
@ -132,7 +132,15 @@ class InitialGenerationLayer {
|
||||
detectElementType(tag) {
|
||||
const tagLower = tag.toLowerCase();
|
||||
|
||||
if (tagLower.includes('titre') || tagLower.includes('h1') || tagLower.includes('h2')) {
|
||||
// 🔥 FIX: Vérifier d'abord les suffixes _title vs _text pour éviter confusion
|
||||
if (tagLower.endsWith('_title')) {
|
||||
return 'titre';
|
||||
} else if (tagLower.endsWith('_text')) {
|
||||
return 'contenu';
|
||||
}
|
||||
|
||||
// Legacy patterns (pour compatibilité)
|
||||
if (tagLower.includes('titre_h') || tagLower === 'titre_h1' || tagLower.startsWith('titre_')) {
|
||||
return 'titre';
|
||||
} else if (tagLower.includes('intro') || tagLower.includes('introduction')) {
|
||||
return 'introduction';
|
||||
@ -140,6 +148,8 @@ class InitialGenerationLayer {
|
||||
return 'conclusion';
|
||||
} else if (tagLower.includes('faq') || tagLower.includes('question')) {
|
||||
return 'faq';
|
||||
} else if (tagLower.startsWith('txt_') || tagLower.includes('_text')) {
|
||||
return 'contenu';
|
||||
} else {
|
||||
return 'contenu';
|
||||
}
|
||||
|
||||
@ -154,7 +154,12 @@ function analyzeStyleConsistency(content, expectedPersonality = null) {
|
||||
|
||||
// 1. Analyser alignement personnalité
|
||||
if (expectedPersonality && expectedPersonality.vocabulairePref) {
|
||||
const personalityWords = expectedPersonality.vocabulairePref.toLowerCase().split(',');
|
||||
// Convertir en string si ce n'est pas déjà le cas
|
||||
const vocabPref = typeof expectedPersonality.vocabulairePref === 'string'
|
||||
? expectedPersonality.vocabulairePref
|
||||
: String(expectedPersonality.vocabulairePref);
|
||||
|
||||
const personalityWords = vocabPref.toLowerCase().split(',');
|
||||
const contentLower = content.toLowerCase();
|
||||
|
||||
personalityWords.forEach(word => {
|
||||
@ -483,15 +488,307 @@ function formatDuration(ms) {
|
||||
* GÉNÉRATION SIMPLE (REMPLACE CONTENTGENERATION.JS)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Détecter le type d'élément (legacy InitialGeneration.js logic)
|
||||
* Retourne un string simple : 'titre', 'intro', 'paragraphe', 'faq_question', 'faq_reponse', 'conclusion'
|
||||
*/
|
||||
function detectElementType(tag) {
|
||||
const tagLower = tag.toLowerCase();
|
||||
|
||||
// 🔥 FIX: Vérifier d'abord les préfixes de type spécifique (Intro_, Titre_, Txt_)
|
||||
// avant les suffixes génériques (_title, _text)
|
||||
|
||||
// Intro_H2_1, Intro_H3_5, etc. → type 'intro'
|
||||
if (tagLower.startsWith('intro_')) {
|
||||
return 'intro';
|
||||
}
|
||||
|
||||
// Titre_H2_1, Titre_H3_5, etc. → type 'titre'
|
||||
if (tagLower.startsWith('titre_')) {
|
||||
return 'titre';
|
||||
}
|
||||
|
||||
// Txt_H2_1, Txt_H3_5, etc. → type 'paragraphe'
|
||||
if (tagLower.startsWith('txt_')) {
|
||||
return 'paragraphe';
|
||||
}
|
||||
|
||||
// Conclusion_* → type 'conclusion'
|
||||
if (tagLower.startsWith('conclu') || tagLower.includes('c_1') || tagLower === 'c1') {
|
||||
return 'conclusion';
|
||||
}
|
||||
|
||||
// FAQ
|
||||
if (tagLower.includes('faq') || tagLower.includes('question') || tagLower.startsWith('q_') || tagLower.startsWith('q-')) {
|
||||
return 'faq_question';
|
||||
}
|
||||
|
||||
if (tagLower.includes('answer') || tagLower.includes('réponse') || tagLower.includes('reponse') || tagLower.startsWith('a_') || tagLower.startsWith('a-')) {
|
||||
return 'faq_reponse';
|
||||
}
|
||||
|
||||
// Suffixes génériques pour format alternatif (H2_1_title, H2_1_text)
|
||||
// À vérifier APRÈS les préfixes pour éviter les conflits
|
||||
if (tagLower.endsWith('_title')) {
|
||||
return 'titre';
|
||||
} else if (tagLower.endsWith('_text')) {
|
||||
return 'paragraphe';
|
||||
}
|
||||
|
||||
// Paragraphes (défaut)
|
||||
return 'paragraphe';
|
||||
}
|
||||
|
||||
/**
|
||||
* Détecter contrainte de longueur dans une instruction
|
||||
* Retourne { hasConstraint: boolean, constraint: string|null }
|
||||
*/
|
||||
function detectLengthConstraintInInstruction(instruction) {
|
||||
if (!instruction) return { hasConstraint: false, constraint: null };
|
||||
|
||||
const lowerInstr = instruction.toLowerCase();
|
||||
|
||||
// Patterns de contraintes : "X mots", "X-Y mots", "environ X mots", "maximum X mots"
|
||||
const patterns = [
|
||||
/(\d+)\s*-\s*(\d+)\s*mots?/i, // "80-200 mots"
|
||||
/environ\s+(\d+)\s*mots?/i, // "environ 100 mots"
|
||||
/maximum\s+(\d+)\s*mots?/i, // "maximum 25 mots"
|
||||
/minimum\s+(\d+)\s*mots?/i, // "minimum 50 mots"
|
||||
/(\d+)\s+mots?\s+(maximum|minimum)/i, // "25 mots maximum"
|
||||
/^(\d+)\s*mots?$/i, // "25 mots" seul
|
||||
/\b(\d+)\s*mots?\b/i // "X mots" quelque part
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = instruction.match(pattern);
|
||||
if (match) {
|
||||
return { hasConstraint: true, constraint: match[0] };
|
||||
}
|
||||
}
|
||||
|
||||
return { hasConstraint: false, constraint: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Créer un prompt adapté au type d'élément avec contraintes de longueur (legacy logic)
|
||||
* @param {string} associatedTitle - Titre généré précédemment pour les textes/intros (important pour cohérence)
|
||||
*/
|
||||
function createTypedPrompt(tag, type, instruction, csvData, associatedTitle = null) {
|
||||
const keyword = csvData.mc0 || '';
|
||||
const title = csvData.t0 || '';
|
||||
const personality = csvData.personality;
|
||||
|
||||
// 🔥 NOUVEAU : Détecter si l'instruction contient déjà une contrainte de longueur
|
||||
const instructionConstraint = detectLengthConstraintInInstruction(instruction);
|
||||
|
||||
// 📊 LOG: Afficher détection contrainte
|
||||
if (instructionConstraint.hasConstraint) {
|
||||
logSh(` 🔍 Contrainte détectée dans instruction: "${instructionConstraint.constraint}"`, 'DEBUG');
|
||||
} else {
|
||||
logSh(` ⚙️ Aucune contrainte détectée, utilisation contrainte type "${type}"`, 'DEBUG');
|
||||
}
|
||||
|
||||
let lengthConstraint = '';
|
||||
let specificInstructions = '';
|
||||
|
||||
// Si l'instruction a déjà une contrainte, ne pas en ajouter une autre
|
||||
if (instructionConstraint.hasConstraint) {
|
||||
lengthConstraint = `RESPECTE STRICTEMENT la contrainte de longueur indiquée dans l'instruction : "${instructionConstraint.constraint}"`;
|
||||
|
||||
// Instructions génériques selon type (sans répéter la longueur)
|
||||
switch (type) {
|
||||
case 'titre':
|
||||
specificInstructions = `Le titre doit être:
|
||||
- COURT et PERCUTANT
|
||||
- Pas de phrases complètes
|
||||
- Intégrer "${keyword}"`;
|
||||
break;
|
||||
|
||||
case 'intro':
|
||||
specificInstructions = `L'introduction doit:
|
||||
- Présenter le sujet
|
||||
- Accrocher le lecteur`;
|
||||
break;
|
||||
|
||||
case 'conclusion':
|
||||
specificInstructions = `La conclusion doit:
|
||||
- Résumer les points clés
|
||||
- Appel à l'action si pertinent`;
|
||||
break;
|
||||
|
||||
case 'faq_question':
|
||||
specificInstructions = `La question FAQ doit être:
|
||||
- Courte et directe
|
||||
- Formulée du point de vue utilisateur`;
|
||||
break;
|
||||
|
||||
case 'faq_reponse':
|
||||
specificInstructions = `La réponse FAQ doit être:
|
||||
- Directe et informative
|
||||
- Répondre précisément à la question`;
|
||||
break;
|
||||
|
||||
case 'paragraphe':
|
||||
default:
|
||||
specificInstructions = `Le paragraphe doit:
|
||||
- Développer un aspect du sujet
|
||||
- Contenu informatif et engageant`;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Pas de contrainte dans l'instruction → utiliser les contraintes par défaut du type
|
||||
switch (type) {
|
||||
case 'titre':
|
||||
lengthConstraint = '8-15 mots MAXIMUM';
|
||||
specificInstructions = `Le titre doit être:
|
||||
- COURT et PERCUTANT (8-15 mots max)
|
||||
- Pas de phrases complètes
|
||||
- Intégrer "${keyword}"`;
|
||||
break;
|
||||
|
||||
case 'intro':
|
||||
lengthConstraint = '40-80 mots (2-3 phrases courtes)';
|
||||
specificInstructions = `L'introduction doit:
|
||||
- Présenter le sujet
|
||||
- Accrocher le lecteur
|
||||
- 40-80 mots seulement`;
|
||||
break;
|
||||
|
||||
case 'conclusion':
|
||||
lengthConstraint = '40-80 mots (2-3 phrases courtes)';
|
||||
specificInstructions = `La conclusion doit:
|
||||
- Résumer les points clés
|
||||
- Appel à l'action si pertinent
|
||||
- 40-80 mots seulement`;
|
||||
break;
|
||||
|
||||
case 'faq_question':
|
||||
lengthConstraint = '10-20 mots';
|
||||
specificInstructions = `La question FAQ doit être:
|
||||
- Courte et directe (10-20 mots)
|
||||
- Formulée du point de vue utilisateur`;
|
||||
break;
|
||||
|
||||
case 'faq_reponse':
|
||||
lengthConstraint = '60-120 mots (3-5 phrases)';
|
||||
specificInstructions = `La réponse FAQ doit être:
|
||||
- Directe et informative (60-120 mots)
|
||||
- Répondre précisément à la question`;
|
||||
break;
|
||||
|
||||
case 'paragraphe':
|
||||
default:
|
||||
lengthConstraint = '80-200 mots (3-6 phrases)';
|
||||
specificInstructions = `Le paragraphe doit:
|
||||
- Développer un aspect du sujet
|
||||
- Contenu informatif et engageant
|
||||
- 80-200 mots (PAS PLUS)`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 NOUVEAU : Injecter le titre associé pour textes/intros
|
||||
let titleContext = '';
|
||||
if (associatedTitle && (type === 'intro' || type === 'paragraphe')) {
|
||||
titleContext = `\n🎯 TITRE ASSOCIÉ (IMPORTANT - utilise-le comme base): "${associatedTitle}"\n⚠️ CRUCIAL: Le contenu doit développer et être cohérent avec ce titre spécifique.\n`;
|
||||
}
|
||||
|
||||
// 🔥 Helper : Sélectionner aléatoirement max N éléments d'un array
|
||||
const selectRandomItems = (arr, max = 2) => {
|
||||
if (!Array.isArray(arr) || arr.length === 0) return arr;
|
||||
if (arr.length <= max) return arr;
|
||||
|
||||
// Fisher-Yates shuffle puis prendre les N premiers
|
||||
const shuffled = [...arr];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
return shuffled.slice(0, max);
|
||||
};
|
||||
|
||||
// 🔥 NOUVEAU : Contexte personnalité enrichi
|
||||
// ⚠️ EXCLUSION : Pas de personnalité pour les questions FAQ (seulement pour les réponses)
|
||||
let personalityContext = '';
|
||||
const includePersonality = personality && type !== 'faq_question';
|
||||
|
||||
if (includePersonality) {
|
||||
// 🎲 Sélection aléatoire de 2 éléments max pour D, E, F, J
|
||||
const vocabArray = Array.isArray(personality.vocabulairePref)
|
||||
? personality.vocabulairePref
|
||||
: (personality.vocabulairePref || '').split(',').map(s => s.trim()).filter(s => s);
|
||||
const vocabList = selectRandomItems(vocabArray, 2).join(', ');
|
||||
|
||||
const connecteursArray = Array.isArray(personality.connecteursPref)
|
||||
? personality.connecteursPref
|
||||
: (personality.connecteursPref || '').split(',').map(s => s.trim()).filter(s => s);
|
||||
const connecteursList = selectRandomItems(connecteursArray, 2).join(', ');
|
||||
|
||||
const motsClésArray = Array.isArray(personality.motsClesSecteurs)
|
||||
? personality.motsClesSecteurs
|
||||
: (personality.motsClesSecteurs || '').split(',').map(s => s.trim()).filter(s => s);
|
||||
const motsClesList = selectRandomItems(motsClésArray, 2).join(', ');
|
||||
|
||||
const ctaArray = Array.isArray(personality.ctaStyle)
|
||||
? personality.ctaStyle
|
||||
: (personality.ctaStyle || '').split(',').map(s => s.trim()).filter(s => s);
|
||||
const ctaList = selectRandomItems(ctaArray, 2).join(', ');
|
||||
|
||||
personalityContext = `
|
||||
PROFIL PERSONNALITÉ RÉDACTEUR:
|
||||
- Nom: ${personality.nom || 'Standard'}
|
||||
- Profil: ${personality.description || 'Expert généraliste'}
|
||||
- Style: ${personality.style || 'professionnel'}
|
||||
${motsClesList ? `- Secteurs d'expertise: ${motsClesList}` : ''}
|
||||
${vocabList ? `- Vocabulaire préféré: ${vocabList}` : ''}
|
||||
${connecteursList ? `- Connecteurs préférés: ${connecteursList}` : ''}
|
||||
${personality.longueurPhrases ? `- Longueur phrases: ${personality.longueurPhrases}` : ''}
|
||||
${personality.niveauTechnique ? `- Niveau technique: ${personality.niveauTechnique}` : ''}
|
||||
${ctaList ? `- Style CTA: ${ctaList}` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
const prompt = `Tu es un rédacteur SEO expert. Génère du contenu professionnel et naturel.
|
||||
|
||||
CONTEXTE:
|
||||
- Sujet principal: ${keyword}
|
||||
- Titre de l'article: ${title}
|
||||
${personalityContext}${titleContext}
|
||||
ÉLÉMENT À GÉNÉRER: ${tag} (Type: ${type})
|
||||
|
||||
INSTRUCTION SPÉCIFIQUE:
|
||||
${instruction}
|
||||
|
||||
CONTRAINTE DE LONGUEUR (⚠️ CRUCIAL - À RESPECTER ABSOLUMENT):
|
||||
${lengthConstraint}
|
||||
|
||||
${specificInstructions}
|
||||
|
||||
CONSIGNES RÉDACTIONNELLES:
|
||||
${includePersonality ? `- ADOPTE le style et vocabulaire du profil personnalité ci-dessus
|
||||
- Utilise les connecteurs préférés listés pour fluidifier le texte
|
||||
- Adapte la longueur des phrases selon le profil (${personality?.longueurPhrases || 'moyennes'})
|
||||
- Niveau technique: ${personality?.niveauTechnique || 'moyen'}` : '- Formulation neutre et professionnelle (question FAQ)'}
|
||||
- Ton naturel et humain, pas robotique
|
||||
- Intégration fluide du mot-clé "${keyword}"
|
||||
${associatedTitle ? `- DÉVELOPPE spécifiquement le titre: "${associatedTitle}"` : ''}
|
||||
- PAS de formatage markdown (ni **, ni ##, ni -)
|
||||
- PAS de préambule ou conclusion ajoutée
|
||||
- ⚠️ IMPÉRATIF: RESPECTE la contrainte de longueur indiquée ci-dessus
|
||||
|
||||
RÉPONSE (contenu uniquement, sans intro comme "Voici le contenu..."):`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génération simple avec LLM configurable (compatible avec l'ancien système)
|
||||
*/
|
||||
async function generateSimple(hierarchy, csvData, options = {}) {
|
||||
const LLMManager = require('../LLMManager');
|
||||
const llmProvider = options.llmProvider || 'claude-sonnet-4-5';
|
||||
|
||||
const llmProvider = options.llmProvider || 'claude';
|
||||
|
||||
logSh(`🔥 Génération simple avec ${llmProvider.toUpperCase()}`, 'INFO');
|
||||
logSh(`🔥 Génération avec contraintes de longueur par type (${llmProvider.toUpperCase()})`, 'INFO');
|
||||
|
||||
if (!hierarchy || Object.keys(hierarchy).length === 0) {
|
||||
throw new Error('Hiérarchie vide ou invalide');
|
||||
@ -509,136 +806,296 @@ async function generateSimple(hierarchy, csvData, options = {}) {
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Générer chaque élément avec Claude
|
||||
for (const [tag, item] of Object.entries(hierarchy)) {
|
||||
try {
|
||||
logSh(`🎯 Génération: ${tag}`, 'DEBUG');
|
||||
|
||||
// Extraire l'instruction correctement selon la structure
|
||||
let instruction = '';
|
||||
if (typeof item === 'string') {
|
||||
instruction = item;
|
||||
} else if (item.instructions) {
|
||||
instruction = item.instructions;
|
||||
} else if (item.title && item.title.instructions) {
|
||||
instruction = item.title.instructions;
|
||||
} else if (item.text && item.text.instructions) {
|
||||
instruction = item.text.instructions;
|
||||
} else {
|
||||
logSh(`⚠️ Pas d'instruction trouvée pour ${tag}, structure: ${JSON.stringify(Object.keys(item))}`, 'WARNING');
|
||||
continue; // Skip cet élément
|
||||
}
|
||||
|
||||
// Fonction pour résoudre les variables dans les instructions
|
||||
// Fonction utilitaire pour résoudre les variables
|
||||
const resolveVariables = (text, csvData) => {
|
||||
return text.replace(/\{\{?([^}]+)\}?\}/g, (match, variable) => {
|
||||
const cleanVar = variable.trim();
|
||||
|
||||
// Variables simples
|
||||
if (cleanVar === 'MC0') return csvData.mc0 || '';
|
||||
if (cleanVar === 'T0') return csvData.t0 || '';
|
||||
if (cleanVar === 'T-1') return csvData.tMinus1 || '';
|
||||
if (cleanVar === 'L-1') return csvData.lMinus1 || '';
|
||||
|
||||
// Variables avec index MC+1_X
|
||||
if (cleanVar.startsWith('MC+1_')) {
|
||||
const index = parseInt(cleanVar.split('_')[1]) - 1;
|
||||
const mcPlus1 = (csvData.mcPlus1 || '').split(',').map(s => s.trim());
|
||||
const resolved = mcPlus1[index] || csvData.mc0 || '';
|
||||
logSh(` 🔍 Variable ${cleanVar} → "${resolved}" (index ${index}, mcPlus1: ${mcPlus1.length} items)`, 'DEBUG');
|
||||
return resolved;
|
||||
return mcPlus1[index] || csvData.mc0 || '';
|
||||
}
|
||||
|
||||
// Variables avec index T+1_X
|
||||
if (cleanVar.startsWith('T+1_')) {
|
||||
const index = parseInt(cleanVar.split('_')[1]) - 1;
|
||||
const tPlus1 = (csvData.tPlus1 || '').split(',').map(s => s.trim());
|
||||
const resolved = tPlus1[index] || csvData.t0 || '';
|
||||
logSh(` 🔍 Variable ${cleanVar} → "${resolved}" (index ${index}, tPlus1: ${tPlus1.length} items)`, 'DEBUG');
|
||||
return resolved;
|
||||
return tPlus1[index] || csvData.t0 || '';
|
||||
}
|
||||
|
||||
// Variables avec index L+1_X
|
||||
if (cleanVar.startsWith('L+1_')) {
|
||||
const index = parseInt(cleanVar.split('_')[1]) - 1;
|
||||
const lPlus1 = (csvData.lPlus1 || '').split(',').map(s => s.trim());
|
||||
const resolved = lPlus1[index] || '';
|
||||
logSh(` 🔍 Variable ${cleanVar} → "${resolved}" (index ${index}, lPlus1: ${lPlus1.length} items)`, 'DEBUG');
|
||||
return resolved;
|
||||
return lPlus1[index] || '';
|
||||
}
|
||||
|
||||
// Variable inconnue
|
||||
logSh(` ⚠️ Variable inconnue: "${cleanVar}" (match: "${match}")`, 'WARNING');
|
||||
return csvData.mc0 || '';
|
||||
});
|
||||
};
|
||||
|
||||
// Nettoyer l'instruction des balises HTML et résoudre les variables
|
||||
const originalInstruction = instruction;
|
||||
// Fonction pour extraire l'instruction de l'élément
|
||||
const extractInstruction = (tag, item) => {
|
||||
if (typeof item === 'string') return item;
|
||||
if (item.instructions) return item.instructions;
|
||||
if (item.title && item.title.instructions) return item.title.instructions;
|
||||
if (item.text && item.text.instructions) return item.text.instructions;
|
||||
|
||||
// NE PLUS nettoyer le HTML ici - c'est fait dans ElementExtraction.js
|
||||
instruction = instruction.trim();
|
||||
|
||||
logSh(` 📝 Instruction avant résolution (${tag}): ${instruction.substring(0, 100)}...`, 'DEBUG');
|
||||
instruction = resolveVariables(instruction, csvData);
|
||||
logSh(` ✅ Instruction après résolution (${tag}): ${instruction.substring(0, 100)}...`, 'DEBUG');
|
||||
|
||||
// Nettoyer les accolades mal formées restantes
|
||||
instruction = instruction
|
||||
.replace(/\{[^}]*/g, '') // Supprimer accolades non fermées
|
||||
.replace(/[{}]/g, '') // Supprimer accolades isolées
|
||||
.trim();
|
||||
|
||||
// Vérifier que l'instruction n'est pas vide ou invalide
|
||||
if (!instruction || instruction.length < 10) {
|
||||
logSh(`⚠️ Instruction trop courte ou vide pour ${tag}, skip`, 'WARNING');
|
||||
continue;
|
||||
if (item.questions && Array.isArray(item.questions) && item.questions.length > 0) {
|
||||
const faqItem = item.questions[0];
|
||||
if (faqItem.originalElement && faqItem.originalElement.resolvedContent) {
|
||||
return faqItem.originalElement.resolvedContent;
|
||||
}
|
||||
return `Générer une ${tag.startsWith('q') ? 'question' : 'réponse'} FAQ pertinente sur ${csvData.mc0}`;
|
||||
}
|
||||
|
||||
const prompt = `Tu es un expert en rédaction SEO. Tu dois générer du contenu professionnel et naturel.
|
||||
return `Générer du contenu pertinent pour la section ${tag} sur "${csvData.mc0}"`;
|
||||
};
|
||||
|
||||
CONTEXTE:
|
||||
- Mot-clé principal: ${csvData.mc0}
|
||||
- Titre principal: ${csvData.t0}
|
||||
- Personnalité: ${csvData.personality?.nom} (${csvData.personality?.style})
|
||||
try {
|
||||
// Grouper éléments par couples (titre/texte et FAQ)
|
||||
const batches = [];
|
||||
|
||||
INSTRUCTION SPÉCIFIQUE:
|
||||
${instruction}
|
||||
for (const [sectionKey, section] of Object.entries(hierarchy)) {
|
||||
const batch = [];
|
||||
|
||||
CONSIGNES:
|
||||
- Contenu naturel et engageant
|
||||
- Intégration naturelle du mot-clé "${csvData.mc0}"
|
||||
- Style ${csvData.personality?.style || 'professionnel'}
|
||||
- Pas de formatage markdown
|
||||
- Réponse directe sans préambule
|
||||
// Couple titre + texte
|
||||
if (section.title && section.text) {
|
||||
// 🔥 FIX: Utiliser le nom original pour préserver le type (Intro_, Txt_, etc.)
|
||||
const titleTag = section.title.originalElement?.name || `${sectionKey}_title`;
|
||||
const textTag = section.text.originalElement?.name || `${sectionKey}_text`;
|
||||
|
||||
RÉPONSE:`;
|
||||
batch.push({ tag: titleTag, item: section.title, isCouple: 'titre' });
|
||||
batch.push({ tag: textTag, item: section.text, isCouple: 'texte' });
|
||||
} else if (section.title) {
|
||||
const tag = section.title.originalElement?.name || sectionKey;
|
||||
batch.push({ tag: tag, item: section.title, isCouple: null });
|
||||
} else if (section.text) {
|
||||
const tag = section.text.originalElement?.name || sectionKey;
|
||||
batch.push({ tag: tag, item: section.text, isCouple: null });
|
||||
}
|
||||
|
||||
const response = await LLMManager.callLLM(llmProvider, prompt, {
|
||||
// Paires FAQ (q_1 + a_1, q_2 + a_2, etc.)
|
||||
if (section.questions && section.questions.length > 0) {
|
||||
for (let i = 0; i < section.questions.length; i += 2) {
|
||||
const question = section.questions[i];
|
||||
const answer = section.questions[i + 1];
|
||||
|
||||
if (question) {
|
||||
batch.push({
|
||||
tag: question.hierarchyPath || `faq_q_${i}`,
|
||||
item: question,
|
||||
isCouple: 'faq_question'
|
||||
});
|
||||
}
|
||||
|
||||
if (answer) {
|
||||
batch.push({
|
||||
tag: answer.hierarchyPath || `faq_a_${i}`,
|
||||
item: answer,
|
||||
isCouple: 'faq_reponse'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (batch.length > 0) {
|
||||
batches.push(...batch);
|
||||
}
|
||||
}
|
||||
|
||||
logSh(`📊 Total éléments à générer: ${batches.length}`, 'INFO');
|
||||
|
||||
// 🔥 NOUVEAU : Tracker le dernier titre généré pour l'associer au texte suivant
|
||||
let lastGeneratedTitle = null;
|
||||
|
||||
// Générer chaque élément avec prompt typé
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
const { tag, item, isCouple } = batches[i];
|
||||
|
||||
try {
|
||||
logSh(`🎯 Génération: ${tag}${isCouple ? ` (couple: ${isCouple})` : ''}`, 'DEBUG');
|
||||
|
||||
// 🔥 NOUVEAU : Détecter si le prochain élément est un texte associé à un titre
|
||||
const isTitle = isCouple === 'titre';
|
||||
const nextBatch = i < batches.length - 1 ? batches[i + 1] : null;
|
||||
const nextIsText = nextBatch && (nextBatch.isCouple === 'texte');
|
||||
|
||||
if (isTitle && nextIsText) {
|
||||
logSh(` 🔗 Détecté couple titre→texte : ${tag} → ${nextBatch.tag}`, 'DEBUG');
|
||||
}
|
||||
|
||||
// Extraire et résoudre l'instruction
|
||||
let instruction = extractInstruction(tag, item);
|
||||
instruction = instruction.trim();
|
||||
instruction = resolveVariables(instruction, csvData);
|
||||
|
||||
// Résoudre variables non résolues manuellement
|
||||
const unresolvedPattern = /\b(MC\+1_\d+|T\+1_\d+|L\+1_\d+|MC0|T0|T-1|L-1)\b/gi;
|
||||
const unresolved = instruction.match(unresolvedPattern);
|
||||
if (unresolved) {
|
||||
unresolved.forEach(varName => {
|
||||
const upperVar = varName.toUpperCase();
|
||||
let replacement = csvData.mc0 || '';
|
||||
|
||||
if (upperVar === 'MC0') replacement = csvData.mc0 || '';
|
||||
else if (upperVar === 'T0') replacement = csvData.t0 || '';
|
||||
else if (upperVar === 'T-1') replacement = csvData.tMinus1 || '';
|
||||
else if (upperVar === 'L-1') replacement = csvData.lMinus1 || '';
|
||||
else if (upperVar.startsWith('MC+1_')) {
|
||||
const idx = parseInt(upperVar.split('_')[1]) - 1;
|
||||
replacement = (csvData.mcPlus1 || '').split(',')[idx]?.trim() || csvData.mc0 || '';
|
||||
} else if (upperVar.startsWith('T+1_')) {
|
||||
const idx = parseInt(upperVar.split('_')[1]) - 1;
|
||||
replacement = (csvData.tPlus1 || '').split(',')[idx]?.trim() || csvData.t0 || '';
|
||||
} else if (upperVar.startsWith('L+1_')) {
|
||||
const idx = parseInt(upperVar.split('_')[1]) - 1;
|
||||
replacement = (csvData.lPlus1 || '').split(',')[idx]?.trim() || '';
|
||||
}
|
||||
|
||||
instruction = instruction.replace(new RegExp(varName, 'gi'), replacement);
|
||||
});
|
||||
}
|
||||
|
||||
// Nettoyer accolades mal formées
|
||||
instruction = instruction.replace(/\{[^}]*/g, '').replace(/[{}]/g, '').trim();
|
||||
|
||||
if (!instruction || instruction.length < 10) {
|
||||
logSh(` ⚠️ ${tag}: Instruction trop courte (${instruction?.length || 0} chars), utilisation fallback`, 'WARNING');
|
||||
instruction = `Générer du contenu pertinent pour ${tag} sur "${csvData.mc0}"`;
|
||||
}
|
||||
|
||||
// Détecter le type d'élément
|
||||
const elementType = detectElementType(tag);
|
||||
logSh(` 📝 Type détecté: ${elementType}`, 'DEBUG');
|
||||
|
||||
// 🔥 NOUVEAU : Si c'est un texte et qu'on a un titre généré juste avant, l'utiliser
|
||||
const shouldUseTitle = (isCouple === 'texte') && lastGeneratedTitle;
|
||||
if (shouldUseTitle) {
|
||||
logSh(` 🎯 Utilisation du titre associé: "${lastGeneratedTitle}"`, 'INFO');
|
||||
}
|
||||
|
||||
// Créer le prompt avec contraintes de longueur + titre associé si disponible
|
||||
const prompt = createTypedPrompt(tag, elementType, instruction, csvData, shouldUseTitle ? lastGeneratedTitle : null);
|
||||
|
||||
// Appeler le LLM avec maxTokens augmenté
|
||||
let maxTokens = 1000; // Défaut augmenté
|
||||
if (llmProvider.startsWith('gpt-5')) {
|
||||
maxTokens = 2500; // GPT-5 avec reasoning tokens
|
||||
} else if (llmProvider.startsWith('gpt-4')) {
|
||||
maxTokens = 1500;
|
||||
} else if (llmProvider.startsWith('claude')) {
|
||||
maxTokens = 2000;
|
||||
}
|
||||
|
||||
logSh(` 📏 MaxTokens: ${maxTokens} pour ${llmProvider}`, 'DEBUG');
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await LLMManager.callLLM(llmProvider, prompt, {
|
||||
temperature: 0.9,
|
||||
maxTokens: 300,
|
||||
timeout: 30000
|
||||
maxTokens: maxTokens,
|
||||
timeout: 45000
|
||||
}, csvData.personality);
|
||||
} catch (llmError) {
|
||||
logSh(`❌ Erreur LLM pour ${tag}: ${llmError.message}`, 'ERROR');
|
||||
response = null;
|
||||
}
|
||||
|
||||
if (response && response.trim()) {
|
||||
result.content[tag] = cleanGeneratedContent(response.trim());
|
||||
const cleaned = cleanGeneratedContent(response.trim());
|
||||
result.content[tag] = cleaned;
|
||||
result.stats.processed++;
|
||||
result.stats.enhanced++;
|
||||
|
||||
// 🔥 NOUVEAU : Si c'est un titre, le stocker pour l'utiliser avec le texte suivant
|
||||
if (isTitle) {
|
||||
lastGeneratedTitle = cleaned;
|
||||
logSh(` 📌 Titre stocké pour le texte suivant: "${cleaned}"`, 'DEBUG');
|
||||
}
|
||||
|
||||
// 🔥 NOUVEAU : Si on vient de générer un texte, réinitialiser le titre
|
||||
if (isCouple === 'texte') {
|
||||
lastGeneratedTitle = null;
|
||||
}
|
||||
|
||||
const wordCount = cleaned.split(/\s+/).length;
|
||||
logSh(` ✅ Généré: ${tag} (${wordCount} mots)`, 'DEBUG');
|
||||
} else {
|
||||
logSh(`⚠️ Réponse vide pour ${tag}`, 'WARNING');
|
||||
result.content[tag] = `Contenu ${tag} généré automatiquement`;
|
||||
// Fallback avec prompt simplifié
|
||||
logSh(` ⚠️ Réponse vide, retry avec gpt-4o-mini`, 'WARNING');
|
||||
|
||||
try {
|
||||
const simplePrompt = `Rédige du contenu professionnel sur "${csvData.mc0}" pour ${tag}. ${elementType === 'titre' ? 'Maximum 15 mots.' : elementType === 'intro' || elementType === 'conclusion' ? 'Environ 50-80 mots.' : 'Environ 100-150 mots.'}`;
|
||||
|
||||
const retryResponse = await LLMManager.callLLM('gpt-4o-mini', simplePrompt, {
|
||||
temperature: 0.7,
|
||||
maxTokens: 500,
|
||||
timeout: 20000
|
||||
});
|
||||
|
||||
if (retryResponse && retryResponse.trim()) {
|
||||
const cleaned = cleanGeneratedContent(retryResponse.trim());
|
||||
result.content[tag] = cleaned;
|
||||
result.stats.processed++;
|
||||
result.stats.enhanced++;
|
||||
|
||||
// 🔥 NOUVEAU : Stocker le titre même dans le fallback
|
||||
if (isTitle) {
|
||||
lastGeneratedTitle = cleaned;
|
||||
logSh(` 📌 Titre stocké (fallback): "${cleaned}"`, 'DEBUG');
|
||||
}
|
||||
if (isCouple === 'texte') {
|
||||
lastGeneratedTitle = null;
|
||||
}
|
||||
|
||||
logSh(` ✅ Retry réussi pour ${tag}`, 'INFO');
|
||||
} else {
|
||||
result.content[tag] = `Contenu professionnel sur ${csvData.mc0}. [Généré automatiquement]`;
|
||||
result.stats.processed++;
|
||||
|
||||
// 🔥 NOUVEAU : Réinitialiser si c'était un texte
|
||||
if (isCouple === 'texte') {
|
||||
lastGeneratedTitle = null;
|
||||
}
|
||||
}
|
||||
} catch (retryError) {
|
||||
result.content[tag] = `Contenu professionnel sur ${csvData.mc0}. [Erreur: ${retryError.message.substring(0, 50)}]`;
|
||||
result.stats.processed++;
|
||||
|
||||
// 🔥 NOUVEAU : Réinitialiser si c'était un texte
|
||||
if (isCouple === 'texte') {
|
||||
lastGeneratedTitle = null;
|
||||
}
|
||||
|
||||
logSh(` ❌ Retry échoué: ${retryError.message}`, 'ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logSh(`❌ Erreur génération ${tag}: ${error.message}`, 'ERROR');
|
||||
result.content[tag] = `Contenu ${tag} - Erreur de génération`;
|
||||
result.content[tag] = `Contenu professionnel sur ${csvData.mc0}. [Erreur: ${error.message.substring(0, 50)}]`;
|
||||
result.stats.processed++;
|
||||
|
||||
// 🔥 NOUVEAU : Réinitialiser si c'était un texte
|
||||
if (isCouple === 'texte') {
|
||||
lastGeneratedTitle = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.stats.duration = Date.now() - startTime;
|
||||
|
||||
logSh(`✅ Génération simple terminée: ${result.stats.enhanced}/${result.stats.processed} éléments (${result.stats.duration}ms)`, 'INFO');
|
||||
const generatedElements = Object.keys(result.content).length;
|
||||
const successfulElements = result.stats.enhanced;
|
||||
const fallbackElements = generatedElements - successfulElements;
|
||||
|
||||
logSh(`✅ Génération terminée: ${generatedElements} éléments`, 'INFO');
|
||||
logSh(` ✓ Succès: ${successfulElements}, Fallback: ${fallbackElements}, Durée: ${result.stats.duration}ms`, 'INFO');
|
||||
|
||||
return result;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user