284 lines
9.5 KiB
JavaScript
284 lines
9.5 KiB
JavaScript
// ========================================
|
|
// INITIAL GENERATION LAYER - GÉNÉRATION INITIALE MODULAIRE
|
|
// Responsabilité: Génération de contenu initial réutilisable
|
|
// LLM: Claude Sonnet-4 (précision et créativité équilibrée)
|
|
// ========================================
|
|
|
|
const { callLLM } = require('../LLMManager');
|
|
const { logSh } = require('../ErrorReporting');
|
|
const { tracer } = require('../trace');
|
|
const { chunkArray, sleep } = require('../selective-enhancement/SelectiveUtils');
|
|
|
|
/**
|
|
* COUCHE GÉNÉRATION INITIALE MODULAIRE
|
|
*/
|
|
class InitialGenerationLayer {
|
|
constructor() {
|
|
this.name = 'InitialGeneration';
|
|
this.defaultLLM = 'claude';
|
|
this.priority = 0; // Priorité maximale - appliqué en premier
|
|
}
|
|
|
|
/**
|
|
* MAIN METHOD - Générer contenu initial
|
|
*/
|
|
async apply(contentStructure, config = {}) {
|
|
return await tracer.run('InitialGenerationLayer.apply()', async () => {
|
|
const {
|
|
llmProvider = this.defaultLLM,
|
|
temperature = 0.7,
|
|
csvData = null,
|
|
context = {}
|
|
} = config;
|
|
|
|
await tracer.annotate({
|
|
initialGeneration: true,
|
|
llmProvider,
|
|
temperature,
|
|
elementsCount: Object.keys(contentStructure).length,
|
|
mc0: csvData?.mc0
|
|
});
|
|
|
|
const startTime = Date.now();
|
|
logSh(`🎯 INITIAL GENERATION: Génération contenu initial (${llmProvider})`, 'INFO');
|
|
logSh(` 📊 ${Object.keys(contentStructure).length} éléments à générer`, 'INFO');
|
|
|
|
try {
|
|
// Créer les éléments à générer à partir de la structure
|
|
const elementsToGenerate = this.prepareElementsForGeneration(contentStructure, csvData);
|
|
|
|
// Générer en chunks pour gérer les gros contenus
|
|
const results = {};
|
|
const chunks = chunkArray(Object.entries(elementsToGenerate), 4); // Chunks de 4 pour Claude
|
|
|
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
const chunk = chunks[chunkIndex];
|
|
|
|
try {
|
|
logSh(` 📦 Chunk génération ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
|
|
|
|
const generationPrompt = this.createInitialGenerationPrompt(chunk, csvData, config);
|
|
|
|
const response = await callLLM(llmProvider, generationPrompt, {
|
|
temperature,
|
|
maxTokens: 4000
|
|
}, csvData?.personality);
|
|
|
|
const chunkResults = this.parseInitialGenerationResponse(response, chunk);
|
|
Object.assign(results, chunkResults);
|
|
|
|
logSh(` ✅ Chunk génération ${chunkIndex + 1}: ${Object.keys(chunkResults).length} générés`, 'DEBUG');
|
|
|
|
// Délai entre chunks
|
|
if (chunkIndex < chunks.length - 1) {
|
|
await sleep(2000);
|
|
}
|
|
|
|
} catch (error) {
|
|
logSh(` ❌ Chunk génération ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
|
|
|
|
// Fallback: contenu basique
|
|
chunk.forEach(([tag, instruction]) => {
|
|
results[tag] = this.createFallbackContent(tag, csvData);
|
|
});
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
const stats = {
|
|
generated: Object.keys(results).length,
|
|
total: Object.keys(contentStructure).length,
|
|
generationRate: (Object.keys(results).length / Math.max(Object.keys(contentStructure).length, 1)) * 100,
|
|
duration,
|
|
llmProvider,
|
|
temperature
|
|
};
|
|
|
|
logSh(`✅ INITIAL GENERATION TERMINÉE: ${stats.generated}/${stats.total} générés (${duration}ms)`, 'INFO');
|
|
|
|
await tracer.event('Initial generation appliquée', stats);
|
|
|
|
return { content: results, stats };
|
|
|
|
} catch (error) {
|
|
const duration = Date.now() - startTime;
|
|
logSh(`❌ INITIAL GENERATION ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
|
|
throw error;
|
|
}
|
|
}, { contentStructure: Object.keys(contentStructure), config });
|
|
}
|
|
|
|
/**
|
|
* PRÉPARER ÉLÉMENTS POUR GÉNÉRATION
|
|
*/
|
|
prepareElementsForGeneration(contentStructure, csvData) {
|
|
const elements = {};
|
|
|
|
// Convertir la structure en instructions de génération
|
|
Object.entries(contentStructure).forEach(([tag, placeholder]) => {
|
|
elements[tag] = {
|
|
type: this.detectElementType(tag),
|
|
instruction: this.createInstructionFromPlaceholder(placeholder, csvData),
|
|
context: csvData?.mc0 || 'contenu personnalisé'
|
|
};
|
|
});
|
|
|
|
return elements;
|
|
}
|
|
|
|
/**
|
|
* DÉTECTER TYPE D'ÉLÉMENT
|
|
*/
|
|
detectElementType(tag) {
|
|
const tagLower = tag.toLowerCase();
|
|
|
|
if (tagLower.includes('titre') || tagLower.includes('h1') || tagLower.includes('h2')) {
|
|
return 'titre';
|
|
} else if (tagLower.includes('intro') || tagLower.includes('introduction')) {
|
|
return 'introduction';
|
|
} else if (tagLower.includes('conclusion')) {
|
|
return 'conclusion';
|
|
} else if (tagLower.includes('faq') || tagLower.includes('question')) {
|
|
return 'faq';
|
|
} else {
|
|
return 'contenu';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* CRÉER INSTRUCTION À PARTIR DU PLACEHOLDER
|
|
*/
|
|
createInstructionFromPlaceholder(placeholder, csvData) {
|
|
// Si c'est déjà une vraie instruction, la garder
|
|
if (typeof placeholder === 'string' && placeholder.length > 30) {
|
|
return placeholder;
|
|
}
|
|
|
|
// Sinon, créer une instruction basique
|
|
const mc0 = csvData?.mc0 || 'produit';
|
|
return `Rédige un contenu professionnel et engageant sur ${mc0}`;
|
|
}
|
|
|
|
/**
|
|
* CRÉER PROMPT GÉNÉRATION INITIALE
|
|
*/
|
|
createInitialGenerationPrompt(chunk, csvData, config) {
|
|
const personality = csvData?.personality;
|
|
const mc0 = csvData?.mc0 || 'contenu personnalisé';
|
|
|
|
let prompt = `MISSION: Génère du contenu SEO initial de haute qualité.
|
|
|
|
CONTEXTE: ${mc0} - Article optimisé SEO
|
|
${personality ? `PERSONNALITÉ: ${personality.nom} (${personality.style})` : ''}
|
|
TEMPÉRATURE: ${config.temperature || 0.7} (créativité équilibrée)
|
|
|
|
ÉLÉMENTS À GÉNÉRER:
|
|
|
|
${chunk.map(([tag, data], i) => `[${i + 1}] TAG: ${tag}
|
|
TYPE: ${data.type}
|
|
INSTRUCTION: ${data.instruction}
|
|
CONTEXTE: ${data.context}`).join('\n\n')}
|
|
|
|
CONSIGNES GÉNÉRATION:
|
|
- CRÉE du contenu original et engageant${personality ? ` avec le style ${personality.style}` : ''}
|
|
- INTÈGRE naturellement le mot-clé "${mc0}"
|
|
- RESPECTE les bonnes pratiques SEO (mots-clés, structure)
|
|
- ADAPTE longueur selon type d'élément:
|
|
* Titres: 8-15 mots
|
|
* Introduction: 2-3 phrases (40-80 mots)
|
|
* Contenu: 3-6 phrases (80-200 mots)
|
|
* Conclusion: 2-3 phrases (40-80 mots)
|
|
- ÉVITE contenu générique, sois spécifique et informatif
|
|
- UTILISE un ton professionnel mais accessible
|
|
|
|
VOCABULAIRE RECOMMANDÉ SELON CONTEXTE:
|
|
- Si signalétique: matériaux (dibond, aluminium), procédés (gravure, impression)
|
|
- Adapte selon le domaine du mot-clé principal
|
|
|
|
FORMAT RÉPONSE:
|
|
[1] Contenu généré pour premier élément
|
|
[2] Contenu généré pour deuxième élément
|
|
etc...
|
|
|
|
IMPORTANT: Réponse DIRECTE par les contenus générés, pas d'explication.`;
|
|
|
|
return prompt;
|
|
}
|
|
|
|
/**
|
|
* PARSER RÉPONSE GÉNÉRATION INITIALE
|
|
*/
|
|
parseInitialGenerationResponse(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 generatedContent = match[2].trim();
|
|
const [tag] = chunk[index];
|
|
|
|
// Nettoyer contenu généré
|
|
generatedContent = this.cleanGeneratedContent(generatedContent);
|
|
|
|
if (generatedContent && generatedContent.length > 10) {
|
|
results[tag] = generatedContent;
|
|
logSh(`✅ Généré [${tag}]: "${generatedContent.substring(0, 60)}..."`, 'DEBUG');
|
|
} else {
|
|
results[tag] = this.createFallbackContent(tag, chunk[index][1]);
|
|
logSh(`⚠️ Fallback génération [${tag}]: contenu invalide`, 'WARNING');
|
|
}
|
|
|
|
index++;
|
|
}
|
|
|
|
// Compléter les manquants
|
|
while (index < chunk.length) {
|
|
const [tag, data] = chunk[index];
|
|
results[tag] = this.createFallbackContent(tag, data);
|
|
index++;
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* NETTOYER CONTENU GÉNÉRÉ
|
|
*/
|
|
cleanGeneratedContent(content) {
|
|
if (!content) return content;
|
|
|
|
// Supprimer préfixes indésirables
|
|
content = content.replace(/^(voici\s+)?le\s+contenu\s+(généré|pour)\s*[:.]?\s*/gi, '');
|
|
content = content.replace(/^(contenu|élément)\s+(généré|pour)\s*[:.]?\s*/gi, '');
|
|
content = content.replace(/^(bon,?\s*)?(alors,?\s*)?/gi, '');
|
|
|
|
// Nettoyer formatage
|
|
content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
|
|
content = content.replace(/\s{2,}/g, ' '); // Espaces multiples
|
|
content = content.trim();
|
|
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* CRÉER CONTENU FALLBACK
|
|
*/
|
|
createFallbackContent(tag, data) {
|
|
const mc0 = data?.context || 'produit';
|
|
const type = data?.type || 'contenu';
|
|
|
|
switch (type) {
|
|
case 'titre':
|
|
return `${mc0.charAt(0).toUpperCase()}${mc0.slice(1)} de qualité professionnelle`;
|
|
case 'introduction':
|
|
return `Découvrez notre gamme complète de ${mc0}. Qualité premium et service personnalisé.`;
|
|
case 'conclusion':
|
|
return `Faites confiance à notre expertise pour votre ${mc0}. Contactez-nous pour plus d'informations.`;
|
|
default:
|
|
return `Notre ${mc0} répond à vos besoins avec des solutions adaptées et un service de qualité.`;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = { InitialGenerationLayer }; |