Text-to-Speech
Safetensors
Chatterbox
Uzbek
chatterbox-tts
uzbek
tts
lora
voice-cloning
turkic-languages
low-resource
Instructions to use UAzimov/Uzbek-tts-chatterbox with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Chatterbox
How to use UAzimov/Uzbek-tts-chatterbox with Chatterbox:
# pip install chatterbox-tts import torchaudio as ta from chatterbox.tts import ChatterboxTTS model = ChatterboxTTS.from_pretrained(device="cuda") text = "Ezreal and Jinx teamed up with Ahri, Yasuo, and Teemo to take down the enemy's Nexus in an epic late-game pentakill." wav = model.generate(text) ta.save("test-1.wav", wav, model.sr) # If you want to synthesize with a different voice, specify the audio prompt AUDIO_PROMPT_PATH="YOUR_FILE.wav" wav = model.generate(text, audio_prompt_path=AUDIO_PROMPT_PATH) ta.save("test-2.wav", wav, model.sr) - Notebooks
- Google Colab
- Kaggle
Download scripts/uz_normalize.py from UAzimov/Uzbek-tts-chatterbox: direct link, hf CLI and curl.
- Browser
- Download file 6.42 kB
-
https://huggingface.co/UAzimov/Uzbek-tts-chatterbox/resolve/main/scripts/uz_normalize.py
- Command line
-
hf download hf://UAzimov/Uzbek-tts-chatterbox/scripts/uz_normalize.py
-
curl -L -o uz_normalize.py https://huggingface.co/UAzimov/Uzbek-tts-chatterbox/resolve/main/scripts/uz_normalize.py
6.42 kB
| #!/usr/bin/env python3 | |
| """ | |
| Uzbek TTS text normalizer — digits/numbers to Uzbek words + apostrophe normalization. | |
| Usage: | |
| from uz_normalize import normalize_uz | |
| text = normalize_uz("1991-yil 31-avgustda soat 9 da") | |
| # -> "bir ming to'qqiz yuz to'qson birinchi yil o'ttiz birinchi avgustda soat to'qqizda" | |
| Rules: | |
| 1. All apostrophe-like chars (ʻ ʼ ‘ ’ `) -> ASCII ' | |
| 2. Years (1000-2999 followed by -yil/-yilda etc) -> cardinal reading | |
| 3. Ordinals: digit + -inchi/-anchi suffixes -> ordinal words | |
| 4. Dates: DD-DDDD month patterns handled via ordinal day + year | |
| 5. Times: soat N da/Ni -> "soat to'qqizda" | |
| 6. Cardinals: standalone numbers -> full Uzbek reading | |
| 7. Percent, decimals covered | |
| This normalizes for models trained on ASCII-apostrophe data (e.g. our | |
| Chatterbox LoRA). For okina-style data flip PREFER_OKINA=True. | |
| """ | |
| import re | |
| PREFER_OKINA = False | |
| APO = "'" if not PREFER_OKINA else "\u02bb" | |
| ONES = ["", "bir", "ikki", "uch", "to'rt", "besh", "olti", "yetti", "sakkiz", "to'qqiz"] | |
| TENS = ["", "o'n", "yigirma", "o'ttiz", "qirq", "ellik", "oltmish", "yetmish", "sakson", "to'qson"] | |
| def _two_digits(n: int) -> str: | |
| """1-99 -> uzbek words""" | |
| t, o = divmod(n, 10) | |
| parts = [] | |
| if t: parts.append(TENS[t]) | |
| if o: parts.append(ONES[o]) | |
| return " ".join(parts) if parts else "nol" | |
| def _three_digits(n: int) -> str: | |
| """1-999""" | |
| h, rest = divmod(n, 100) | |
| parts = [] | |
| if h == 1: parts.append("yuz") | |
| elif h > 1: parts.append(ONES[h] + " yuz") | |
| if rest: parts.append(_two_digits(rest)) | |
| return " ".join(parts) | |
| def number_to_uzbek(n: int) -> str: | |
| """0..999_999_999_999 -> uzbek words (long scale-ish, uzbek uses milliard/million)""" | |
| if n == 0: return "nol" | |
| parts = [] | |
| for divisor, name in [(10**9, "milliard"), (10**6, "million"), (1000, "ming")]: | |
| q, n = divmod(n, divisor) | |
| if q: | |
| if divisor == 1000 and q == 1: | |
| parts.append("bir ming") | |
| else: | |
| parts.append(number_to_uzbek(q) + " " + name) | |
| if n: parts.append(_three_digits(n)) | |
| return " ".join(parts) | |
| MONTHS = { | |
| "yanvar": 1, "fevral": 2, "mart": 3, "aprel": 4, "may": 5, "iyun": 6, | |
| "iyul": 7, "avgust": 8, "sentabr": 9, "oktabr": 10, "noyabr": 11, "dekabr": 12, | |
| } | |
| def _ordinal(word: str) -> str: | |
| """cardinal phrase -> ordinal (ayt... inchi). Uzbek: oxirgi bo'g'inga -inchi/-anchi. | |
| Vowel harmony approximation: last vowel a/I -> -anchi? actually rule: | |
| ends with k/t/p/q -> voice; simplified common forms used here.""" | |
| # Common convention: birinchi, ikkinchi, uchinchi are irregular; others = word + "-inchi" | |
| IRREG = {"bir": "birinchi", "ikki": "ikkinchi", "uch": "uchinchi", | |
| "to'rt": "to'rtinchi", "besh": "beshinchi", "olti": "oltinchi", | |
| "yetti": "yetinchi", "sakkiz": "sakkizinchi", "to'qqiz": "to'qqizinchi", | |
| "o'n": "o'ninchi"} | |
| if word in IRREG: return IRREG[word] | |
| return word + "inchi" | |
| def _expand_year(m): | |
| y = int(m.group(1)) | |
| suf = m.group(2) or "" | |
| words = number_to_uzbek(y) | |
| if suf.startswith("-yil"): | |
| # ordinal years often read as-is; keep "yil" suffix attached | |
| tail = suf[len("-yil"):] | |
| return f"{words} yil{tail}" | |
| return f"{words}{suf}" | |
| def _expand_number_token(m): | |
| num_str = m.group(0) | |
| try: | |
| return number_to_uzbek(int(num_str)) | |
| except ValueError: | |
| return num_str | |
| def normalize_numbers(text: str) -> str: | |
| # protect existing words; only touch digit runs and date/time patterns | |
| # 1. years: 4-digit number directly before yil* | |
| text = re.sub(r"\b(\d{4})(-?\s*y[a-z']*)", _expand_year, text) | |
| text = re.sub(r"(\bsoat\s+)?\b(\d{1,2}):(\d{2})\b", lambda m: (m.group(1) or "soat ") + f"{number_to_uzbek(int(m.group(2)))} {number_to_uzbek(int(m.group(3)))}", text) | |
| # 2b. "soat N da/ni/dan" (without colon) | |
| def _time(m): | |
| h = int(m.group(1)) | |
| suf = m.group(2) or "" | |
| return f"soat {number_to_uzbek(h)}{suf}" | |
| text = re.sub(r"\bsoat\s+(\d{1,2})(-?[a-z']*)?", _time, text) | |
| # 3. ordinals & day-of-date: N-avgust -> ordinal; N-inchi handled by irregular table | |
| def _ord_date(m): | |
| n = int(m.group(1)) | |
| rest = m.group(2) # e.g. "-avgustda" or "-inchi" | |
| card = number_to_uzbek(n) | |
| last = card.split()[-1] | |
| first = " ".join(card.split()[:-1]) | |
| restword = rest.lstrip("-") | |
| if restword.startswith("inchi") or restword.startswith("nchi"): | |
| # already an ordinal marker: convert number only, keep suffix | |
| ordw = _ordinal(last) | |
| out = (first + " " + ordw if first else ordw) + (restword[5:] if restword.startswith("inchi") and len(restword) > 5 else "") | |
| return out | |
| ordw = _ordinal(last) | |
| return (first + " " + ordw if first else ordw) + " " + restword | |
| text = re.sub(r"\b(\d{1,4})-([a-z\u02bb']+)\b", _ord_date, text) | |
| # 4. plain integers (incl. inside hyphenated like 31-avgust already partially handled; | |
| # handle remaining digit runs) | |
| text = re.sub(r"\b\d{1,9}\b", _expand_number_token, text) | |
| # 5. percent | |
| text = text.replace("%", " foiz") | |
| return text | |
| def normalize_apostrophes(text: str) -> str: | |
| table = str.maketrans({"ʻ": APO, "ʼ": APO, "‘": APO, "’": APO, "`": APO, | |
| "ʻ": APO}) | |
| return text.translate(table) | |
| def normalize_uz(text: str) -> str: | |
| """Full pipeline: apostrophes + numbers. Apply BEFORE sending to TTS.""" | |
| text = normalize_apostrophes(text) | |
| text = normalize_numbers(text) | |
| # collapse multiple spaces | |
| text = re.sub(r"[ \t]+", " ", text).strip() | |
| return text | |
| if __name__ == "__main__": | |
| tests = [ | |
| ("Assalomu alaykum, bugun havo juda ham chiroyli.", None), | |
| ("Oʻzbekiston Respublikasi 1991-yil 31-avgustda mustaqiligi eʼlon qildi.", | |
| "O'zbekiston Respublikasi bir ming to'qqiz yuz to'qson bir yil o'ttiz birinchi avgustda mustaqiligi e'lon qildi."), | |
| ("Ertaga soat 9 da uchrashamizmi?", None), | |
| ("50 foiz odam rozi", None), | |
| ("2-inchi gʻoya", None), | |
| ("Kompyuter oʻyinlari 100 yildan beri", None), | |
| ] | |
| for t, expected in tests: | |
| out = normalize_uz(t) | |
| status = "" | |
| if expected is not None: | |
| status = "OK" if out == expected else f"MISMATCH (want: {expected})" | |
| print(f"IN : {t}\nOUT: {out}\n {status}\n") | |