# -*- 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")