Corrections majeures: - Normalisation ligatures (œ→oe, æ→ae) pour éviter fragmentation tokens - Normalisation complète lexique (clés + synonymes) sans accents - Correction faux positif "dansent"→"dans" (longueur radical ≥5) Enrichissement lexique (+212 entrées): - Verbes: battre (pulum), penser/réfléchir (umis), voler (aliuk) - Mots grammaticaux: nous (tanu), possessifs (sa/mon→na), démonstratifs (ce→ko) - Temporels: hier/avant (at), demain/après (ok), autour (no) - Formes conjuguées ajoutées pour manger, battre, penser Améliorations techniques: - Lemmatisation verbale améliorée (radical ≥5 lettres) - Système normalizeText() dans lexiqueLoader.js - Liaisons sacrées pour compositions culturelles Note: Problème connu de lemmatisation à investiguer (formes fléchies non trouvées) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
326 lines
9.4 KiB
JavaScript
326 lines
9.4 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'));
|
|
|
|
if (content.dictionnaire) {
|
|
// Fusionner les entrées
|
|
for (const [motFr, data] of Object.entries(content.dictionnaire)) {
|
|
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
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
};
|