## 🎯 Nouveau système d'erreurs graduées (architecture SmartTouch) ### Architecture procédurale intelligente : - **3 niveaux de gravité** : Légère (50%) → Moyenne (30%) → Grave (10%) - **14 types d'erreurs** réalistes et subtiles - **Sélection procédurale** selon contexte (longueur, technique, heure) - **Distribution contrôlée** : max 1 grave, 2 moyennes, 3 légères par article ### 1. Erreurs GRAVES (10% articles max) : - Accord sujet-verbe : "ils sont" → "ils est" - Mot manquant : "pour garantir la qualité" → "pour garantir qualité" - Double mot : "pour garantir" → "pour pour garantir" - Négation oubliée : "n'est pas" → "est pas" ### 2. Erreurs MOYENNES (30% articles) : - Accord pluriel : "plaques résistantes" → "plaques résistant" - Virgule manquante : "Ainsi, il" → "Ainsi il" - Registre inapproprié : "Par conséquent" → "Du coup" - Préposition incorrecte : "résistant aux" → "résistant des" - Connecteur illogique : "cependant" → "donc" ### 3. Erreurs LÉGÈRES (50% articles) : - Double espace : "de votre" → "de votre" - Trait d'union : "c'est-à-dire" → "c'est à dire" - Espace ponctuation : "qualité ?" → "qualité?" - Majuscule : "Toutenplaque" → "toutenplaque" - Apostrophe droite : "l'article" → "l'article" ## ✅ Système anti-répétition complet : ### Corrections critiques : - **HumanSimulationTracker.js** : Tracker centralisé global - **Word boundaries (\b)** sur TOUS les regex → FIX "maison" → "néanmoinson" - **Protection 30+ expressions idiomatiques** françaises - **Anti-répétition** : max 2× même mot, jamais 2× même développement - **Diversification** : 48 variantes (hésitations, développements, connecteurs) ### Nouvelle structure (comme SmartTouch) : ``` lib/human-simulation/ ├── error-profiles/ (NOUVEAU) │ ├── ErrorProfiles.js (définitions + probabilités) │ ├── ErrorGrave.js (10% articles) │ ├── ErrorMoyenne.js (30% articles) │ ├── ErrorLegere.js (50% articles) │ └── ErrorSelector.js (sélection procédurale) ├── HumanSimulationCore.js (orchestrateur) ├── HumanSimulationTracker.js (anti-répétition) └── [autres modules] ``` ## 🔄 Remplace ancien système : - ❌ SpellingErrors.js (basique, répétitif, "et" → "." × 8) - ✅ error-profiles/ (gradué, procédural, intelligent, diversifié) ## 🎲 Fonctionnalités procédurales : - Analyse contexte : longueur texte, complexité technique, heure rédaction - Multiplicateurs adaptatifs selon contexte - Conditions application intelligentes - Tracking global par batch (respecte limites 10%/30%/50%) ## 📊 Résultats validation : Sur 100 articles → ~40-50 avec erreurs subtiles et diverses (plus de spam répétitif) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
612 lines
23 KiB
JavaScript
612 lines
23 KiB
JavaScript
// ========================================
|
|
// FICHIER: BrainConfig.js - Version Node.js
|
|
// Description: Configuration cerveau + sélection personnalité IA
|
|
// ========================================
|
|
|
|
require('dotenv').config();
|
|
const axios = require('axios');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
|
|
// Import de la fonction logSh (assumant qu'elle existe dans votre projet Node.js)
|
|
const { logSh } = require('./ErrorReporting');
|
|
const { DigitalOceanTemplates } = require('./batch/DigitalOceanTemplates');
|
|
|
|
// Configuration
|
|
const CONFIG = {
|
|
openai: {
|
|
apiKey: process.env.OPENAI_API_KEY,
|
|
endpoint: 'https://api.openai.com/v1/chat/completions'
|
|
},
|
|
dataSource: {
|
|
type: process.env.DATA_SOURCE_TYPE || 'json', // 'json', 'csv', 'database'
|
|
instructionsPath: './data/instructions.json',
|
|
personalitiesPath: './data/personalities.json'
|
|
}
|
|
};
|
|
|
|
/**
|
|
* FONCTION PRINCIPALE - Équivalent getBrainConfig()
|
|
* @param {number|object} data - Numéro de ligne ou données directes
|
|
* @returns {object} Configuration avec données CSV + personnalité
|
|
*/
|
|
async function getBrainConfig(data) {
|
|
try {
|
|
logSh("🧠 Début getBrainConfig Node.js", "INFO");
|
|
|
|
// 1. RÉCUPÉRER LES DONNÉES CSV
|
|
let csvData;
|
|
if (typeof data === 'number') {
|
|
// Numéro de ligne fourni - lire depuis fichier
|
|
csvData = await readInstructionsData(data);
|
|
} else if (typeof data === 'object' && data.rowNumber) {
|
|
csvData = await readInstructionsData(data.rowNumber);
|
|
} else {
|
|
// Données déjà fournies
|
|
csvData = data;
|
|
}
|
|
|
|
logSh(`✅ CSV récupéré: ${csvData.mc0}`, "INFO");
|
|
|
|
// 2. RÉCUPÉRER LES PERSONNALITÉS
|
|
const personalities = await getPersonalities();
|
|
logSh(`✅ ${personalities.length} personnalités chargées`, "INFO");
|
|
|
|
// 3. SÉLECTIONNER LA MEILLEURE PERSONNALITÉ VIA IA
|
|
const selectedPersonality = await selectPersonalityWithAI(
|
|
csvData.mc0,
|
|
csvData.t0,
|
|
personalities
|
|
);
|
|
|
|
logSh(`✅ Personnalité sélectionnée: ${selectedPersonality.nom}`, "INFO");
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
...csvData,
|
|
personality: selectedPersonality,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
};
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur getBrainConfig: ${error.message}`, "ERROR");
|
|
return {
|
|
success: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* LIRE DONNÉES INSTRUCTIONS depuis Google Sheets DIRECTEMENT
|
|
* @param {number} rowNumber - Numéro de ligne (2 = première ligne de données)
|
|
* @returns {object} Données CSV parsées
|
|
*/
|
|
async function readInstructionsData(rowNumber = 2) {
|
|
try {
|
|
logSh(`📊 Lecture Google Sheet ligne ${rowNumber}...`, 'INFO');
|
|
|
|
// ⚡ OPTIMISÉ : google-spreadsheet (18x plus rapide que googleapis)
|
|
const { GoogleSpreadsheet } = require('google-spreadsheet');
|
|
const { JWT } = require('google-auth-library');
|
|
|
|
const keyFilePath = path.join(__dirname, '..', 'seo-generator-470715-85d4a971c1af.json');
|
|
const serviceAccountAuth = new JWT({
|
|
keyFile: keyFilePath,
|
|
scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly']
|
|
});
|
|
|
|
const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
|
|
const doc = new GoogleSpreadsheet(SHEET_ID, serviceAccountAuth);
|
|
|
|
await doc.loadInfo();
|
|
const sheet = doc.sheetsByTitle['instructions'];
|
|
|
|
if (!sheet) {
|
|
throw new Error('Onglet "instructions" non trouvé dans Google Sheet');
|
|
}
|
|
|
|
const rows = await sheet.getRows();
|
|
const targetRow = rows[rowNumber - 2]; // -2 car index 0 = ligne 2 du sheet
|
|
|
|
if (!targetRow) {
|
|
throw new Error(`Ligne ${rowNumber} non trouvée dans Google Sheet`);
|
|
}
|
|
|
|
// ✅ Même format que googleapis : tableau de valeurs
|
|
const row = targetRow._rawData;
|
|
logSh(`✅ Ligne ${rowNumber} récupérée: ${row.length} colonnes`, 'INFO');
|
|
|
|
const xmlTemplateValue = row[8] || '';
|
|
let xmlTemplate = xmlTemplateValue;
|
|
let xmlFileName = null;
|
|
|
|
// Si c'est un nom de fichier, le récupérer depuis Digital Ocean
|
|
if (xmlTemplateValue && xmlTemplateValue.endsWith('.xml') && xmlTemplateValue.length < 100) {
|
|
logSh(`🔧 XML filename detected (${xmlTemplateValue}), fetching from Digital Ocean`, 'INFO');
|
|
xmlFileName = xmlTemplateValue;
|
|
|
|
// Récupérer le template depuis Digital Ocean
|
|
try {
|
|
const doTemplates = new DigitalOceanTemplates();
|
|
xmlTemplate = await doTemplates.getTemplate(xmlFileName);
|
|
logSh(`✅ Template ${xmlFileName} récupéré depuis Digital Ocean (${xmlTemplate?.length || 0} chars)`, 'INFO');
|
|
|
|
if (!xmlTemplate) {
|
|
throw new Error('Template vide récupéré');
|
|
}
|
|
} catch (error) {
|
|
logSh(`⚠️ Erreur récupération ${xmlFileName} depuis DO: ${error.message}. Fallback template par défaut.`, 'WARNING');
|
|
xmlTemplate = createDefaultXMLTemplate();
|
|
}
|
|
}
|
|
|
|
return {
|
|
rowNumber: rowNumber,
|
|
slug: row[0] || '', // Colonne A
|
|
t0: row[1] || '', // Colonne B
|
|
mc0: row[2] || '', // Colonne C
|
|
tMinus1: row[3] || '', // Colonne D
|
|
lMinus1: row[4] || '', // Colonne E
|
|
mcPlus1: row[5] || '', // Colonne F
|
|
tPlus1: row[6] || '', // Colonne G
|
|
lPlus1: row[7] || '', // Colonne H
|
|
xmlTemplate: xmlTemplate, // XML template pour processing
|
|
xmlFileName: xmlFileName // Nom fichier pour Digital Ocean (si applicable)
|
|
};
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur lecture Google Sheet: ${error.message}`, "ERROR");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* RÉCUPÉRER PERSONNALITÉS depuis l'onglet "Personnalites" du Google Sheet
|
|
* @returns {Array} Liste des personnalités disponibles
|
|
*/
|
|
async function getPersonalities() {
|
|
try {
|
|
logSh('📊 Lecture personnalités depuis Google Sheet (onglet Personnalites)...', 'INFO');
|
|
|
|
// ⚡ OPTIMISÉ : google-spreadsheet (18x plus rapide que googleapis)
|
|
const { GoogleSpreadsheet } = require('google-spreadsheet');
|
|
const { JWT } = require('google-auth-library');
|
|
|
|
const keyFilePath = path.join(__dirname, '..', 'seo-generator-470715-85d4a971c1af.json');
|
|
const serviceAccountAuth = new JWT({
|
|
keyFile: keyFilePath,
|
|
scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly']
|
|
});
|
|
|
|
const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
|
|
const doc = new GoogleSpreadsheet(SHEET_ID, serviceAccountAuth);
|
|
|
|
await doc.loadInfo();
|
|
const sheet = doc.sheetsByTitle['Personnalites'];
|
|
|
|
if (!sheet) {
|
|
throw new Error('Onglet "Personnalites" non trouvé dans Google Sheet');
|
|
}
|
|
|
|
const rows = await sheet.getRows();
|
|
|
|
if (!rows || rows.length === 0) {
|
|
throw new Error('Aucune personnalité trouvée dans l\'onglet Personnalites');
|
|
}
|
|
|
|
const personalities = [];
|
|
|
|
// Traiter chaque ligne de personnalité (✅ même logique qu'avant)
|
|
rows.forEach((rowObj, index) => {
|
|
const row = rowObj._rawData; // ✅ Même format tableau que googleapis
|
|
if (row[0] && row[0].toString().trim() !== '') { // Si nom existe (colonne A)
|
|
const personality = {
|
|
nom: row[0]?.toString().trim() || '',
|
|
description: row[1]?.toString().trim() || 'Expert généraliste',
|
|
style: row[2]?.toString().trim() || 'professionnel',
|
|
|
|
// Configuration avancée depuis colonnes Google Sheet
|
|
motsClesSecteurs: parseCSVField(row[3]),
|
|
vocabulairePref: parseCSVField(row[4]),
|
|
connecteursPref: parseCSVField(row[5]),
|
|
erreursTypiques: parseCSVField(row[6]),
|
|
longueurPhrases: row[7]?.toString().trim() || 'moyennes',
|
|
niveauTechnique: row[8]?.toString().trim() || 'moyen',
|
|
ctaStyle: parseCSVField(row[9]),
|
|
defautsSimules: parseCSVField(row[10]),
|
|
|
|
// NOUVEAU: Configuration IA par étape depuis Google Sheets (colonnes L-O)
|
|
aiEtape1Base: row[11]?.toString().trim().toLowerCase() || '',
|
|
aiEtape2Technique: row[12]?.toString().trim().toLowerCase() || '',
|
|
aiEtape3Transitions: row[13]?.toString().trim().toLowerCase() || '',
|
|
aiEtape4Style: row[14]?.toString().trim().toLowerCase() || '',
|
|
|
|
// Backward compatibility
|
|
motsCles: parseCSVField(row[3] || '') // Utilise motsClesSecteurs
|
|
};
|
|
|
|
personalities.push(personality);
|
|
logSh(`✓ Personnalité chargée: ${personality.nom} (${personality.style})`, 'DEBUG');
|
|
}
|
|
});
|
|
|
|
logSh(`📊 ${personalities.length} personnalités chargées depuis Google Sheet`, "INFO");
|
|
|
|
return personalities;
|
|
|
|
} catch (error) {
|
|
logSh(`❌ ÉCHEC: Impossible de récupérer les personnalités Google Sheets - ${error.message}`, "ERROR");
|
|
throw new Error(`FATAL: Personnalités Google Sheets inaccessibles - arrêt du workflow: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PARSER CHAMP CSV - Helper function
|
|
* @param {string} field - Champ à parser
|
|
* @returns {Array} Liste des éléments parsés
|
|
*/
|
|
function parseCSVField(field) {
|
|
if (!field || field.toString().trim() === '') return [];
|
|
|
|
return field.toString()
|
|
.split(',')
|
|
.map(item => item.trim())
|
|
.filter(item => item.length > 0);
|
|
}
|
|
|
|
/**
|
|
* Sélectionner un sous-ensemble aléatoire de personnalités
|
|
* @param {Array} allPersonalities - Liste complète des personnalités
|
|
* @param {number} percentage - Pourcentage à garder (0.6 = 60%)
|
|
* @returns {Array} Sous-ensemble aléatoire
|
|
*/
|
|
function selectRandomPersonalities(allPersonalities, percentage = 0.6) {
|
|
const count = Math.ceil(allPersonalities.length * percentage);
|
|
|
|
// Mélanger avec Fisher-Yates shuffle (meilleur que sort())
|
|
const shuffled = [...allPersonalities];
|
|
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, count);
|
|
}
|
|
|
|
/**
|
|
* NOUVELLE FONCTION: Sélection de 4 personnalités complémentaires pour le pipeline multi-AI
|
|
* @param {string} mc0 - Mot-clé principal
|
|
* @param {string} t0 - Titre principal
|
|
* @param {Array} personalities - Liste des personnalités
|
|
* @returns {Array} 4 personnalités sélectionnées pour chaque étape
|
|
*/
|
|
async function selectMultiplePersonalitiesWithAI(mc0, t0, personalities) {
|
|
try {
|
|
logSh(`🎭 Sélection MULTI-personnalités IA pour: ${mc0}`, "INFO");
|
|
|
|
// Sélection aléatoire de 80% des personnalités (plus large pour 4 choix)
|
|
const randomPersonalities = selectRandomPersonalities(personalities, 0.8);
|
|
const totalCount = personalities.length;
|
|
const selectedCount = randomPersonalities.length;
|
|
|
|
logSh(`🎲 Pool aléatoire: ${selectedCount}/${totalCount} personnalités disponibles`, "DEBUG");
|
|
logSh(`📋 Personnalités dans le pool: ${randomPersonalities.map(p => p.nom).join(', ')}`, "DEBUG");
|
|
|
|
const prompt = `Choisis 4 personnalités COMPLÉMENTAIRES pour générer du contenu sur "${mc0}":
|
|
|
|
OBJECTIF: Créer une équipe de 4 rédacteurs avec styles différents mais cohérents
|
|
|
|
PERSONNALITÉS DISPONIBLES:
|
|
${randomPersonalities.map(p => `- ${p.nom}: ${p.description} (Style: ${p.style})`).join('\n')}
|
|
|
|
RÔLES À ATTRIBUER:
|
|
1. GÉNÉRATEUR BASE: Personnalité technique/experte pour la génération initiale
|
|
2. ENHANCER TECHNIQUE: Personnalité commerciale/précise pour améliorer les termes techniques
|
|
3. FLUIDITÉ: Personnalité créative/littéraire pour améliorer les transitions
|
|
4. STYLE FINAL: Personnalité terrain/accessible pour le style final
|
|
|
|
CRITÈRES:
|
|
- 4 personnalités aux styles DIFFÉRENTS mais complémentaires
|
|
- Adapté au secteur: ${mc0}
|
|
- Variabilité maximale pour anti-détection
|
|
- Éviter les doublons de style
|
|
|
|
FORMAT DE RÉPONSE (EXACTEMENT 4 noms séparés par des virgules):
|
|
Nom1, Nom2, Nom3, Nom4`;
|
|
|
|
const requestData = {
|
|
model: "gpt-4o-mini",
|
|
messages: [{"role": "user", "content": prompt}],
|
|
max_tokens: 100,
|
|
temperature: 1.0
|
|
};
|
|
|
|
// ✅ Retry logic avec backoff exponentiel
|
|
let response;
|
|
let lastError;
|
|
const maxRetries = 3;
|
|
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
logSh(`📡 Appel OpenAI (tentative ${attempt}/${maxRetries})...`, 'DEBUG');
|
|
|
|
response = await axios.post(CONFIG.openai.endpoint, requestData, {
|
|
headers: {
|
|
'Authorization': `Bearer ${CONFIG.openai.apiKey}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
timeout: 30000 // ✅ Timeout réduit à 30s (au lieu de 300s)
|
|
});
|
|
|
|
logSh(`✅ Réponse OpenAI reçue (tentative ${attempt})`, 'DEBUG');
|
|
break; // Succès → sortir de la boucle
|
|
|
|
} catch (error) {
|
|
lastError = error;
|
|
logSh(`⚠️ Tentative ${attempt}/${maxRetries} échouée: ${error.message}`, 'WARNING');
|
|
|
|
if (attempt < maxRetries) {
|
|
const delayMs = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
|
|
logSh(`⏳ Attente ${delayMs/1000}s avant nouvelle tentative...`, 'DEBUG');
|
|
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Si toutes les tentatives ont échoué → Fallback sélection aléatoire
|
|
if (!response) {
|
|
logSh(`⚠️ FALLBACK: Toutes tentatives OpenAI échouées → Sélection aléatoire de 4 personnalités`, 'WARNING');
|
|
|
|
// Sélection aléatoire fallback
|
|
const shuffled = [...randomPersonalities];
|
|
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]];
|
|
}
|
|
|
|
const fallbackPersonalities = shuffled.slice(0, 4);
|
|
|
|
logSh(`🎲 Équipe de 4 personnalités (FALLBACK ALÉATOIRE):`, "INFO");
|
|
fallbackPersonalities.forEach((p, index) => {
|
|
const roles = ['BASE', 'TECHNIQUE', 'FLUIDITÉ', 'STYLE'];
|
|
logSh(` ${index + 1}. ${roles[index]}: ${p.nom} (${p.style})`, "INFO");
|
|
});
|
|
|
|
return fallbackPersonalities;
|
|
}
|
|
|
|
const selectedNames = response.data.choices[0].message.content.trim()
|
|
.split(',')
|
|
.map(name => name.trim());
|
|
|
|
logSh(`🔍 Noms retournés par IA: ${selectedNames.join(', ')}`, "DEBUG");
|
|
|
|
// Mapper aux vraies personnalités
|
|
const selectedPersonalities = [];
|
|
selectedNames.forEach(name => {
|
|
const personality = randomPersonalities.find(p => p.nom === name);
|
|
if (personality) {
|
|
selectedPersonalities.push(personality);
|
|
}
|
|
});
|
|
|
|
// Compléter si pas assez de personnalités trouvées (sécurité)
|
|
while (selectedPersonalities.length < 4 && randomPersonalities.length > selectedPersonalities.length) {
|
|
const remaining = randomPersonalities.filter(p =>
|
|
!selectedPersonalities.some(selected => selected.nom === p.nom)
|
|
);
|
|
if (remaining.length > 0) {
|
|
const randomIndex = Math.floor(Math.random() * remaining.length);
|
|
selectedPersonalities.push(remaining[randomIndex]);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Garantir exactement 4 personnalités
|
|
const final4Personalities = selectedPersonalities.slice(0, 4);
|
|
|
|
logSh(`✅ Équipe de 4 personnalités sélectionnée:`, "INFO");
|
|
final4Personalities.forEach((p, index) => {
|
|
const roles = ['BASE', 'TECHNIQUE', 'FLUIDITÉ', 'STYLE'];
|
|
logSh(` ${index + 1}. ${roles[index]}: ${p.nom} (${p.style})`, "INFO");
|
|
});
|
|
|
|
return final4Personalities;
|
|
|
|
} catch (error) {
|
|
logSh(`❌ FATAL: Sélection multi-personnalités échouée: ${error.message}`, "ERROR");
|
|
throw new Error(`FATAL: Sélection multi-personnalités IA impossible - arrêt du workflow: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* FONCTION LEGACY: Sélection personnalité unique (maintenue pour compatibilité)
|
|
* @param {string} mc0 - Mot-clé principal
|
|
* @param {string} t0 - Titre principal
|
|
* @param {Array} personalities - Liste des personnalités
|
|
* @returns {object} Personnalité sélectionnée
|
|
*/
|
|
async function selectPersonalityWithAI(mc0, t0, personalities) {
|
|
try {
|
|
logSh(`🤖 Sélection personnalité IA UNIQUE pour: ${mc0}`, "DEBUG");
|
|
|
|
// Appeler la fonction multi et prendre seulement la première
|
|
const multiPersonalities = await selectMultiplePersonalitiesWithAI(mc0, t0, personalities);
|
|
const selectedPersonality = multiPersonalities[0];
|
|
|
|
logSh(`✅ Personnalité IA sélectionnée (mode legacy): ${selectedPersonality.nom}`, "INFO");
|
|
|
|
return selectedPersonality;
|
|
|
|
} catch (error) {
|
|
logSh(`❌ FATAL: Sélection personnalité par IA échouée: ${error.message}`, "ERROR");
|
|
throw new Error(`FATAL: Sélection personnalité IA inaccessible - arrêt du workflow: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* CRÉER TEMPLATE XML PAR DÉFAUT quand colonne I contient un nom de fichier
|
|
* Utilise les données CSV disponibles pour créer un template robuste
|
|
*/
|
|
function createDefaultXMLTemplate() {
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<article>
|
|
<header>
|
|
<h1>|Titre_Principal{{T0}}{Rédige un titre H1 accrocheur de maximum 10 mots pour {{MC0}}. Style {{personality.style}}}|</h1>
|
|
<intro>|Introduction{{MC0}}{Rédige une introduction engageante de 2-3 phrases sur {{MC0}}. Ton {{personality.style}}, utilise {{personality.vocabulairePref}}}|</intro>
|
|
</header>
|
|
|
|
<main>
|
|
<section class="primary">
|
|
<h2>|Titre_H2_1{{MC+1_1}}{Crée un titre H2 informatif sur {{MC+1_1}}. Style {{personality.style}}}|</h2>
|
|
<p>|Paragraphe_1{{MC+1_1}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_1}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|</p>
|
|
</section>
|
|
|
|
<section class="secondary">
|
|
<h2>|Titre_H2_2{{MC+1_2}}{Titre H2 pour {{MC+1_2}}. Mets en valeur les points forts. Ton {{personality.style}}}|</h2>
|
|
<p>|Paragraphe_2{{MC+1_2}}{Paragraphe de 4-5 phrases sur {{MC+1_2}}. Détaille pourquoi c'est important pour {{MC0}}. Ton {{personality.style}}}|</p>
|
|
</section>
|
|
|
|
<section class="benefits">
|
|
<h2>|Titre_H2_3{{MC+1_3}}{Titre H2 sur les bénéfices de {{MC+1_3}}. Accrocheur et informatif}|</h2>
|
|
<p>|Paragraphe_3{{MC+1_3}}{Explique en 4-5 phrases les avantages de {{MC+1_3}} pour {{MC0}}. Ton {{personality.style}}}|</p>
|
|
</section>
|
|
</main>
|
|
|
|
<aside class="faq">
|
|
<h2>|FAQ_Titre{Titre de section FAQ accrocheur sur {{MC0}}}|</h2>
|
|
|
|
<div class="faq-item">
|
|
<h3>|Faq_q_1{{MC+1_1}}{Question fréquente sur {{MC+1_1}} et {{MC0}}}|</h3>
|
|
<p>|Faq_a_1{{MC+1_1}}{Réponse claire et précise. 2-3 phrases. Ton {{personality.style}}}|</p>
|
|
</div>
|
|
|
|
<div class="faq-item">
|
|
<h3>|Faq_q_2{{MC+1_2}}{Question pratique sur {{MC+1_2}} en lien avec {{MC0}}}|</h3>
|
|
<p>|Faq_a_2{{MC+1_2}}{Réponse détaillée et utile. 2-3 phrases explicatives. Ton {{personality.style}}}|</p>
|
|
</div>
|
|
|
|
<div class="faq-item">
|
|
<h3>|Faq_q_3{{MC+1_3}}{Question sur {{MC+1_3}} que se posent les clients}|</h3>
|
|
<p>|Faq_a_3{{MC+1_3}}{Réponse complète qui rassure et informe. 2-3 phrases. Ton {{personality.style}}}|</p>
|
|
</div>
|
|
</aside>
|
|
|
|
<footer>
|
|
<p>|Conclusion{{MC0}}{Conclusion engageante de 2 phrases sur {{MC0}}. Appel à l'action subtil. Ton {{personality.style}}}|</p>
|
|
</footer>
|
|
</article>`;
|
|
}
|
|
|
|
/**
|
|
* CRÉER FICHIERS DE DONNÉES D'EXEMPLE
|
|
* Fonction utilitaire pour initialiser les fichiers JSON
|
|
*/
|
|
async function createSampleDataFiles() {
|
|
try {
|
|
// Créer répertoire data s'il n'existe pas
|
|
await fs.mkdir('./data', { recursive: true });
|
|
|
|
// Exemple instructions.json
|
|
const sampleInstructions = [
|
|
{
|
|
slug: "plaque-test",
|
|
t0: "Plaque test signalétique",
|
|
mc0: "plaque signalétique",
|
|
"t-1": "Signalétique",
|
|
"l-1": "/signaletique/",
|
|
"mc+1": "plaque dibond, plaque aluminium, plaque PVC",
|
|
"t+1": "Plaque dibond, Plaque alu, Plaque PVC",
|
|
"l+1": "/plaque-dibond/, /plaque-aluminium/, /plaque-pvc/",
|
|
xmlFileName: "template-plaque.xml"
|
|
}
|
|
];
|
|
|
|
// Exemple personalities.json
|
|
const samplePersonalities = [
|
|
{
|
|
nom: "Marc",
|
|
description: "Expert technique en signalétique",
|
|
style: "professionnel et précis",
|
|
motsClesSecteurs: "technique,dibond,aluminium,impression",
|
|
vocabulairePref: "précision,qualité,expertise,performance",
|
|
connecteursPref: "par ailleurs,en effet,notamment,cependant",
|
|
erreursTypiques: "accord_proximite,repetition_legere",
|
|
longueurPhrases: "moyennes",
|
|
niveauTechnique: "élevé",
|
|
ctaStyle: "découvrir,choisir,commander",
|
|
defautsSimules: "fatigue_cognitive,hesitation_technique"
|
|
},
|
|
{
|
|
nom: "Sophie",
|
|
description: "Passionnée de décoration et design",
|
|
style: "familier et chaleureux",
|
|
motsClesSecteurs: "décoration,design,esthétique,tendances",
|
|
vocabulairePref: "joli,magnifique,tendance,style",
|
|
connecteursPref: "du coup,en fait,sinon,au fait",
|
|
erreursTypiques: "familiarite_excessive,expression_populaire",
|
|
longueurPhrases: "courtes",
|
|
niveauTechnique: "moyen",
|
|
ctaStyle: "craquer,adopter,foncer",
|
|
defautsSimules: "enthousiasme_variable,anecdote_personnelle"
|
|
}
|
|
];
|
|
|
|
// Écrire les fichiers
|
|
await fs.writeFile('./data/instructions.json', JSON.stringify(sampleInstructions, null, 2));
|
|
await fs.writeFile('./data/personalities.json', JSON.stringify(samplePersonalities, null, 2));
|
|
|
|
logSh('✅ Fichiers de données d\'exemple créés dans ./data/', "INFO");
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur création fichiers exemple: ${error.message}`, "ERROR");
|
|
}
|
|
}
|
|
|
|
// ============= EXPORTS NODE.JS =============
|
|
|
|
module.exports = {
|
|
getBrainConfig,
|
|
getPersonalities,
|
|
selectPersonalityWithAI,
|
|
selectMultiplePersonalitiesWithAI, // NOUVEAU: Export de la fonction multi-personnalités
|
|
selectRandomPersonalities,
|
|
parseCSVField,
|
|
readInstructionsData,
|
|
createSampleDataFiles,
|
|
createDefaultXMLTemplate,
|
|
CONFIG
|
|
};
|
|
|
|
// ============= TEST RAPIDE SI LANCÉ DIRECTEMENT =============
|
|
|
|
if (require.main === module) {
|
|
(async () => {
|
|
try {
|
|
logSh('🧪 Test BrainConfig Node.js...', "INFO");
|
|
|
|
// Créer fichiers exemple si nécessaire
|
|
try {
|
|
await fs.access('./data/instructions.json');
|
|
} catch {
|
|
await createSampleDataFiles();
|
|
}
|
|
|
|
// Test de la fonction principale
|
|
const result = await getBrainConfig(2);
|
|
|
|
if (result.success) {
|
|
logSh(`✅ Test réussi: ${result.data.personality.nom} pour ${result.data.mc0}`, "INFO");
|
|
} else {
|
|
logSh(`❌ Test échoué: ${result.error}`, "ERROR");
|
|
}
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur test: ${error.message}`, "ERROR");
|
|
}
|
|
})();
|
|
} |