confluent/docs/TRAVAIL_RACINES_FRANCAISES.md
2025-12-04 20:12:10 +08:00

15 KiB

Travail : Système de Racines Françaises

Objectif

Améliorer le matching des mots français conjugués/accordés en utilisant un système de racines françaises au lieu de se fier uniquement à la lemmatisation automatique.

Problème actuel

  • "manger" trouve "mukis"
  • "mangé" ne trouve RIEN
  • "vu" ne trouve pas "voir"
  • "pris" ne trouve pas "prendre"

Le lemmatizer actuel est trop basique et rate beaucoup de formes conjuguées.

Solution : Système hybride avec racines manuelles

1. Déclaration manuelle des racines françaises dans le lexique

Principe : Ajouter un champ racine_fr dans chaque entrée du lexique avec le dénominateur commun le plus long qui couvre toutes les conjugaisons/formes du mot.

IMPORTANT : Ce n'est PAS une extraction automatique des 4 premières lettres. C'est un travail manuel intelligent pour déterminer la meilleure racine pour chaque mot.

Exemples :

  • "manger" → racine_fr: "mang" (4 lettres) → couvre mangé, mange, mangeait, mangerons...
  • "donner" → racine_fr: "donn" (4 lettres) → couvre donné, donne, donnait, donnerons...
  • "comparer" → racine_fr: "compar" (6 lettres) → couvre comparé, compare, comparaison...
  • "comprendre" → racine_fr: "compr" (5 lettres) → couvre compris, comprend, comprenait...

Structure dans le lexique :

"manger": {
  "racine_fr": "mang",
  "traductions": [...],
  "synonymes_fr": ["mange", "mangé", "mangeait"]
}

"comparer": {
  "racine_fr": "compar",
  "traductions": [...],
  "synonymes_fr": ["comparé", "compare", "comparaison"]
}

"comprendre": {
  "racine_fr": "compr",
  "traductions": [...],
  "synonymes_fr": ["compris", "comprend", "comprenait"]
}

Comportement du matching :

Quand l'utilisateur tape "comparé" :

  1. Le système cherche les entrées dont racine_fr matche le début de "comparé"
  2. Il trouve "compar" → match "comparer"
  3. Il ne trouve PAS "compr" (trop court) → ne propose pas "comprendre"

Quand l'utilisateur tape "comprend" :

  1. Le système trouve :
    • "compr" (5 lettres) → match "comprendre"
    • "comp" pourrait aussi matcher si on avait un mot avec cette racine
  2. En cas de multiples matches, le système retourne TOUS les candidats au LLM
  3. Le LLM choisit selon le contexte de la phrase

Avantages :

  • Contrôle total sur la longueur de chaque racine (4, 5, 6 lettres selon le besoin)
  • Gère les ambiguïtés en laissant le LLM arbitrer
  • Plus précis qu'une extraction automatique aveugle

2. Formes irrégulières dans synonymes_fr

Principe : Ajouter toutes les formes conjuguées dans le champ synonymes_fr du lexique

Exemples de cas nécessitant synonymes_fr :

Formes trop différentes de la racine

  • "voir"racine_fr: "voi" couvre "voit", "voyait", "voyons"

    • MAIS "vu", "vus", "vue", "vues" sont trop différents → à mettre dans synonymes_fr
  • "être" → formes trop différentes (est, suis, sont, été, était, sera, fut...)

    • Impossible d'avoir une racine unique → tout dans synonymes_fr
  • "avoir" → formes trop différentes (a, ai, ont, eu, avait, aura...)

    • Impossible d'avoir une racine unique → tout dans synonymes_fr

Verbes irréguliers avec changement de radical

  • "prendre"racine_fr: "pren" couvre "prend", "prenait", "prendra"

    • MAIS "pris", "prit", "prise" ont un radical différent → dans synonymes_fr
  • "faire"racine_fr: "fai" couvre "fais", "faisait", "faisons"

    • MAIS "fait", "faite", "faites" ont un radical différent → dans synonymes_fr

Combinaison racine + synonymes :

"voir": {
  "racine_fr": "voi",
  "synonymes_fr": ["vu", "vus", "vue", "vues", "observer", "regarder"]
}

"prendre": {
  "racine_fr": "pren",
  "synonymes_fr": ["pris", "prit", "prise", "prises"]
}

Le système essaie d'abord la racine, puis les synonymes.


TRAVAIL 1 : Modification du code (contextAnalyzer.js)

Fichier : ConfluentTranslator/contextAnalyzer.js

Modifications à faire :

A. Modifier searchWord() pour utiliser le champ racine_fr

Principe : Chercher les entrées dont le champ racine_fr est contenu au début du mot recherché.

Emplacement : ligne ~124 dans la fonction searchWord()

Modifier la logique de matching pour ajouter :

    // NOUVEAU: Correspondance sur racine française déclarée
    // Si l'entrée a un champ racine_fr et que le mot commence par cette racine
    else if (entry.racine_fr && word.startsWith(entry.racine_fr.toLowerCase())) {
      score = 0.75;
    }

Insertion dans la cascade de matching (après synonymes lemmatisés, avant le return) :

    // Correspondance exacte sur le mot français
    if (key === word || entry.mot_francais?.toLowerCase() === word) {
      score = 1.0;
    }
    // Correspondance sur formes lemmatisées
    else if (lemmas.some(lemma => key === lemma || entry.mot_francais?.toLowerCase() === lemma)) {
      score = 0.95;
    }
    // Correspondance sur synonymes
    else if (entry.synonymes_fr?.some(syn => syn.toLowerCase() === word)) {
      score = 0.9;
    }
    // Correspondance sur synonymes lemmatisés
    else if (entry.synonymes_fr?.some(syn => lemmas.includes(syn.toLowerCase()))) {
      score = 0.85;
    }
    // NOUVEAU: Correspondance sur racine française déclarée
    else if (entry.racine_fr && word.startsWith(entry.racine_fr.toLowerCase())) {
      score = 0.75;
    }

Code final de searchWord() :

function searchWord(word, dictionnaire) {
  const results = [];
  const lemmas = simpleLemmatize(word);

  for (const [key, entry] of Object.entries(dictionnaire)) {
    let score = 0;

    // Correspondance exacte sur le mot français
    if (key === word || entry.mot_francais?.toLowerCase() === word) {
      score = 1.0;
    }
    // Correspondance sur formes lemmatisées
    else if (lemmas.some(lemma => key === lemma || entry.mot_francais?.toLowerCase() === lemma)) {
      score = 0.95;
    }
    // Correspondance sur synonymes
    else if (entry.synonymes_fr?.some(syn => syn.toLowerCase() === word)) {
      score = 0.9;
    }
    // Correspondance sur synonymes lemmatisés
    else if (entry.synonymes_fr?.some(syn => lemmas.includes(syn.toLowerCase()))) {
      score = 0.85;
    }
    // NOUVEAU: Correspondance sur racine française déclarée dans le lexique
    else if (entry.racine_fr && word.startsWith(entry.racine_fr.toLowerCase())) {
      score = 0.75;
    }

    if (score > 0) {
      results.push({
        mot_francais: entry.mot_francais || key,
        traductions: entry.traductions || [],
        score,
        source: entry.source_files || []
      });
    }
  }

  return results;
}

Note : Pas besoin d'exporter de nouvelle fonction, on utilise simplement le champ racine_fr du lexique.


TRAVAIL 2 : Compléter le lexique avec racines et synonymes

Objectif : Pour chaque verbe du lexique, ajouter :

  1. Le champ racine_fr avec le dénominateur commun optimal
  2. Les formes conjuguées irrégulières dans synonymes_fr

Fichiers à modifier :

ancien-confluent/lexique/06-actions.json

Structure à suivre pour chaque verbe

"[verbe_infinitif]": {
  "racine_fr": "[racine_optimale]",
  "traductions": [...],
  "synonymes_fr": ["[formes_irrégulières]", "[autres_synonymes]"]
}

Verbes à compléter (par priorité)

Priorité 1 : Verbes très courants

1. voir

"voir": {
  "racine_fr": "voi",
  "traductions": [...],
  "synonymes_fr": ["observer", "regarder", "vu", "vus", "vue", "vues"]
}

Note : "voit", "voyait", "voyons", "verra" sont couverts par la racine "voi". Seules les formes avec radical différent (vu/vue) vont dans synonymes_fr.

2. prendre

"prendre": {
  "racine_fr": "pren",
  "traductions": [...],
  "synonymes_fr": ["pris", "prit", "prise", "prises"]
}

Note : "prend", "prenait", "prendra" sont couverts par la racine "pren".

3. faire

"faire": {
  "racine_fr": "fai",
  "traductions": [...],
  "synonymes_fr": ["créer", "fait", "faits", "faite", "faites"]
}

Note : "fais", "faisait", "faisons" sont couverts par la racine "fai".

4. manger (nouveau verbe si pas déjà présent)

"manger": {
  "racine_fr": "mang",
  "traductions": [...],
  "synonymes_fr": []
}

Note : Toutes les formes (mange, mangé, mangeait, mangera) sont couvertes par la racine "mang". Pas besoin de synonymes.

5. aller

"aller": {
  "racine_fr": "all",
  "traductions": [...],
  "synonymes_fr": ["va", "vas", "vais", "ira", "iras", "iront"]
}

Note : "allé", "allait" sont couverts par "all". Les formes irrégulières (va/vais, ira) vont dans synonymes_fr.

6. donner

"donner": {
  "racine_fr": "donn",
  "traductions": [...],
  "synonymes_fr": []
}

Note : Toutes les formes sont couvertes par "donn".

7. dire

"dire": {
  "racine_fr": "di",
  "traductions": [...],
  "synonymes_fr": ["parler", "dit", "dits", "dite", "dites"]
}

Note : "dis", "disait", "dira" sont couverts par "di". "dit/dite" ont un radical différent donc dans synonymes_fr.

Priorité 2 : Verbes auxiliaires (très irréguliers)

8. être

"être": {
  "racine_fr": null,
  "traductions": [...],
  "synonymes_fr": ["est", "es", "suis", "sont", "sommes", "êtes", "été", "était", "étais", "étant", "sera", "seras", "seront", "fut", "fus"]
}

Note : Aucune racine commune possible. Toutes les formes dans synonymes_fr.

9. avoir

"avoir": {
  "racine_fr": "av",
  "traductions": [...],
  "synonymes_fr": ["a", "as", "ai", "ont", "eu", "eue", "eus", "eues", "aura", "auras", "auront"]
}

Note : "avait", "avais", "avons", "avez" couverts par "av". Formes irrégulières (a/ai/ont, eu, aura) dans synonymes_fr.

Priorité 3 : Autres verbes courants

10. savoir

"savoir": {
  "racine_fr": "sav",
  "traductions": [...],
  "synonymes_fr": ["connaître", "sait", "sais", "su", "sue", "sus", "sues", "saura", "sauront"]
}

Note : "savait", "savons", "sachant" couverts par "sav". Formes irrégulières (sait/sais, su, saura) dans synonymes_fr.

11. chasser

"chasser": {
  "racine_fr": "chass",
  "traductions": [...],
  "synonymes_fr": ["traquer"]
}

Note : Toutes les conjugaisons couvertes par "chass".

12. transmettre

"transmettre": {
  "racine_fr": "transm",
  "traductions": [...],
  "synonymes_fr": ["enseigner", "transmis", "transmise", "transmises"]
}

Note : "transmet", "transmettait", "transmettra" couverts par "transm". Participe passé (transmis) dans synonymes_fr.

13. garder

"garder": {
  "racine_fr": "gard",
  "traductions": [...],
  "synonymes_fr": ["protéger"]
}

Note : Toutes les conjugaisons couvertes par "gard".

14. porter

"porter": {
  "racine_fr": "port",
  "traductions": [...],
  "synonymes_fr": ["transporter"]
}

Note : Toutes les conjugaisons couvertes par "port".

15. apprendre

"apprendre": {
  "racine_fr": "appren",
  "traductions": [...],
  "synonymes_fr": ["appris", "apprise", "apprises"]
}

Note : "apprend", "apprenait", "apprendra" couverts par "appren". Participe passé (appris) dans synonymes_fr.


Tests à faire après modifications

Test 1 : Vérifier les racines automatiques

curl -X POST http://localhost:3000/api/debug/prompt \
  -H "Content-Type: application/json" \
  -d '{"text": "Il mangeait le pain", "target": "ancien"}' | grep -A 5 "wordsFound"

Résultat attendu : "mangeait" → trouve "mukis" via racine "mang"

Test 2 : Vérifier les exceptions manuelles

curl -X POST http://localhost:3000/api/debug/prompt \
  -H "Content-Type: application/json" \
  -d '{"text": "Il a vu et pris", "target": "ancien"}' | grep -A 10 "wordsFound"

Résultat attendu :

  • "vu" → trouve "mirak" via synonymes_fr
  • "pris" → trouve "pasak" via synonymes_fr

Test 3 : Phrase complète

# Même test que le problématique
curl -X POST http://localhost:3000/api/debug/prompt \
  -H "Content-Type: application/json" \
  -d '{"text": "C est le collier du loup qui a mange mon frere", "target": "ancien"}' | python -c "import sys, json; data=json.load(sys.stdin); print('Found:', [w['input'] for w in data['metadata']['wordsFound']])"

Résultat attendu : ["collier", "loup", "mange", "frere"] tous trouvés


Estimation de travail

  • Code (contextAnalyzer.js) : ~5 lignes à ajouter (une seule condition else if), 5 minutes
  • Lexique (06-actions.json) :
    • Ajouter champ racine_fr pour 15 verbes : ~15 minutes
    • Ajouter formes irrégulières dans synonymes_fr : ~30 formes uniques, ~20 minutes

Total : ~40 minutes de travail

Révision par rapport à l'estimation initiale : Beaucoup plus simple car on ne fait pas d'extraction automatique, juste du matching sur un champ déclaré.


Notes importantes

  1. Lowercase : Tout est déjà en lowercase dans tokenizeFrench(), pas besoin de le refaire dans le matching
  2. Normalisation des accents : Déjà fait en amont (mangé → mange dans la tokenization)
  3. Ordre de priorité matching :
    • 1.0 = Exacte
    • 0.95 = Lemma
    • 0.9 = Synonyme exact
    • 0.85 = Synonyme lemmatisé
    • 0.75 = Racine française déclarée (NOUVEAU)
  4. Choix de la racine :
    • Doit être le dénominateur commun le plus long qui couvre la majorité des conjugaisons
    • Exemples : "mang" (4 lettres), "compar" (6 lettres), "transm" (6 lettres)
    • Pas de règle fixe sur la longueur : décision au cas par cas
  5. Multiples matches :
    • Si plusieurs entrées matchent (ex: "comp" et "compr"), TOUTES sont retournées au LLM
    • Le LLM choisit selon le contexte de la phrase
    • Pas de filtrage automatique

Résumé du système final

Workflow de matching pour un mot tapé par l'utilisateur

  1. L'utilisateur tape "comparé"
  2. Le système tokenize et met en lowercase → "compare" (accent normalisé)
  3. Le système cherche dans le lexique avec cette cascade :
    • Score 1.0 : Match exact sur clé ou mot_francais
    • Score 0.95 : Match sur lemme →
    • Score 0.9 : Match exact dans synonymes_fr
    • Score 0.85 : Match lemme dans synonymes_fr
    • Score 0.75 : "compare".startsWith(entry.racine_fr) trouve "compar" dans l'entrée "comparer"
  4. Le système retourne l'entrée "comparer" avec score 0.75
  5. Le LLM utilise la traduction confluente

Avantages de cette approche

  • Contrôle total : chaque racine est choisie manuellement pour être optimale
  • Ambiguïté gérée : multiples matches possibles, le LLM arbitre
  • Performance : pas de calcul complexe, juste du startsWith()
  • Maintenance : facile d'ajuster une racine si elle ne convient pas
  • Hybride intelligent : racines pour les formes régulières, synonymes pour les irrégulières