Spaces:
Running
Running
| # -*- coding: utf-8 -*- | |
| """ | |
| Normalisation de texte malgache (PLT) pour TTS. | |
| A appeler AVANT la phonemisation / Piper a l'inference. | |
| from normalize_plt import normalize_plt | |
| texte_pret = normalize_plt("Dr. Rakoto dia manana 25 taona.") | |
| Regles des nombres : languagesandnumbers.com (merina standard). | |
| Traductions titres/unites/symboles : validees DeepSeek + Gemini. | |
| """ | |
| import re | |
| # ============================================================ | |
| # 1. NOMBRES -> malgache en toutes lettres | |
| # ============================================================ | |
| UNITS = {0:"aotra",1:"iray",2:"roa",3:"telo",4:"efatra",5:"dimy", | |
| 6:"enina",7:"fito",8:"valo",9:"sivy"} | |
| UNITS_COMPOSED = {1:"iraika"} | |
| TENS = {2:"roapolo",3:"telopolo",4:"efapolo",5:"dimampolo", | |
| 6:"enimpolo",7:"fitopolo",8:"valopolo",9:"sivifolo"} | |
| HUNDREDS = {2:"roanjato",3:"telonjato",4:"efajato",5:"dimanjato", | |
| 6:"eninjato",7:"fitonjato",8:"valonjato",9:"sivinjato"} | |
| def _sub1000(n, grand_contexte=False): | |
| u = n % 10; d = (n // 10) % 10; c = n // 100 | |
| bas_parts = [] | |
| if u: | |
| bas_parts.append(UNITS_COMPOSED.get(u, UNITS[u]) if (d or c) else UNITS[u]) | |
| if d == 1 and u: | |
| bas = bas_parts[0] + " ambin'ny folo" | |
| elif d == 1: | |
| bas = "folo" | |
| elif d >= 2: | |
| bas_parts.append(TENS[d]); bas = " amby ".join(bas_parts) | |
| else: | |
| bas = " amby ".join(bas_parts) if bas_parts else "" | |
| cent = "" | |
| if c == 1: cent = "zato" | |
| elif c >= 2: cent = HUNDREDS[c] | |
| if bas and cent: | |
| return bas + (" sy " if grand_contexte else " amby ") + cent | |
| return bas or cent | |
| def _compose_grand(n): | |
| if n < 1000: | |
| return _sub1000(n, grand_contexte=False) | |
| parts = []; reste = n | |
| for val, name in [(10**6,"tapitrisa"),(10**5,"hetsy"),(10**4,"alina"),(1000,"arivo")]: | |
| q = reste // val | |
| if q: | |
| if val >= 10**4: | |
| coef = "iray" if q == 1 else _compose_grand(q) | |
| parts.append((coef + " " + name, val)) | |
| else: | |
| coef = "" if q == 1 else _compose_grand(q) + " " | |
| parts.append((coef + name, val)) | |
| reste %= val | |
| if reste: | |
| parts.append((_sub1000(reste, grand_contexte=True), 1)) | |
| parts_sorted = [p[0] for p in sorted(parts, key=lambda x: x[1])] | |
| return " sy ".join(parts_sorted) | |
| def nombre_malgache(n): | |
| n = int(n) | |
| if n == 0: return "aotra" | |
| if n < 0: return "minisy " + _compose_grand(-n) | |
| return _compose_grand(n) | |
| # ============================================================ | |
| # 2. TABLES (validees DeepSeek + Gemini) | |
| # ============================================================ | |
| TITRES = { | |
| r"\bDr\.?": "Dokotera", | |
| r"\bPr\.?": "Profesora", | |
| r"\bMme\b": "Ramatoa", | |
| r"\bRtoa\b": "Ramatoa", | |
| r"\bMlle\b": "Ramatoakely", | |
| r"\bMelle\b": "Ramatoakely", | |
| r"\bRkely\b": "Ramatoakely", | |
| r"\bMr\.?": "Andriamatoa", | |
| r"\bAmtoa\b": "Andriamatoa", | |
| r"\bSte\.?": "Masindahy", # Ste AVANT St | |
| r"\bSt\.?": "Masindahy", | |
| r"\bVve\b": "Mpitondratena", | |
| r"\bMe\b": "Maitre", | |
| } | |
| ABREV = { | |
| r"\betc\.?": "sy ny sisa", | |
| r"\bex\.?": "ohatra", | |
| r"\bcf\.?": "jereo", | |
| r"\bp\.": "pejy", | |
| r"\bn°": "laharana faha", | |
| r"\bN°": "laharana faha", | |
| r"\btél\.?": "telefaonina", | |
| } | |
| MONNAIES = { | |
| "FCFA":"farantsa CFA","MGA":"Ariary","Ar":"Ariary", | |
| "USD":"dolara amerikana","EUR":"euro", | |
| "€":"euro","$":"dolara","£":"livre sterling","¥":"yen", | |
| } | |
| UNITES = { | |
| "km":"kilometatra","cm":"santimetatra","mm":"milimetatra", | |
| "kg":"kilao","mg":"miligrama","g":"grama", | |
| "L":"litatra","ml":"mililitatra","ha":"hektara", | |
| } | |
| SYMBOLES = {"+":"miampy","=":"mitovy","×":"ampitomboina"} | |
| def _nb(x): return _compose_grand(int(x)) | |
| # ============================================================ | |
| # 3. NORMALISATION | |
| # ============================================================ | |
| def normalize_plt(text): | |
| t = text | |
| # 0) apostrophes courbes -> apostrophe droite (') EN TOUT PREMIER. Bug reel | |
| # constate : tafitaribhi (G2P) echoue SILENCIEUSEMENT sur une apostrophe | |
| # courbe -- le mot concerne (ex. "tamin'ny" avec ' U+2019, tres frequent : | |
| # clavier mobile, Word, copier-coller) ressort NON phonemise (texte brut | |
| # mele aux phonemes du reste de la phrase), ce que le modele n'a jamais vu | |
| # a l'entrainement -> hallucination localisee sur CE mot (allongement/saut/ | |
| # repetition). Le corpus d'entrainement (mimba/styletts2-plt-corpus) applique | |
| # exactement cette normalisation avant phonemisation (fiche du dataset). | |
| t = t.replace("’", "'").replace("‘", "'").replace("ʼ", "'") | |
| # 1) titres + abreviations EN PREMIER (nettoie les points Dr. p. etc.) | |
| for table in (TITRES, ABREV): | |
| for pat, repl in table.items(): | |
| t = re.sub(pat, repl, t) | |
| # 2) montants nombre+monnaie | |
| t = re.sub(r"(\d+)\s*(FCFA|MGA|Ar|USD|EUR|€|\$|£|¥)", | |
| lambda m: f"{_nb(m.group(1))} {MONNAIES.get(m.group(2), m.group(2))}", t) | |
| # 3) nombre+unite | |
| t = re.sub(r"(\d+)\s*(km|cm|mm|kg|mg|ml|L|ha|g)\b", | |
| lambda m: f"{_nb(m.group(1))} {UNITES.get(m.group(2), m.group(2))}", t) | |
| # 4) pourcentage / degres | |
| t = re.sub(r"(\d+)\s*%", lambda m: f"{_nb(m.group(1))} isan-jato", t) | |
| t = re.sub(r"(\d+)\s*°C", lambda m: f"{_nb(m.group(1))} degre Celsius", t) | |
| t = re.sub(r"(\d+)\s*°", lambda m: f"{_nb(m.group(1))} degre", t) | |
| # 5) heures 14h30 / 14h | |
| t = re.sub(r"(\d{1,2})h(\d{2})?", | |
| lambda m: f"{_nb(m.group(1))} ora" + (f" sy {_nb(m.group(2))} minitra" if m.group(2) else ""), t) | |
| # 6) symboles seuls (pas les codes monnaie deja geres -> evite 'Ariary iary') | |
| for sym, repl in [("€","euro"),("$","dolara"),("£","livre sterling"),("¥","yen")]: | |
| t = t.replace(sym, " "+repl+" ") | |
| for sym, repl in SYMBOLES.items(): | |
| t = t.replace(sym, " "+repl+" ") | |
| # 7) nombres isoles restants (SEULEMENT les nombres autonomes, bornes par | |
| # une limite de mot des deux cotes -- evite de convertir un chiffre colle | |
| # a des lettres, ex. pseudo "Zaw2" ou hashtag "#100Africanmyths", qui ne | |
| # sont pas de vrais nombres a epeler et deviendraient des mots inventes) | |
| t = re.sub(r"\b\d+\b", lambda m: _nb(m.group(0)), t) | |
| # 8) nettoyage | |
| t = re.sub(r"\s+", " ", t).strip() | |
| # 9) garde de ponctuation finale (comme _preprocess() de app.py) : sans | |
| # terminaison, le modele n'a pas de repere de fin et a tendance a repeter | |
| # la derniere syllabe. On ajoute un point si le texte ne finit pas deja | |
| # par une ponctuation terminale (le vocab BlueTTS contient . , ! ? ; :). | |
| if t and not re.search(r"[.!?;:,]$", t): | |
| t += "." | |
| return t | |
| if __name__ == "__main__": | |
| tests = [ | |
| "Dr. Rakoto dia manana 25 taona.", | |
| "Nividy 500 FCFA sy 1200 Ar aho.", | |
| "Mandeha 15 km sy mividy 2 kg vary.", | |
| "Mahazo 15% ny mpianatra, 30°C ny andro.", | |
| "Tonga tamin'ny 14h30 izy.", | |
| "St. Paul sy Ste. Marie no eto.", | |
| "Jereo p. 25 sy ohatra etc.", | |
| "2 + 3 = 5 ary 4 × 2.", | |
| ] | |
| for s in tests: | |
| print("IN :", s); print("OUT:", normalize_plt(s), "\n") | |