Dynamic Accent Flash: add Flash-DPO (CosyVoice3) as --version flash (default), diacritic front-end, Flash results, samples
e4ae2bb verified Download frontend/normalize.py from Modularcomputing/dynamic-accent-preview: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/Modularcomputing/dynamic-accent-preview/resolve/main/frontend/normalize.py
- Command line
-
hf download hf://Modularcomputing/dynamic-accent-preview/frontend/normalize.py
-
curl -L -o normalize.py https://huggingface.co/Modularcomputing/dynamic-accent-preview/resolve/main/frontend/normalize.py
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)) | |