confluent/ConfluentTranslator/src/utils/lexiqueLoader.js
StillHammer 4b0f916d1c Restructuration complète du projet ConfluentTranslator
- Nouvelle architecture modulaire avec src/api, src/core, src/utils
- Séparation claire docs/ (admin, changelog, dev, security) et tests/ (unit, integration, scripts)
- server.js devient un simple point d'entrée
- Ajout de STRUCTURE.md documentant l'architecture
- Archivage ancien-confluent/ avec générateur de lexique complet

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 23:28:12 +08:00

337 lines
9.9 KiB
JavaScript

const fs = require('fs');
const path = require('path');
/**
* Normalise un texte : lowercase + retire accents + ligatures
* @param {string} text - Texte à normaliser
* @returns {string} - Texte normalisé
*/
function normalizeText(text) {
return text
.toLowerCase()
.replace(/œ/g, 'oe')
.replace(/æ/g, 'ae')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '');
}
/**
* Charge dynamiquement tous les fichiers de lexique d'un dossier
* @param {string} lexiqueDir - Chemin vers le dossier contenant les fichiers JSON
* @returns {Object} - Lexique fusionné avec métadonnées
*/
function loadLexiqueFromDir(lexiqueDir) {
const result = {
meta: {
source_dir: lexiqueDir,
files_loaded: [],
total_entries: 0,
loaded_at: new Date().toISOString()
},
dictionnaire: {}
};
if (!fs.existsSync(lexiqueDir)) {
console.warn(`Lexique directory not found: ${lexiqueDir}`);
return result;
}
const files = fs.readdirSync(lexiqueDir)
.filter(f => f.endsWith('.json') && !f.startsWith('_'));
for (const file of files) {
const filePath = path.join(lexiqueDir, file);
try {
const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
// Fonction helper pour merger des entrées
const mergeEntries = (entries, sectionName = 'dictionnaire') => {
for (const [motFr, data] of Object.entries(entries)) {
const key = normalizeText(motFr);
if (!result.dictionnaire[key]) {
result.dictionnaire[key] = {
mot_francais: motFr,
traductions: [],
synonymes_fr: [],
source_files: []
};
}
// Ajouter les traductions
if (data.traductions) {
for (const trad of data.traductions) {
// Éviter les doublons
const exists = result.dictionnaire[key].traductions.some(
t => t.confluent === trad.confluent
);
if (!exists) {
result.dictionnaire[key].traductions.push({
...trad,
source_file: file,
source_section: sectionName
});
}
}
}
// Ajouter les synonymes
if (data.synonymes_fr) {
for (const syn of data.synonymes_fr) {
if (!result.dictionnaire[key].synonymes_fr.includes(syn)) {
result.dictionnaire[key].synonymes_fr.push(syn);
}
// Créer une entrée pour le synonyme qui pointe vers le mot principal
const synKey = normalizeText(syn);
if (!result.dictionnaire[synKey]) {
result.dictionnaire[synKey] = {
mot_francais: syn,
traductions: result.dictionnaire[key].traductions,
synonymes_fr: [motFr],
source_files: [file],
is_synonym_of: motFr
};
}
}
}
if (!result.dictionnaire[key].source_files.includes(file)) {
result.dictionnaire[key].source_files.push(file);
}
}
};
// Charger la section "dictionnaire" si elle existe
if (content.dictionnaire) {
mergeEntries(content.dictionnaire, 'dictionnaire');
}
// Charger la section "pronoms" si elle existe (pour 02-racines-standards.json)
if (content.pronoms) {
mergeEntries(content.pronoms, 'pronoms');
}
result.meta.files_loaded.push(file);
} catch (error) {
console.error(`Error loading ${file}:`, error.message);
}
}
result.meta.total_entries = Object.keys(result.dictionnaire).length;
return result;
}
/**
* Charge et fusionne le lexique simple depuis data/lexique-francais-confluent.json
* @param {string} baseDir - Chemin de base du projet
* @param {Object} existingLexique - Lexique existant à enrichir
* @returns {Object} - Lexique enrichi
*/
function mergeSimpleLexique(baseDir, existingLexique) {
const simpleLexiquePath = path.join(baseDir, 'data', 'lexique-francais-confluent.json');
if (!fs.existsSync(simpleLexiquePath)) {
console.warn(` Simple lexique not found: ${simpleLexiquePath}`);
return existingLexique;
}
try {
const content = JSON.parse(fs.readFileSync(simpleLexiquePath, 'utf-8'));
let addedCount = 0;
// Parcourir le dictionnaire simple (structure: {"mot": "traduction"})
if (content.dictionnaire) {
for (const [section, entries] of Object.entries(content.dictionnaire)) {
if (typeof entries === 'object') {
for (const [motFr, traduction] of Object.entries(entries)) {
const key = normalizeText(motFr);
// N'ajouter que si pas déjà présent
if (!existingLexique.dictionnaire[key]) {
existingLexique.dictionnaire[key] = {
mot_francais: motFr,
traductions: [{
confluent: traduction,
type: 'racine', // Type par défaut
forme_liee: traduction,
domaine: 'general'
}],
synonymes_fr: [],
source_files: ['data/lexique-francais-confluent.json']
};
addedCount++;
}
}
}
}
}
console.log(` Merged ${addedCount} entries from simple lexique`);
existingLexique.meta.total_entries = Object.keys(existingLexique.dictionnaire).length;
} catch (error) {
console.error(` Error merging simple lexique: ${error.message}`);
}
return existingLexique;
}
/**
* Charge les lexiques pour les deux variantes de la langue
* @param {string} baseDir - Chemin de base du projet confluent
* @returns {Object} - Lexiques proto et ancien
*/
function loadAllLexiques(baseDir) {
const protoDir = path.join(baseDir, 'proto-confluent', 'lexique');
const ancienDir = path.join(baseDir, 'ancien-confluent', 'lexique');
console.log('Loading Proto-Confluent lexique...');
const proto = loadLexiqueFromDir(protoDir);
console.log(` Loaded ${proto.meta.total_entries} entries from ${proto.meta.files_loaded.length} files`);
console.log('Loading Ancien-Confluent lexique...');
let ancien = loadLexiqueFromDir(ancienDir);
console.log(` Loaded ${ancien.meta.total_entries} entries from ${ancien.meta.files_loaded.length} files`);
// Fusionner le lexique simple
ancien = mergeSimpleLexique(baseDir, ancien);
return { proto, ancien };
}
/**
* Construit un index inversé (confluent -> français) pour recherche rapide
* @param {Object} lexique - Lexique chargé
* @returns {Object} - Index inversé
*/
function buildReverseIndex(lexique) {
const index = {};
for (const [key, data] of Object.entries(lexique.dictionnaire)) {
if (data.traductions) {
for (const trad of data.traductions) {
const confluentWord = trad.confluent.toLowerCase();
if (!index[confluentWord]) {
index[confluentWord] = [];
}
index[confluentWord].push({
francais: data.mot_francais,
type: trad.type,
domaine: trad.domaine
});
}
}
}
return index;
}
/**
* Recherche un mot dans le lexique
* @param {Object} lexique - Lexique chargé
* @param {string} query - Mot à rechercher
* @param {string} direction - 'fr2conf' ou 'conf2fr'
* @returns {Array} - Résultats de recherche
*/
function searchLexique(lexique, query, direction = 'fr2conf') {
const results = [];
const queryLower = normalizeText(query);
if (direction === 'fr2conf') {
// Recherche exacte
if (lexique.dictionnaire[queryLower]) {
results.push({
match: 'exact',
...lexique.dictionnaire[queryLower]
});
}
// Recherche partielle
for (const [key, data] of Object.entries(lexique.dictionnaire)) {
if (key !== queryLower && key.includes(queryLower)) {
results.push({
match: 'partial',
...data
});
}
}
} else {
// Recherche dans les traductions (confluent -> français)
for (const [key, data] of Object.entries(lexique.dictionnaire)) {
if (data.traductions) {
for (const trad of data.traductions) {
if (trad.confluent.toLowerCase() === queryLower) {
results.push({
match: 'exact',
...data
});
break;
} else if (trad.confluent.toLowerCase().includes(queryLower)) {
results.push({
match: 'partial',
...data
});
break;
}
}
}
}
}
return results;
}
/**
* Génère un résumé du lexique pour inclusion dans les prompts LLM
* @param {Object} lexique - Lexique chargé
* @param {number} maxEntries - Nombre max d'entrées à inclure
* @returns {string} - Résumé formaté
*/
function generateLexiqueSummary(lexique, maxEntries = 200) {
const lines = [];
let count = 0;
// Grouper par domaine
const byDomain = {};
for (const [key, data] of Object.entries(lexique.dictionnaire)) {
if (data.is_synonym_of) continue; // Skip synonyms
if (!data.traductions || data.traductions.length === 0) continue;
const trad = data.traductions[0];
const domain = trad.domaine || 'general';
if (!byDomain[domain]) {
byDomain[domain] = [];
}
byDomain[domain].push({
fr: data.mot_francais,
conf: trad.confluent,
type: trad.type
});
}
// Formater par domaine
for (const [domain, entries] of Object.entries(byDomain).sort()) {
if (count >= maxEntries) break;
lines.push(`\n## ${domain.toUpperCase()}`);
for (const entry of entries.slice(0, 20)) {
if (count >= maxEntries) break;
lines.push(`${entry.fr}: ${entry.conf}`);
count++;
}
}
return lines.join('\n');
}
module.exports = {
loadLexiqueFromDir,
loadAllLexiques,
buildReverseIndex,
searchLexique,
generateLexiqueSummary
};