Download jml_v21/texte.py from Kimyayd/FonBench: direct link, hf CLI and curl.
- Browser
- Download file 6.55 kB
-
https://huggingface.co/spaces/Kimyayd/FonBench/resolve/main/jml_v21/texte.py
- Command line
-
hf download hf://spaces/Kimyayd/FonBench/jml_v21/texte.py
-
curl -L -o texte.py https://huggingface.co/spaces/Kimyayd/FonBench/resolve/main/jml_v21/texte.py
6.55 kB
| """Normalisation du texte fon — module autonome du chantier (aucune dépendance au code du dépôt). | |
| Trois espaces d'écriture, du plus riche au plus pauvre : | |
| brut ──normaliser()──► forme canonique : NFD, minuscules, homoglyphes corrigés | |
| ──cible_tonale()─► ce qui se prononce : lettres, espaces, tons ; ni ponctuation ni chiffres | |
| ──sans_tons()───► les mêmes lettres, tons retirés des voyelles | |
| **L'espace d'évaluation** est `espace_eval(t) = sans_tons(cible_tonale(t))`, appliqué à la | |
| référence ET à l'hypothèse. Il reproduit à l'identique la définition utilisée par FonBench et par | |
| le document scientifique du projet : c'est la condition pour que nos chiffres soient comparables | |
| aux 26 modèles externes déjà mesurés. L'identité est **vérifiée par un test** sur tout le corpus | |
| (`pipeline/tests/test_equivalence_texte.py`), pas supposée. | |
| Choix documentés (DECISIONS.md, D-TXT-*) : | |
| - NFD en interne : `ɔ́` et `ɛ́` n'ont pas de forme précomposée ; en NFC le corpus serait mixte. | |
| - Les apostrophes d'élision disparaissent (`d'é` → `dé`) : c'est la convention de l'espace | |
| d'évaluation historique. Un modèle qui les écrirait serait pénalisé à tort. | |
| - Les chiffres disparaissent : la norme veut qu'on transcrive la langue dite ; les références | |
| qui en contiennent encore (0,6 %) sont signalées à part, pas corrigées en silence. | |
| - Un ton n'est retiré que s'il porte sur une voyelle : sur une consonne (`ń`), c'est une lettre. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| import unicodedata | |
| # Tons du fon, en marques combinantes (NFD) | |
| AIGU, CARON, GRAVE, CIRCONFLEXE, MACRON = "́", "̌", "̀", "̂", "̄" | |
| TONS = frozenset({AIGU, CARON, GRAVE, CIRCONFLEXE, MACRON}) | |
| # Diacritiques sans valeur tonale en fon, rencontrés dans le corpus : retirés | |
| PARASITES = frozenset({"̧", "̈", "̣"}) # cédille, tréma, point souscrit | |
| LETTRES = frozenset("abcdefghijklmnopqrstuvwxyz") | {"ɔ", "ɖ", "ɛ"} | |
| CHIFFRES = frozenset("0123456789") | |
| VOYELLES = frozenset("aeiou") | {"ɔ", "ɛ"} | |
| BALISAGE = frozenset("<>+*") | |
| HOMOGLYPHES = { # graphie fautive → lettre ou signe correct | |
| "ε": "ɛ", "Ε": "ɛ", # epsilon grec | |
| "ð": "ɖ", "đ": "ɖ", "ɗ": "ɖ", # d barrés ou crochetés | |
| "‚": ",", "“": '"', "”": '"', "‘": "'", "’": "'", | |
| "œ": "oe", | |
| } | |
| _ESPACES = re.compile(r" {2,}") | |
| def normaliser(texte: str) -> str: | |
| """Forme canonique : NFD, minuscules, homoglyphes corrigés. Idempotente.""" | |
| t = unicodedata.normalize("NFD", texte).lower() | |
| t = unicodedata.normalize("NFD", t) # la minusculisation peut recomposer | |
| for faux, juste in HOMOGLYPHES.items(): | |
| t = t.replace(faux, juste) | |
| return unicodedata.normalize("NFD", t) | |
| def cible_tonale(texte: str, chiffres: bool = False) -> str: | |
| """Ce qui se prononce : lettres, espaces et tons. Tout le reste est retiré. | |
| `chiffres=True` (D-NUM-1, variante d'entraînement) : chaque chiffre 0-9 est gardé comme un mot isolé | |
| (« 21h » → « 2 1 h ») ; `recoller_chiffres` refait les nombres après décodage. L'espace d'évaluation | |
| officiel, lui, ne change pas (chiffres retirés).""" | |
| sortie: list[str] = [] | |
| for c in normaliser(texte): | |
| if c in TONS: | |
| # un ton sans lettre avant lui (tapé après une espace) ne se prononce pas | |
| if sortie and sortie[-1] != " ": | |
| sortie.append(c) | |
| elif chiffres and c in CHIFFRES: | |
| sortie.extend((" ", c, " ")) # un chiffre = un « mot » (KenLM : vocabulaire fermé 0-9) | |
| elif c in LETTRES or c == " ": | |
| sortie.append(c) | |
| elif c.isdigit() or c in PARASITES or c in BALISAGE \ | |
| or unicodedata.category(c).startswith("P"): | |
| continue | |
| elif c.isspace(): | |
| sortie.append(" ") | |
| # tout autre caractère (hors alphabet) est retiré | |
| return _ESPACES.sub(" ", "".join(sortie)).strip() | |
| def sans_tons(texte: str) -> str: | |
| """Retire les tons portés par une voyelle ; garde ceux d'une consonne (ce sont des lettres).""" | |
| sortie, base = [], "" | |
| for c in unicodedata.normalize("NFD", texte): | |
| if c in TONS: | |
| if base in VOYELLES: | |
| continue | |
| else: | |
| base = c | |
| sortie.append(c) | |
| return "".join(sortie) | |
| def espace_eval(texte: str, chiffres: bool = False) -> str: | |
| """Espace de mesure du WER sans tons : identique à FonBench (avec `chiffres=False`).""" | |
| return " ".join(sans_tons(cible_tonale(texte, chiffres)).split()) | |
| def espace_eval_tonal(texte: str, chiffres: bool = False) -> str: | |
| """Espace de mesure du WER avec tons.""" | |
| return " ".join(cible_tonale(texte, chiffres).split()) | |
| def recoller_chiffres(t: str) -> str: | |
| """Sortie d'un modèle D-NUM-1 : les chiffres isolés consécutifs redeviennent un nombre (« 2 1 » → « 21 »).""" | |
| out: list[str] = [] | |
| for w in t.split(): | |
| if w.isdigit() and out and out[-1].isdigit(): | |
| out[-1] += w | |
| else: | |
| out.append(w) | |
| return " ".join(out) | |
| def suite_chiffres(t: str) -> str: | |
| """Mesure D-NUM-1 : la suite ordonnée des chiffres d'un texte, sans séparateurs (insensible au groupement).""" | |
| return "".join(c for c in unicodedata.normalize("NFD", t) if c in CHIFFRES) | |
| def _auto_test() -> None: | |
| assert espace_eval("Un nɔ́ ɖɔxó, kpódó mɛ!") == "un nɔ ɖɔxo kpodo mɛ" | |
| assert espace_eval("D'é mɛ 2026 \"bureau\".") == "dé mɛ bureau".replace("é", "e") | |
| assert espace_eval_tonal("Kɔ́ 2 026 F, 3.5 %", chiffres=True) == unicodedata.normalize("NFD", "kɔ́ 2 0 2 6 f 3 5") | |
| assert espace_eval("ɖo 21h", chiffres=True) == "ɖo 2 1 h" and recoller_chiffres("ɖo 2 1 h 0 5") == "ɖo 21 h 05" | |
| assert suite_chiffres("Numéro : 05 55, le 2 026") == "05552026" | |
| assert espace_eval("Kɔ 2026", chiffres=False) == "kɔ" | |
| assert espace_eval_tonal("Mǐ ná yi") == unicodedata.normalize("NFD", "mǐ ná yi") | |
| assert espace_eval("ɛ́ ε ð") == "ɛ ɛ ɖ" # homoglyphes | |
| assert sans_tons(unicodedata.normalize("NFD", "ń")) == unicodedata.normalize("NFD", "ń") | |
| assert espace_eval(" a ́ b ") == "a b" # ton orphelin, espaces multiples | |
| for t in ("Nɛ́ ká gbɔn", "É cí nú mì ɖɔ"): | |
| assert normaliser(normaliser(t)) == normaliser(t) # idempotence | |
| print("texte.py : auto-test OK") | |
| if __name__ == "__main__": | |
| _auto_test() | |