seogeneratorserver/fdsm
StillHammer dbf1a3de8c Add technical plan for multi-format export system
Added plan.md with complete architecture for format-agnostic content generation:
- Support for Markdown, HTML, Plain Text, JSON formats
- New FormatExporter module with neutral data structure
- Integration strategy with existing ContentAssembly and ArticleStorage
- Bonus features: SEO metadata generation, readability scoring, WordPress Gutenberg format
- Implementation roadmap with 4 phases (6h total estimated)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 16:14:29 +08:00

555 lines
28 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

diff --git a/.gitignore b/.gitignore
index cff1b08..7217f01 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,8 +53,7 @@ test_*.js
*_debug.js
test-*.js

-# HTML généré (logs viewer)
-logs-viewer.html
+# HTML généré était ici mais maintenant on le garde dans tools/

# Unit test reports
TEST*.xml
diff --git a/code.js b/code.js
index a1dcd72..a2bdb22 100644
--- a/code.js
+++ b/code.js
@@ -1,6 +1,6 @@
/*
code.js — bundle concaténé
- Généré: 2025-09-03T04:21:57.159Z
+ Généré: 2025-09-04T01:10:08.540Z
Source: lib
Fichiers: 16
Ordre: topo
@@ -57,12 +57,13 @@ const fileDest = pino.destination({
});
tee.pipe(fileDest);

-// Custom levels for Pino to include TRACE and PROMPT
+// Custom levels for Pino to include TRACE, PROMPT, and LLM
const customLevels = {
trace: 5, // Below debug (10)
debug: 10,
info: 20,
prompt: 25, // New level for prompts (between info and warn)
+ llm: 26, // New level for LLM interactions (between prompt and warn)
warn: 30,
error: 40,
fatal: 50
@@ -178,6 +179,9 @@ async function logSh(message, level = 'INFO') {
case 'prompt':
logger.prompt(traceData, message);
break;
+ case 'llm':
+ logger.llm(traceData, message);
+ break;
default:
logger.info(traceData, message);
}
@@ -1282,7 +1286,10 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
// 📢 AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA
logSh(`\n🔍 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT');
logSh(prompt, 'PROMPT');
- logSh(`===== FIN PROMPT ${llmProvider.toUpperCase()} (${personality?.nom || 'AUCUNE'}) =====\n`, 'PROMPT');
+ 
+ // 📤 LOG LLM REQUEST COMPLET
+ logSh(`📤 LLM REQUEST [${llmProvider.toUpperCase()}] (${config.model}) | Personnalité: ${personality?.nom || 'AUCUNE'}`, 'LLM');
+ logSh(prompt, 'LLM');

// Préparer la requête selon le provider
const requestData = buildRequestData(llmProvider, prompt, options, personality);
@@ -1293,8 +1300,12 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
// Parser la réponse selon le format du provider
const content = parseResponse(llmProvider, response);

+ // 📥 LOG LLM RESPONSE COMPLET
+ logSh(`📥 LLM RESPONSE [${llmProvider.toUpperCase()}] (${config.model}) | Durée: ${Date.now() - startTime}ms`, 'LLM');
+ logSh(content, 'LLM');
+ 
const duration = Date.now() - startTime;
- logSh(`✅ ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms: "${content.substring(0, 150)}${content.length > 150 ? '...' : ''}"`, 'INFO');
+ logSh(`✅ ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms`, 'INFO');

// Enregistrer les stats d'usage
await recordUsageStats(llmProvider, prompt.length, content.length, duration);
@@ -1727,6 +1738,8 @@ module.exports = {
LLM_CONFIG
};

+
+
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/ElementExtraction.js │
@@ -2309,38 +2322,21 @@ CONTEXTE:
`;

missingElements.forEach((missing, index) => {
- prompt += `${index + 1}. [${missing.name}] `;
- 
- // INSTRUCTIONS SPÉCIFIQUES PAR TYPE
- if (missing.type.includes('titre_h1')) {
- prompt += `→ Titre H1 principal (8-10 mots) pour ${contextAnalysis.mainKeyword}\n`;
- } else if (missing.type.includes('titre_h2')) {
- prompt += `→ Titre H2 section (6-8 mots) lié à ${contextAnalysis.mainKeyword}\n`;
- } else if (missing.type.includes('titre_h3')) {
- prompt += `→ Sous-titre H3 (4-6 mots) spécialisé ${contextAnalysis.mainKeyword}\n`;
- } else if (missing.type.includes('texte') || missing.type.includes('txt')) {
- prompt += `→ Thème/sujet pour paragraphe 150 mots sur ${contextAnalysis.mainKeyword}\n`;
- } else if (missing.type.includes('faq_question')) {
- prompt += `→ Question client directe sur ${contextAnalysis.mainKeyword} (8-12 mots)\n`;
- } else if (missing.type.includes('faq_reponse')) {
- prompt += `→ Thème réponse experte ${contextAnalysis.mainKeyword} (2-4 mots)\n`;
- } else {
- prompt += `→ Expression/mot-clé pertinent ${contextAnalysis.mainKeyword}\n`;
- }
+ prompt += `${index + 1}. [${missing.name}] → Mot-clé SEO\n`;
});

prompt += `\nCONSIGNES:
-- Reste dans le thème ${contextAnalysis.mainKeyword}
-- Varie les angles et expressions
-- Évite répétitions avec mots-clés existants
-- Précis et pertinents
+- Thème: ${contextAnalysis.mainKeyword}
+- Mots-clés SEO naturels
+- Varie les termes
+- Évite répétitions

FORMAT:
[${missingElements[0].name}]
-Expression/mot-clé généré 1
+mot-clé

[${missingElements[1] ? missingElements[1].name : 'exemple'}]
-Expression/mot-clé généré 2
+mot-clé

etc...`;

@@ -2596,6 +2592,11 @@ 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
@@ -2804,8 +2805,8 @@ async function generateAllContentBase(hierarchy, csvData, aiProvider) {
}

/**
- * ÉTAPE 2 - Enhancement technique BATCH OPTIMISÉ avec IA configurable
- * OPTIMISATION : 1 appel extraction + 1 appel enhancement au lieu de 20+
+ * É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');
@@ -2872,96 +2873,96 @@ async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) {
}

/**
- * NOUVELLE FONCTION : Extraction batch TOUS les termes techniques
+ * Analyser un seul élément pour détecter 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.
+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

-CONTENUS À ANALYSER:
-
-${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
-CONTENU: "${baseContents[tag]}"`).join('\n\n')}
+CONTENU À ANALYSER:
+TAG: ${tag}
+CONTENU: "${content}"

CONSIGNES:
-- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
+- 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
-- Si aucun terme technique → "AUCUN"
+- Focus: matériaux, procédés, normes, dimensions, technologies spécifiques

-EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé
-EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique
+EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé, anodisation
+EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique, haute performance

-FORMAT RÉPONSE EXACT:
-[1] dibond, impression UV, 3mm OU AUCUN
-[2] aluminium, fraisage CNC OU AUCUN
-[3] AUCUN
-etc... (${contentEntries.length} lignes total)`;
+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 analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, {
- temperature: 0.3,
- maxTokens: 2000
- }, csvData.personality);
- 
- return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries);
- 
+ 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(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR');
- throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`);
+ logSh(`❌ ERREUR analyse ${tag}: ${error.message}`, 'ERROR');
+ return false; // En cas d'erreur, on skip l'enhancement
}
}

/**
- * NOUVELLE FONCTION : Enhancement batch TOUS les éléments
+ * Enhancer un seul élément techniquement
*/
-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.
+async function enhanceSingleElementTechnical(tag, content, csvData, aiProvider) {
+ const prompt = `MISSION: Améliore ce contenu en intégrant des termes techniques précis.

-PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style})
-CONTEXTE: ${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')}
+CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression

-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
+CONTENU À AMÉLIORER:
+TAG: ${tag}
+CONTENU: "${content}"

-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)`;
+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 enhanced = await callLLM(aiProvider, batchEnhancementPrompt, {
- temperature: 0.4,
- maxTokens: 5000 // Plus large pour batch total
- }, csvData.personality);
- 
- return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement);
- 
+ const enhancedContent = await callLLM(aiProvider, prompt, { temperature: 0.7 });
+ return enhancedContent.trim();
} 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}`);
+ 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
*/
@@ -3015,7 +3016,8 @@ async function enhanceAllTransitions(baseContents, csvData, aiProvider) {

const batchTransitionsPrompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.

-PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style})
+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:
@@ -3093,9 +3095,10 @@ async function enhanceAllPersonalityStyle(baseContents, csvData, aiProvider) {

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}
+STYLE CIBLE: ${personality.style} adapté au web professionnel
VOCABULAIRE: ${personality.vocabulairePref}
CONNECTEURS: ${personality.connecteursPref}
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
@@ -3149,9 +3152,8 @@ etc...`;
/**
* Sleep function replacement for Utilities.sleep
*/
-function sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
-}
+
+// FONCTION SUPPRIMÉE : sleep() dupliquée - déjà définie ligne 12

/**
* RESTAURÉ DEPUIS .GS : Génération des paires FAQ cohérentes
@@ -3187,32 +3189,33 @@ async function generateFAQPairsRestored(faqPairs, csvData, aiProvider) {
function createBatchFAQPairsPrompt(faqPairs, csvData) {
const personality = csvData.personality;

- let prompt = `PERSONNALITÉ: ${personality.nom} | ${personality.description}
-STYLE: ${personality.style}
-VOCABULAIRE: ${personality.vocabulairePref}
-CONNECTEURS: ${personality.connecteursPref}
-NIVEAU TECHNIQUE: ${personality.niveauTechnique}
+ let prompt = `=== 1. CONTEXTE ===
+Entreprise: Autocollant.fr - signalétique personnalisée
+Sujet: ${csvData.mc0}
+Section: FAQ pour article SEO commercial

-GÉNÈRE ${faqPairs.length} PAIRES FAQ COHÉRENTES pour ${csvData.mc0}:
+=== 2. PERSONNALITÉ ===
+Rédacteur: ${personality.nom}
+Style: ${personality.style}
+Ton: ${personality.description || 'professionnel'}

-RÈGLES STRICTES:
-- QUESTIONS: Neutres, directes, langage client naturel (8-15 mots)
-- RÉPONSES: Style ${personality.style}, vocabulaire ${personality.vocabulairePref} (50-80 mots)
-- Sujets à couvrir: prix, livraison, personnalisation, installation, durabilité
-- ÉVITE répétitions excessives et expressions trop familières
-- Style ${personality.nom} reconnaissable mais PROFESSIONNEL
-- PAS de messages d'excuse ("je n'ai pas l'information")
-- RÉPONDS DIRECTEMENT par questions et réponses, sans préfixe
+=== 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 ===

-PAIRES À 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}]
- Question client sur ${csvData.mc0} → Réponse ${personality.style}
+ prompt += `${index + 1}. [${questionTag}] + [${answerTag}] - Paire FAQ naturelle
`;
});

@@ -3533,10 +3536,25 @@ function findAssociatedTitle(textElement, existingResults) {
function createBatchBasePrompt(elements, type, csvData, existingResults = {}) {
const personality = csvData.personality;

- let prompt = `RÉDACTEUR: ${personality.nom} | Style: ${personality.style}
-SUJET: ${csvData.mc0}
-
-${type === 'titre' ? 'GÉNÈRE DES TITRES COURTS ET IMPACTANTS' : `GÉNÈRE ${elements.length} ${type.toUpperCase()}S PROFESSIONNELS`}:
+ 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
@@ -3548,7 +3566,7 @@ ${type === 'titre' ? 'GÉNÈRE DES TITRES COURTS ET IMPACTANTS' : `GÉNÈRE ${el

if (generatedTitles.length > 0) {
prompt += `
-CONTEXTE - TITRES GÉNÉRÉS:
+Titres existants pour contexte:
${generatedTitles.join('\n')}

`;
@@ -3560,34 +3578,33 @@ ${generatedTitles.join('\n')}

prompt += `${index + 1}. [${cleanTag}] `;

- // INSTRUCTIONS SPÉCIFIQUES ET COURTES PAR TYPE
+ // INSTRUCTIONS PROPRES PAR ÉLÉMENT
if (type === 'titre') {
if (elementInfo.element.type === 'titre_h1') {
- prompt += `CRÉER UN TITRE H1 PRINCIPAL (8-12 mots) sur "${csvData.t0}" - NE PAS écrire "Titre_H1_1"\n`;
+ prompt += `Titre principal accrocheur\n`;
} else if (elementInfo.element.type === 'titre_h2') {
- prompt += `CRÉER UN TITRE H2 SECTION (6-10 mots) sur "${csvData.mc0}" - NE PAS écrire "Titre_H2_X"\n`;
+ prompt += `Titre de section engageant\n`;
} else if (elementInfo.element.type === 'titre_h3') {
- prompt += `CRÉER UN TITRE H3 SOUS-SECTION (4-8 mots) - NE PAS écrire "Titre_H3_X"\n`;
+ prompt += `Sous-titre spécialisé\n`;
} else {
- prompt += `CRÉER UN TITRE ACCROCHEUR (4-10 mots) sur "${csvData.mc0}" - NE PAS écrire "Titre_"\n`;
+ prompt += `Titre pertinent\n`;
}
} else if (type === 'texte') {
- const wordCount = elementInfo.element.name && elementInfo.element.name.includes('H2') ? '150' : '100';
- prompt += `Paragraphe ${wordCount} mots, style ${personality.style}\n`;
+ prompt += `Paragraphe informatif\n`;

// ASSOCIER LE TITRE CORRESPONDANT AUTOMATIQUEMENT
const associatedTitle = findAssociatedTitle(elementInfo, existingResults);
if (associatedTitle) {
- prompt += ` Développe le titre: "${associatedTitle}"\n`;
+ prompt += ` Contexte: "${associatedTitle}"\n`;
}

if (elementInfo.element.resolvedContent) {
- prompt += ` Thème: "${elementInfo.element.resolvedContent}"\n`;
+ prompt += ` Angle: "${elementInfo.element.resolvedContent}"\n`;
}
} else if (type === 'intro') {
- prompt += `Introduction 80-100 mots, ton accueillant\n`;
+ prompt += `Introduction engageante\n`;
} else {
- prompt += `Contenu pertinent pour ${csvData.mc0}\n`;
+ prompt += `Contenu pertinent\n`;
}
});

@@ -3597,15 +3614,16 @@ ${generatedTitles.join('\n')}
- Phrases: ${personality.longueurPhrases}
- Niveau technique: ${personality.niveauTechnique}

-CONSIGNES STRICTES:
-- RESPECTE le style ${personality.style} de ${personality.nom} mais RESTE PROFESSIONNEL
-- INTERDICTION ABSOLUE: "du coup", "bon", "alors", "franchement", "nickel", "tip-top", "costaud" en excès
-- VARIE les connecteurs: ${personality.connecteursPref}
-- POUR LES TITRES: SEULEMENT le titre réel, JAMAIS de référence "Titre_H1_1" ou "Titre_H2_7"
-- EXEMPLE TITRE: "Plaques personnalisées résistantes aux intempéries" PAS "Titre_H2_1" 
-- RÉPONDS DIRECTEMENT par le contenu demandé, SANS introduction ni nom de tag
-- PAS de message d'excuse du type "je n'ai pas l'information"
-- CONTENU cohérent et professionnel, évite la sur-familiarité
+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, '')}]
@@ -3696,107 +3714,11 @@ function cleanXMLTagsFromContent(content) {

// ============= PARSING FUNCTIONS =============

-/**
- * 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;
[31