malagasy-tts / normalize_plt.py
mimba's picture
Add application file
ab7b3be
Raw
History Blame Contribute Delete
7.21 kB
# -*- 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")