"""Nepali text normalization for TTS. Turns raw Nepali text into spoken-form Devanagari (plus any Latin words) for the G2P. Handles digits in both scripts, cardinals (lakh/crore), decimals, ordinals, dates (BS and AD), times, fractions, ranges, negatives, phone numbers, currency (amount spoken first), units, abbreviations, URLs/emails, and zero-width characters. This layer is where most Nepali TTS fails. """ import re import unicodedata _DEV_DIGITS = str.maketrans("०१२३४५६७८९", "0123456789") _ZERO_WIDTH = dict.fromkeys(map(ord, "​‌‍⁠"), None) _DEV = "ऀ-ॿ" _NO_LETTER_BEFORE = rf"(? str: """Integer -> Nepali words using the Indian (lakh/crore) system.""" if n < 0: return "ऋणात्मक " + number_to_words(-n) if n < 100: return SMALL[n] if n < 1000: h, r = divmod(n, 100) return SMALL[h] + " सय" + (" " + number_to_words(r) if r else "") for value, name in _SCALES: if n >= value: q, r = divmod(n, value) return number_to_words(q) + " " + name + (" " + number_to_words(r) if r else "") return SMALL[0] def _decimal_to_words(token: str) -> str: """'3.05' -> 'तीन दशमलव शून्य पाँच'; '.5' -> 'शून्य दशमलव पाँच'.""" whole, _, frac = token.partition(".") words = number_to_words(int(whole)) if whole else SMALL[0] if frac: words += " दशमलव " + " ".join(SMALL[int(d)] for d in frac) return words _ORD_IRREGULAR = {1: "पहिलो", 2: "दोस्रो", 3: "तेस्रो", 4: "चौथो", 5: "पाँचौँ", 6: "छैटौँ", 7: "सातौँ", 8: "आठौँ", 9: "नवौँ", 10: "दशौँ"} def ordinal_to_words(n: int) -> str: if n in _ORD_IRREGULAR: return _ORD_IRREGULAR[n] w = number_to_words(n) return w + ("ौँ" if "क" <= w[-1] <= "ह" else "औँ") _MONTHS_BS = [None, "बैशाख", "जेठ", "असार", "साउन", "भदौ", "असोज", "कात्तिक", "मंसिर", "पुष", "माघ", "फागुन", "चैत"] _MONTHS_AD = [None, "जनवरी", "फेब्रुअरी", "मार्च", "अप्रिल", "मे", "जुन", "जुलाई", "अगस्ट", "सेप्टेम्बर", "अक्टोबर", "नोभेम्बर", "डिसेम्बर"] _ABBREV = { "वि.सं.": "विक्रम संवत्", "ई.सं.": "ईस्वी संवत्", "ई.पू.": "ईसा पूर्व", "कि.मि.": "किलोमिटर", "कि.ग्रा.": "किलोग्राम", "मि.ली.": "मिलिलिटर", "से.मि.": "सेन्टिमिटर", "डा.": "डाक्टर", "नं.": "नम्बर", "प्रा.": "प्राध्यापक", "e.g.": "उदाहरणका लागि", "i.e.": "अर्थात्", "etc.": "आदि", "Dr.": "डाक्टर", "Mr.": "मिस्टर", "Mrs.": "मिसेस", } _ABBREV_RE = re.compile(_NO_LETTER_BEFORE + "(" + "|".join(re.escape(k) for k in sorted(_ABBREV, key=len, reverse=True)) + ")") # (symbol pattern, spoken name). Amount is spoken before the currency name. _CURRENCIES = [ (r"NRs\.?|NPR|ने\.\s?रू\.?|रू\.?|रु\.?|Rs\.?|RS\.?", "रुपैयाँ"), (r"₹|INR|भा\.\s?रू\.?", "भारतीय रुपैयाँ"), (r"US\$|USD|\$", "डलर"), (r"€|EUR", "युरो"), (r"£|GBP", "पाउन्ड"), ] _CURRENCY_AMOUNT = [(re.compile(_NO_LETTER_BEFORE + f"(?:{p})" + r"\s*(\d+(?:\.\d+)?)"), name) for p, name in _CURRENCIES] _CURRENCY_BARE = [(re.compile(rf"{_NO_LETTER_BEFORE}(?:रू|रु)\.?{_NO_DEV_AFTER}"), "रुपैयाँ"), (re.compile(r"(?= 2050: # Bikram Sambat (current BS year is ~2083) return f" {y} साल {_MONTHS_BS[m]} {d} गते " return f" सन् {y} {_MONTHS_AD[m]} {d} " def _time(m): h, mi = int(m.group(1)), int(m.group(2)) if h > 24 or mi > 59: return m.group(0) return f" {h} बजे " if mi == 0 else f" {h} बजेर {mi} मिनेट " def _fraction(m): a, b = int(m.group(1)), int(m.group(2)) if b == 0: return m.group(0) return " " + _FRACTIONS.get((a, b), f"{a} बटा {b}") + " " def _replace_numbers(text: str) -> str: def repl(m): tok = m.group(0) return " " + (_decimal_to_words(tok) if "." in tok else number_to_words(int(tok))) + " " return re.sub(r"\d+(?:\.\d+)?|(? str: """Normalize raw Nepali text into spoken form for the G2P. Idempotent.""" text = unicodedata.normalize("NFC", text).translate(_ZERO_WIDTH).translate(_DEV_DIGITS) text = _ABBREV_RE.sub(lambda m: f" {_ABBREV[m.group(1)]} ", text) text = re.sub(r"(? 125000 text = re.sub(r"(? वाइफाइ keeps its व if nepal: from .pron import nepal_accent text = nepal_accent(text) # English words -> Devanagari as Nepali speakers say them (the TTS saw almost no Latin script) if english: from .english import english_to_devanagari text = english_to_devanagari(text) return re.sub(r"\s+", " ", text).strip() # Latin-script runs are English code-switch. _LATIN_RUN = re.compile(r"[A-Za-z][A-Za-z'’\-]*") def split_codeswitch(text: str): """Yield (segment, is_latin) tuples splitting Nepali vs embedded English.""" idx = 0 for m in _LATIN_RUN.finditer(text): if m.start() > idx: yield text[idx:m.start()], False yield m.group(0), True idx = m.end() if idx < len(text): yield text[idx:], False