UncleanCode's picture
Dynamic Accent Flash: add Flash-DPO (CosyVoice3) as --version flash (default), diacritic front-end, Flash results, samples
e4ae2bb verified
Raw History Blame Contribute Delete
14.6 kB
"""Dynamic Accent Flash text normaliser: numbers, ordinals-as-cardinals, years, dates, times, currency and percent
-> words, per language (eng, pcm, ibo, hau, yor). Pure Python, no dependencies.
from normalize import normalize
normalize("Ọ bụ ₦5,000 n'ụbọchị 21/10/2026", "ibo")
Coverage: integers 0 .. 999,999,999 (larger stay as digits), decimals (point-by-digit), ₦/N/NGN/naira, $/USD,
£, kobo, %, dd/mm/yyyy and "21 October 2026" dates, hh:mm times, 1st/2nd... in eng/pcm.
The Igbo and Hausa systems are decimal and regular. Yorùbá uses the traditional vigesimal/subtractive system for
1-99 (with tone marks) and decimal composition above 100 (ọgọ́rùn-ún, ẹgbẹ̀rún, mílíọ̀nù + "ó lé"); the Yorùbá forms and
all date/currency phrasings need a native-speaker check before release (see TEXT_FRONTEND.md).
"""
import re
import unicodedata
# ------------------------------------------------------------------ English / Pidgin
_E1 = "zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen".split()
_E10 = "_ _ twenty thirty forty fifty sixty seventy eighty ninety".split()
def eng(n):
if n < 20:
return _E1[n]
if n < 100:
return _E10[n // 10] + ("" if n % 10 == 0 else "-" + _E1[n % 10])
if n < 1000:
return _E1[n // 100] + " hundred" + ("" if n % 100 == 0 else " and " + eng(n % 100))
for v, w in ((10 ** 9, "billion"), (10 ** 6, "million"), (1000, "thousand")):
if n >= v:
r = n % v
tail = "" if r == 0 else ((" and " if r < 100 else " ") + eng(r))
return eng(n // v) + " " + w + tail
return str(n)
def eng_year(n):
if 2000 <= n <= 2009:
return eng(n)
if 1100 <= n <= 2099 and n % 100 != 0:
return eng(n // 100) + " " + (("oh " + _E1[n % 100]) if n % 100 < 10 else eng(n % 100))
return eng(n)
_ORD = {"one": "first", "two": "second", "three": "third", "five": "fifth", "eight": "eighth", "nine": "ninth", "twelve": "twelfth"}
def eng_ord(n):
w = eng(n)
head, _, last = w.rpartition(" ") if " " in w else ("", "", w)
h2, dash, l2 = last.rpartition("-")
base = l2 if dash else last
o = _ORD.get(base) or (base[:-1] + "ieth" if base.endswith("y") else base + "th")
last = (h2 + "-" + o) if dash else o
return (head + " " + last).strip()
# ------------------------------------------------------------------ Igbo (decimal)
_I1 = ["efu", "otu", "abụọ", "atọ", "anọ", "ise", "isii", "asaa", "asatọ", "itoolu"]
def ibo(n):
if n < 10:
return _I1[n]
if n < 100:
t, u = divmod(n, 10)
s = "iri" if t == 1 else "iri " + _I1[t]
return s if u == 0 else s + " na " + _I1[u]
for v, w in ((10 ** 9, "ijeri"), (10 ** 6, "nde"), (1000, "puku"), (100, "narị")):
if n >= v:
q, r = divmod(n, v)
s = w if q == 1 else w + " " + ibo(q)
return s if r == 0 else s + " na " + ibo(r)
return str(n)
# ------------------------------------------------------------------ Hausa (decimal)
_H1 = ["sifili", "ɗaya", "biyu", "uku", "huɗu", "biyar", "shida", "bakwai", "takwas", "tara"]
_H10 = [None, "goma", "ashirin", "talatin", "arba'in", "hamsin", "sittin", "saba'in", "tamanin", "casa'in"]
def hau(n):
if n < 10:
return _H1[n]
if n < 20:
return "goma" if n == 10 else "goma sha " + _H1[n - 10]
if n < 100:
t, u = divmod(n, 10)
return _H10[t] if u == 0 else _H10[t] + " da " + _H1[u]
for v, w in ((10 ** 9, "biliyan"), (10 ** 6, "miliyan"), (1000, "dubu"), (100, "ɗari")):
if n >= v:
q, r = divmod(n, v)
s = w if q == 1 else w + " " + hau(q)
return s if r == 0 else s + " da " + hau(r)
return str(n)
# ------------------------------------------------------------------ Yorùbá
# counting forms 1-10, then vigesimal/subtractive 11-99 (standard grammar tables); decimal composition >= 100
_Y1 = ["òdo", "oókan", "eéjì", "ẹẹ́ta", "ẹẹ́rin", "aárùn-ún", "ẹẹ́fà", "eéje", "ẹẹ́jọ", "ẹẹ́sàn-án", "ẹẹ́wàá"]
_Y11 = {11: "ọ̀kanlá", 12: "èjìlá", 13: "ẹ̀tàlá", 14: "ẹ̀rìnlá"}
# tens: name, locative form used after lé/dín ("added to / less than")
_YT = {20: ("ogún", "lógún"), 30: ("ọgbọ̀n", "lọ́gbọ̀n"), 40: ("ogójì", "lógójì"), 50: ("àádọ́ta", "láàádọ́ta"),
60: ("ọgọ́ta", "lọ́gọ́ta"), 70: ("àádọ́rin", "láàádọ́rin"), 80: ("ọgọ́rin", "lọ́gọ́rin"),
90: ("àádọ́rùn-ún", "láàádọ́rùn-ún"), 100: ("ọgọ́rùn-ún", "lọ́gọ́rùn-ún")}
_YLE = {1: "ọ̀kàn", 2: "éjì", 3: "ẹ́tà", 4: "ẹ́rìn"} # +1..+4
_YDIN = {1: "ọ̀kàn", 2: "éjì", 3: "ẹẹ́tà", 4: "ẹẹ́rìn"} # -1..-4
_YFIVE = {20: "ẹ̀ẹ́dógún", 30: "ẹ̀ẹ́dọ́gbọ̀n", 40: "ẹ̀ẹ́dógójì", 50: "ẹ̀ẹ́dáàádọ́ta", 60: "ẹ̀ẹ́dọ́gọ́ta", 70: "ẹ̀ẹ́dáàádọ́rin",
80: "ẹ̀ẹ́dọ́gọ́rin", 90: "ẹ̀ẹ́dáàádọ́rùn-ún", 100: "ẹ̀ẹ́dọ́gọ́rùn-ún"} # (T - 5)
_YADJ = {1: "kan", 2: "méjì", 3: "mẹ́ta", 4: "mẹ́rin", 5: "márùn-ún", 6: "mẹ́fà", 7: "méje", 8: "mẹ́jọ", 9: "mẹ́sàn-án", 10: "mẹ́wàá"}
_YH = {200: "igba", 300: "ọ̀ọ́dúnrún", 400: "irinwó", 500: "ẹ̀ẹ́dẹ́gbẹ̀ta", 600: "ẹgbẹ̀ta", 700: "ẹ̀ẹ́dẹ́gbẹ̀rin",
800: "ẹgbẹ̀rin", 900: "ẹ̀ẹ́dẹ́gbẹ̀rún"}
def _y99(n):
if n <= 10:
return _Y1[n]
if n in _Y11:
return _Y11[n]
if n in _YT:
return _YT[n][0]
T = (n // 10) * 10
u = n - T
if u <= 4: # T + u (u-lé-T)
return _YLE[u] + "lé" + _YT[T][1]
if u == 5: # (T+10) - 5
return _YFIVE[T + 10]
return _YDIN[10 - u] + "dín" + _YT[T + 10][1] # (T+10) - (10-u)
def yor(n):
if n < 100:
return _y99(n)
if n < 1000:
h, r = divmod(n, 100)
s = _YT[100][0] if h == 1 else _YH[h * 100]
return s if r == 0 else s + " ó lé " + _y99(r)
for v, w in ((10 ** 9, "bílíọ̀nù"), (10 ** 6, "mílíọ̀nù"), (1000, "ẹgbẹ̀rún")):
if n >= v:
q, r = divmod(n, v)
s = w + " " + (_YADJ[q] if q in _YADJ else yor(q))
return s if r == 0 else s + " ó lé " + yor(r)
return str(n)
NUM = {"eng": eng, "pcm": eng, "ibo": ibo, "hau": hau, "yor": yor}
MONTHS = {
"eng": "January February March April May June July August September October November December".split(),
"ibo": "Jenụwarị Febụwarị Maachị Epreel Mee Juun Julaị Ọgọọst Septemba Ọktoba Novemba Disemba".split(),
"hau": "Janairu Fabrairu Maris Afrilu Mayu Yuni Yuli Agusta Satumba Oktoba Nuwamba Disamba".split(),
"yor": "Ṣẹ́rẹ́ Èrèlé Ẹrẹ̀nà Ìgbé Ẹ̀bìbí Òkúdu Agẹmọ Ògún Owewe Ọ̀wàrà Bélú Ọ̀pẹ̀".split(),
}
MONTHS["pcm"] = MONTHS["eng"]
_MIDX = {m.lower(): i for i, m in enumerate(MONTHS["eng"])}
_MIDX.update({m[:3].lower(): i for i, m in enumerate(MONTHS["eng"])})
CUR = { # (major, minor) per language; phrase order handled in money()
"NGN": {"eng": ("naira", "kobo"), "pcm": ("naira", "kobo"), "ibo": ("naịra", "kobo"), "hau": ("naira", "kobo"), "yor": ("náírà", "kọ́bọ̀")},
"USD": {"eng": ("dollars", "cents"), "pcm": ("dollars", "cents"), "ibo": ("dọla", "senti"), "hau": ("dala", "santi"), "yor": ("dọ́là", "sẹ́ǹtì")},
"GBP": {"eng": ("pounds", "pence"), "pcm": ("pounds", "pence"), "ibo": ("paụnd", "pens"), "hau": ("fam", "fens"), "yor": ("pọ́n-ùn", "pẹ́ńsì")},
}
def num(n, lang):
return NUM[lang](n) if n < 10 ** 9 else str(n)
def money(amount, code, lang):
whole, _, frac = amount.partition(".")
whole = int(whole.replace(",", "") or 0)
major, minor = CUR[code][lang]
if lang in ("eng", "pcm"):
s = f"{num(whole, lang)} {major}"
if code == "USD" and whole == 1:
s = "one dollar"
if frac and int(frac[:2].ljust(2, "0")):
s += f" {num(int(frac[:2].ljust(2, '0')), lang)} {minor}"
return s
s = f"{major} {num(whole, lang)}" # ibo/hau/yor: currency word first ("naịra puku ise")
if lang == "yor":
s = f"{major} {yor_adj(whole)}"
if frac and int(frac[:2].ljust(2, "0")):
k = int(frac[:2].ljust(2, "0"))
s += {"ibo": " na ", "hau": " da ", "yor": " ó lé "}[lang] + f"{minor} {yor_adj(k) if lang == 'yor' else num(k, lang)}"
return s
def yor_adj(n):
"""Yorùbá number after a noun (adjectival form for 1-10, counting form otherwise)."""
return _YADJ.get(n) or yor(n)
def date(d, m, y, lang):
M = MONTHS[lang][m - 1]
if lang in ("eng", "pcm"):
return f"the {eng_ord(d)} of {M}" + (f", {eng_year(y)}" if y else "")
if lang == "ibo":
return f"ụbọchị {ibo(d)} n'ọnwa {M}" + (f", afọ {ibo(y)}" if y else "")
if lang == "hau":
return f"ranar {hau(d)} ga watan {M}" + (f", shekarar {hau(y)}" if y else "")
return f"ọjọ́ {yor(d)} oṣù {M}" + (f", ọdún {yor(y)}" if y else "")
def clock(h, mi, lang):
if lang in ("eng", "pcm"):
return eng(h) + ("" if mi == 0 else (" oh " + eng(mi) if mi < 10 else " " + eng(mi))) + (" o'clock" if mi == 0 else "")
if lang == "ibo":
return f"elekere {ibo(h)}" + ("" if mi == 0 else f" na nkeji {ibo(mi)}")
if lang == "hau":
return f"ƙarfe {hau(h)}" + ("" if mi == 0 else f" da minti {hau(mi)}")
return f"agogo {yor_adj(h)}" + ("" if mi == 0 else f" ó lé ìṣẹ́jú {yor_adj(mi)}")
def percent(x, lang):
w = decimal(x, lang)
return {"eng": f"{w} percent", "pcm": f"{w} percent", "ibo": f"pasentị {w}", "hau": f"kashi {w} cikin ɗari",
"yor": f"ìdá {w} nínú ọgọ́rùn-ún"}[lang]
_POINT = {"eng": "point", "pcm": "point", "ibo": "ntụpọ", "hau": "digo", "yor": "ààmì"}
def decimal(x, lang):
a, _, b = x.replace(",", "").partition(".")
s = num(int(a or 0), lang)
if b:
s += f" {_POINT[lang]} " + " ".join(num(int(c), lang) for c in b)
return s
_AMT = r"\d{1,3}(?:,\d{3})+(?:\.\d+)?|\d+(?:\.\d+)?"
_RE_MONEY = re.compile(r"(?:(₦|NGN\s?|N(?=\d)|\$|USD\s?|£)\s?(" + _AMT + r")(\s?(?:k|m|bn)\b)?)|(?:(" + _AMT + r")\s?(naira|kobo|dollars?)\b)", re.I)
_RE_DATE = re.compile(r"\b(\d{1,2})[/.-](\d{1,2})[/.-](\d{4})\b")
_RE_DATE2 = re.compile(r"\b(\d{1,2})(?:st|nd|rd|th)?\s+(January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)\.?,?\s*(\d{4})?\b", re.I)
_RE_TIME = re.compile(r"\b(\d{1,2}):(\d{2})(?:\s?(am|pm)\b)?", re.I)
_RE_TIME2 = re.compile(r"\b(\d{1,2})\s?(am|pm)\b", re.I)
_RE_PCT = re.compile(r"(" + _AMT + r")\s?%")
_RE_ORD = re.compile(r"\b(\d+)(st|nd|rd|th)\b", re.I)
_RE_YEAR = re.compile(r"\b(1[5-9]\d\d|20\d\d)\b")
_RE_NUM = re.compile(_AMT)
_DUP = re.compile(r"\b(n'elekere|elekere|karfe|ƙarfe|ranar|agogo|ọjọ́|ụbọchị)\s+(elekere|ƙarfe|ranar|agogo|ọjọ́|ụbọchị)\b", re.I)
_SCALE = {"k": 1000, "m": 10 ** 6, "bn": 10 ** 9}
def normalize(text, lang):
"""Verbalise numbers etc. for one language segment (no <lang> tags inside)."""
lang = lang if lang in NUM else "eng"
t = unicodedata.normalize("NFC", text)
def _money(m):
if m.group(1):
sym, amt, sc = m.group(1).strip().upper(), m.group(2), (m.group(3) or "").strip().lower()
code = {"₦": "NGN", "N": "NGN", "NGN": "NGN", "$": "USD", "USD": "USD", "£": "GBP"}[sym]
else:
amt, word, sc = m.group(4), m.group(5).lower(), ""
if word == "kobo":
k = int(amt.replace(",", ""))
return f"{num(k, lang)} kobo" if lang in ("eng", "pcm") else f"{CUR['NGN'][lang][1]} {yor_adj(k) if lang == 'yor' else num(k, lang)}"
code = "NGN" if word == "naira" else "USD"
if sc:
amt = str(int(round(float(amt.replace(",", "")) * _SCALE[sc])))
return money(amt, code, lang)
t = _RE_MONEY.sub(_money, t)
t = _RE_PCT.sub(lambda m: percent(m.group(1), lang), t)
t = _RE_DATE.sub(lambda m: date(int(m.group(1)), int(m.group(2)), int(m.group(3)), lang)
if 1 <= int(m.group(1)) <= 31 and 1 <= int(m.group(2)) <= 12 else m.group(0), t)
t = _RE_DATE2.sub(lambda m: date(int(m.group(1)), _MIDX[m.group(2).lower()[:3]] + 1,
int(m.group(3)) if m.group(3) else None, lang), t)
def _time(m):
h, mi, ap = int(m.group(1)), int(m.group(2)), (m.group(3) or "").lower()
s = clock(h, mi, lang)
if ap and lang in ("eng", "pcm"):
s += " " + {"am": "a m", "pm": "p m"}[ap]
return s
t = _RE_TIME.sub(_time, t)
t = _RE_TIME2.sub(lambda m: (eng(int(m.group(1))) + " " + {"am": "a m", "pm": "p m"}[m.group(2).lower()])
if lang in ("eng", "pcm") else clock(int(m.group(1)), 0, lang), t)
if lang in ("eng", "pcm"):
t = _RE_ORD.sub(lambda m: eng_ord(int(m.group(1))), t)
t = _RE_YEAR.sub(lambda m: eng_year(int(m.group(1))), t)
else:
t = _RE_ORD.sub(lambda m: num(int(m.group(1)), lang), t)
t = _RE_NUM.sub(lambda m: decimal(m.group(0), lang) if "." in m.group(0) else num(int(m.group(0).replace(",", "")), lang), t)
t = _DUP.sub(lambda m: m.group(1) if m.group(1).lower().endswith(m.group(2).lower()) or m.group(1).lower() == "karfe" else m.group(0), t)
return re.sub(r"\s+", " ", t).strip()
def normalize_tagged(text, default="eng"):
"""Apply normalize() to every <lang>…</lang> segment of a tagged string (untagged text uses `default`)."""
out, pos = [], 0
for m in re.finditer(r"<(\w{3})>(.*?)</\1>", text, re.S):
if m.start() > pos:
out.append(normalize(text[pos:m.start()], default) if text[pos:m.start()].strip() else text[pos:m.start()])
out.append(f"<{m.group(1)}>{normalize(m.group(2), m.group(1))}</{m.group(1)}>")
pos = m.end()
if pos < len(text):
rest = text[pos:]
out.append(normalize(rest, default) if rest.strip() else rest)
return "".join(out)
if __name__ == "__main__":
import sys
for L in ["eng", "pcm", "ibo", "hau", "yor"]:
print(L, "|", normalize("₦5,000 on 21/10/2026 at 7:30, 45% of 1,235 people, 3.5 km", L))