Spaces:
Running on Zero
Running on Zero
Add multilingual text processors
Browse files- app.py +115 -10
- chatterbox/src/chatterbox/models/tokenizers/tokenizer.py +192 -13
- chatterbox/src/chatterbox/tts.py +3 -2
- requirements.txt +11 -0
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import random
|
| 2 |
import numpy as np
|
| 3 |
import torch
|
|
|
|
| 4 |
from chatterbox.src.chatterbox.tts import ChatterboxTTS
|
| 5 |
import gradio as gr
|
| 6 |
import spaces
|
|
@@ -9,9 +10,99 @@ MODEL = None
|
|
| 9 |
# ZeroGPU supports CUDA placement at module load time via CUDA emulation.
|
| 10 |
TARGET_DEVICE = "cuda"
|
| 11 |
|
| 12 |
-
|
| 13 |
-
"
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
LANGUAGE_CHOICES = [('en', 'English'), ('ar', 'Arabic'), ('da', 'Danish'), ('de', 'German'), ('el', 'Greek'), ('es', 'Spanish'), ('fi', 'Finnish'), ('fr', 'French'), ('he', 'Hebrew'), ('hi', 'Hindi'), ('it', 'Italian'), ('ja', 'Japanese'), ('ko', 'Korean'), ('ms', 'Malay'), ('nl', 'Dutch'), ('no', 'Norwegian'), ('pl', 'Polish'), ('pt', 'Portuguese'), ('ru', 'Russian'), ('sv', 'Swedish'), ('sw', 'Swahili'), ('tr', 'Turkish'), ('zh', 'Chinese')]
|
|
@@ -31,12 +122,17 @@ EXAMPLES = [
|
|
| 31 |
]
|
| 32 |
|
| 33 |
|
| 34 |
-
def default_audio_for_ui():
|
| 35 |
-
return
|
| 36 |
|
| 37 |
|
| 38 |
-
def default_text_for_ui():
|
| 39 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
def get_or_load_model():
|
|
@@ -78,8 +174,8 @@ def generate_tts_audio(
|
|
| 78 |
device = current_model.device
|
| 79 |
if seed_num_input != 0:
|
| 80 |
set_seed(int(seed_num_input), device)
|
| 81 |
-
chosen_prompt = audio_prompt_path_input or default_audio_for_ui()
|
| 82 |
lang = language_id_input or DEFAULT_LANGUAGE
|
|
|
|
| 83 |
print(f"Generating on {device} (lang={lang}) for text: '{text_input[:50]}...'")
|
| 84 |
generate_kwargs = {
|
| 85 |
"exaggeration": exaggeration_input,
|
|
@@ -93,6 +189,7 @@ def generate_tts_audio(
|
|
| 93 |
return (current_model.sr, wav.squeeze(0).cpu().numpy())
|
| 94 |
|
| 95 |
|
|
|
|
| 96 |
get_or_load_model()
|
| 97 |
|
| 98 |
|
|
@@ -101,13 +198,15 @@ with gr.Blocks() as demo:
|
|
| 101 |
"""
|
| 102 |
# Chatterbox Multilingual TTS V3
|
| 103 |
Chatterbox Multilingual TTS V3 (t3_mtl23ls_v3).
|
|
|
|
|
|
|
| 104 |
Powered by model [`ResembleAI/chatterbox`](https://huggingface.co/ResembleAI/chatterbox).
|
| 105 |
"""
|
| 106 |
)
|
| 107 |
with gr.Row():
|
| 108 |
with gr.Column():
|
| 109 |
text = gr.Textbox(
|
| 110 |
-
value=default_text_for_ui(),
|
| 111 |
label="Text to synthesize (max chars 300)",
|
| 112 |
max_lines=5,
|
| 113 |
)
|
|
@@ -115,13 +214,19 @@ with gr.Blocks() as demo:
|
|
| 115 |
sources=["upload", "microphone"],
|
| 116 |
type="filepath",
|
| 117 |
label="Reference Audio File (Optional)",
|
| 118 |
-
value=default_audio_for_ui(),
|
| 119 |
)
|
| 120 |
language = gr.Dropdown(
|
| 121 |
choices=[(name, code) for code, name in LANGUAGE_CHOICES],
|
| 122 |
value=DEFAULT_LANGUAGE,
|
| 123 |
label="Language",
|
| 124 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
exaggeration = gr.Slider(0.25, 2, step=.05, label="Exaggeration (Neutral = 0.5)", value=.5)
|
| 126 |
cfg_weight = gr.Slider(0.2, 1, step=.05, label="CFG/Pace", value=0.5)
|
| 127 |
with gr.Accordion("More options", open=False):
|
|
|
|
| 1 |
import random
|
| 2 |
import numpy as np
|
| 3 |
import torch
|
| 4 |
+
from chatterbox.src.chatterbox.models.tokenizers.tokenizer import initialize_russian_stresser
|
| 5 |
from chatterbox.src.chatterbox.tts import ChatterboxTTS
|
| 6 |
import gradio as gr
|
| 7 |
import spaces
|
|
|
|
| 10 |
# ZeroGPU supports CUDA placement at module load time via CUDA emulation.
|
| 11 |
TARGET_DEVICE = "cuda"
|
| 12 |
|
| 13 |
+
LANGUAGE_CONFIG = {
|
| 14 |
+
"ar": {
|
| 15 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/ar_f/ar_prompts2.flac",
|
| 16 |
+
"text": "في الشهر الماضي، وصلنا إلى معلم جديد بمليارين من المشاهدات على قناتنا على يوتيوب.",
|
| 17 |
+
},
|
| 18 |
+
"da": {
|
| 19 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/da_m1.flac",
|
| 20 |
+
"text": "Sidste måned nåede vi en ny milepæl med to milliarder visninger på vores YouTube-kanal.",
|
| 21 |
+
},
|
| 22 |
+
"de": {
|
| 23 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/de_f1.flac",
|
| 24 |
+
"text": "Letzten Monat haben wir einen neuen Meilenstein erreicht: zwei Milliarden Aufrufe auf unserem YouTube-Kanal.",
|
| 25 |
+
},
|
| 26 |
+
"el": {
|
| 27 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/el_m.flac",
|
| 28 |
+
"text": "Τον περασμένο μήνα, φτάσαμε σε ένα νέο ορόσημο με δύο δισεκατομμύρια προβολές στο κανάλι μας στο YouTube.",
|
| 29 |
+
},
|
| 30 |
+
"en": {
|
| 31 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/en_f1.flac",
|
| 32 |
+
"text": "Last month, we reached a new milestone with two billion views on our YouTube channel.",
|
| 33 |
+
},
|
| 34 |
+
"es": {
|
| 35 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/es_f1.flac",
|
| 36 |
+
"text": "El mes pasado alcanzamos un nuevo hito: dos mil millones de visualizaciones en nuestro canal de YouTube.",
|
| 37 |
+
},
|
| 38 |
+
"fi": {
|
| 39 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/fi_m.flac",
|
| 40 |
+
"text": "Viime kuussa saavutimme uuden virstanpylvään kahden miljardin katselukerran kanssa YouTube-kanavallamme.",
|
| 41 |
+
},
|
| 42 |
+
"fr": {
|
| 43 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/fr_f1.flac",
|
| 44 |
+
"text": "Le mois dernier, nous avons atteint un nouveau jalon avec deux milliards de vues sur notre chaîne YouTube.",
|
| 45 |
+
},
|
| 46 |
+
"he": {
|
| 47 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/he_m1.flac",
|
| 48 |
+
"text": "בחודש שעבר הגענו לאבן דרך חדשה עם שני מיליארד צפיות בערוץ היוטיוב שלנו.",
|
| 49 |
+
},
|
| 50 |
+
"hi": {
|
| 51 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/hi_f1.flac",
|
| 52 |
+
"text": "पिछले महीने हमने एक नया मील का पत्थर छुआ: हमारे YouTube चैनल पर दो अरब व्यूज़।",
|
| 53 |
+
},
|
| 54 |
+
"it": {
|
| 55 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/it_m1.flac",
|
| 56 |
+
"text": "Il mese scorso abbiamo raggiunto un nuovo traguardo: due miliardi di visualizzazioni sul nostro canale YouTube.",
|
| 57 |
+
},
|
| 58 |
+
"ja": {
|
| 59 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/ja/ja_prompts1.flac",
|
| 60 |
+
"text": "先月、私たちのYouTubeチャンネルで二十億回の再生回数という新たなマイルストーンに到達しました。",
|
| 61 |
+
},
|
| 62 |
+
"ko": {
|
| 63 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/ko_f.flac",
|
| 64 |
+
"text": "지난달 우리는 유튜브 채널에서 이십억 조회수라는 새로운 이정표에 도달했습니다.",
|
| 65 |
+
},
|
| 66 |
+
"ms": {
|
| 67 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/ms_f.flac",
|
| 68 |
+
"text": "Bulan lepas, kami mencapai pencapaian baru dengan dua bilion tontonan di saluran YouTube kami.",
|
| 69 |
+
},
|
| 70 |
+
"nl": {
|
| 71 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/nl_m.flac",
|
| 72 |
+
"text": "Vorige maand bereikten we een nieuwe mijlpaal met twee miljard weergaven op ons YouTube-kanaal.",
|
| 73 |
+
},
|
| 74 |
+
"no": {
|
| 75 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/no_f1.flac",
|
| 76 |
+
"text": "Forrige måned nådde vi en ny milepæl med to milliarder visninger på YouTube-kanalen vår.",
|
| 77 |
+
},
|
| 78 |
+
"pl": {
|
| 79 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/pl_m.flac",
|
| 80 |
+
"text": "W zeszłym miesiącu osiągnęliśmy nowy kamień milowy z dwoma miliardami wyświetleń na naszym kanale YouTube.",
|
| 81 |
+
},
|
| 82 |
+
"pt": {
|
| 83 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/pt_m1.flac",
|
| 84 |
+
"text": "No mês passado, alcançámos um novo marco: dois mil milhões de visualizações no nosso canal do YouTube.",
|
| 85 |
+
},
|
| 86 |
+
"ru": {
|
| 87 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/ru_m.flac",
|
| 88 |
+
"text": "В прошлом месяце мы достигли нового рубежа: два миллиарда просмотров на нашем YouTube-канале.",
|
| 89 |
+
},
|
| 90 |
+
"sv": {
|
| 91 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/sv_f.flac",
|
| 92 |
+
"text": "Förra månaden nådde vi en ny milstolpe med två miljarder visningar på vår YouTube-kanal.",
|
| 93 |
+
},
|
| 94 |
+
"sw": {
|
| 95 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/sw_m.flac",
|
| 96 |
+
"text": "Mwezi uliopita, tulifika hatua mpya ya maoni ya bilioni mbili kweny kituo chetu cha YouTube.",
|
| 97 |
+
},
|
| 98 |
+
"tr": {
|
| 99 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/tr_m.flac",
|
| 100 |
+
"text": "Geçen ay YouTube kanalımızda iki milyar görüntüleme ile yeni bir dönüm noktasına ulaştık.",
|
| 101 |
+
},
|
| 102 |
+
"zh": {
|
| 103 |
+
"audio": "https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/zh_f2.flac",
|
| 104 |
+
"text": "上个月,我们达到了一个新的里程碑。 我们的YouTube频道观看次数达到了二十亿次,这绝对令人难以置信。",
|
| 105 |
+
},
|
| 106 |
}
|
| 107 |
|
| 108 |
LANGUAGE_CHOICES = [('en', 'English'), ('ar', 'Arabic'), ('da', 'Danish'), ('de', 'German'), ('el', 'Greek'), ('es', 'Spanish'), ('fi', 'Finnish'), ('fr', 'French'), ('he', 'Hebrew'), ('hi', 'Hindi'), ('it', 'Italian'), ('ja', 'Japanese'), ('ko', 'Korean'), ('ms', 'Malay'), ('nl', 'Dutch'), ('no', 'Norwegian'), ('pl', 'Polish'), ('pt', 'Portuguese'), ('ru', 'Russian'), ('sv', 'Swedish'), ('sw', 'Swahili'), ('tr', 'Turkish'), ('zh', 'Chinese')]
|
|
|
|
| 122 |
]
|
| 123 |
|
| 124 |
|
| 125 |
+
def default_audio_for_ui(language_id: str):
|
| 126 |
+
return LANGUAGE_CONFIG.get(language_id, LANGUAGE_CONFIG[DEFAULT_LANGUAGE]).get("audio")
|
| 127 |
|
| 128 |
|
| 129 |
+
def default_text_for_ui(language_id: str):
|
| 130 |
+
return LANGUAGE_CONFIG.get(language_id, LANGUAGE_CONFIG[DEFAULT_LANGUAGE]).get("text", "")
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def on_language_change(language_id: str, current_ref_wav: str | None, current_text: str):
|
| 134 |
+
print(f"Switching UI defaults to language: {language_id}")
|
| 135 |
+
return default_audio_for_ui(language_id), default_text_for_ui(language_id)
|
| 136 |
|
| 137 |
|
| 138 |
def get_or_load_model():
|
|
|
|
| 174 |
device = current_model.device
|
| 175 |
if seed_num_input != 0:
|
| 176 |
set_seed(int(seed_num_input), device)
|
|
|
|
| 177 |
lang = language_id_input or DEFAULT_LANGUAGE
|
| 178 |
+
chosen_prompt = audio_prompt_path_input or default_audio_for_ui(lang)
|
| 179 |
print(f"Generating on {device} (lang={lang}) for text: '{text_input[:50]}...'")
|
| 180 |
generate_kwargs = {
|
| 181 |
"exaggeration": exaggeration_input,
|
|
|
|
| 189 |
return (current_model.sr, wav.squeeze(0).cpu().numpy())
|
| 190 |
|
| 191 |
|
| 192 |
+
initialize_russian_stresser()
|
| 193 |
get_or_load_model()
|
| 194 |
|
| 195 |
|
|
|
|
| 198 |
"""
|
| 199 |
# Chatterbox Multilingual TTS V3
|
| 200 |
Chatterbox Multilingual TTS V3 (t3_mtl23ls_v3).
|
| 201 |
+
UI revision: `multilingual-defaults-v1`
|
| 202 |
+
|
| 203 |
Powered by model [`ResembleAI/chatterbox`](https://huggingface.co/ResembleAI/chatterbox).
|
| 204 |
"""
|
| 205 |
)
|
| 206 |
with gr.Row():
|
| 207 |
with gr.Column():
|
| 208 |
text = gr.Textbox(
|
| 209 |
+
value=default_text_for_ui(DEFAULT_LANGUAGE),
|
| 210 |
label="Text to synthesize (max chars 300)",
|
| 211 |
max_lines=5,
|
| 212 |
)
|
|
|
|
| 214 |
sources=["upload", "microphone"],
|
| 215 |
type="filepath",
|
| 216 |
label="Reference Audio File (Optional)",
|
| 217 |
+
value=default_audio_for_ui(DEFAULT_LANGUAGE),
|
| 218 |
)
|
| 219 |
language = gr.Dropdown(
|
| 220 |
choices=[(name, code) for code, name in LANGUAGE_CHOICES],
|
| 221 |
value=DEFAULT_LANGUAGE,
|
| 222 |
label="Language",
|
| 223 |
)
|
| 224 |
+
language.change(
|
| 225 |
+
fn=on_language_change,
|
| 226 |
+
inputs=[language, ref_wav, text],
|
| 227 |
+
outputs=[ref_wav, text],
|
| 228 |
+
show_progress=False,
|
| 229 |
+
)
|
| 230 |
exaggeration = gr.Slider(0.25, 2, step=.05, label="Exaggeration (Neutral = 0.5)", value=.5)
|
| 231 |
cfg_weight = gr.Slider(0.2, 1, step=.05, label="CFG/Pace", value=0.5)
|
| 232 |
with gr.Accordion("More options", open=False):
|
chatterbox/src/chatterbox/models/tokenizers/tokenizer.py
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
|
|
|
|
|
| 1 |
import logging
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
import torch
|
|
|
|
| 4 |
from tokenizers import Tokenizer
|
| 5 |
|
| 6 |
|
|
@@ -13,9 +21,132 @@ SPECIAL_TOKENS = [SOT, EOT, UNK, SPACE, "[PAD]", "[SEP]", "[CLS]", "[MASK]"]
|
|
| 13 |
|
| 14 |
logger = logging.getLogger(__name__)
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
class MTLTokenizer:
|
| 17 |
def __init__(self, vocab_file_path):
|
| 18 |
self.tokenizer: Tokenizer = Tokenizer.from_file(vocab_file_path)
|
|
|
|
| 19 |
self.check_vocabset_sot_eot()
|
| 20 |
|
| 21 |
def check_vocabset_sot_eot(self):
|
|
@@ -23,24 +154,70 @@ class MTLTokenizer:
|
|
| 23 |
assert SOT in voc
|
| 24 |
assert EOT in voc
|
| 25 |
|
| 26 |
-
def
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
text_tokens = torch.IntTensor(text_tokens).unsqueeze(0)
|
| 29 |
return text_tokens
|
| 30 |
|
| 31 |
-
def encode(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
"""
|
| 33 |
-
|
| 34 |
-
|
| 35 |
The multilingual model expects language tokens like [en], [fr] to be prepended
|
| 36 |
to condition the synthesis for the appropriate language.
|
| 37 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
# Prepend language token if provided
|
| 39 |
if language_id:
|
| 40 |
lang_token = f"[{language_id.lower()}]"
|
| 41 |
txt = lang_token + txt
|
| 42 |
-
|
| 43 |
-
txt = txt.replace(
|
| 44 |
code = self.tokenizer.encode(txt)
|
| 45 |
ids = code.ids
|
| 46 |
return ids
|
|
@@ -49,10 +226,12 @@ class MTLTokenizer:
|
|
| 49 |
if isinstance(seq, torch.Tensor):
|
| 50 |
seq = seq.cpu().numpy()
|
| 51 |
|
| 52 |
-
txt: str = self.tokenizer.decode(
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
txt = txt.replace(
|
| 57 |
-
txt = txt.replace(
|
|
|
|
|
|
|
| 58 |
return txt
|
|
|
|
| 1 |
+
import importlib
|
| 2 |
+
import json
|
| 3 |
import logging
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import subprocess
|
| 6 |
+
import sys
|
| 7 |
+
import threading
|
| 8 |
+
from unicodedata import category, normalize
|
| 9 |
|
| 10 |
import torch
|
| 11 |
+
from huggingface_hub import hf_hub_download
|
| 12 |
from tokenizers import Tokenizer
|
| 13 |
|
| 14 |
|
|
|
|
| 21 |
|
| 22 |
logger = logging.getLogger(__name__)
|
| 23 |
|
| 24 |
+
CANGJIE_FILENAME = "Cangjie5_TC.json"
|
| 25 |
+
CANGJIE_REPO_ID = "ResembleAI/chatterbox"
|
| 26 |
+
RUSSIAN_STRESSER_PACKAGE = (
|
| 27 |
+
"git+https://github.com/Vuizur/add-stress-to-epub.git"
|
| 28 |
+
"@9e064c815373ef6f0360dd97b0d0fc37b52d256f"
|
| 29 |
+
)
|
| 30 |
+
RUSSIAN_STRESSER_INSTALL_DIR = Path.home() / ".cache" / "chatterbox" / "russian-text-stresser"
|
| 31 |
+
_russian_stresser_lock = threading.Lock()
|
| 32 |
+
_russian_stresser_local = threading.local()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class ChineseCangjieConverter:
|
| 36 |
+
"""Convert Chinese characters to Cangjie tokens used by the model."""
|
| 37 |
+
|
| 38 |
+
def __init__(self, model_dir: Path):
|
| 39 |
+
self.word2cj = {}
|
| 40 |
+
self.cj2word = {}
|
| 41 |
+
self.segmenter = None
|
| 42 |
+
self._load_cangjie_mapping(model_dir)
|
| 43 |
+
self._init_segmenter()
|
| 44 |
+
|
| 45 |
+
def _load_cangjie_mapping(self, model_dir: Path) -> None:
|
| 46 |
+
cangjie_path = model_dir / CANGJIE_FILENAME
|
| 47 |
+
if not cangjie_path.exists():
|
| 48 |
+
cangjie_path = Path(
|
| 49 |
+
hf_hub_download(
|
| 50 |
+
repo_id=CANGJIE_REPO_ID,
|
| 51 |
+
filename=CANGJIE_FILENAME,
|
| 52 |
+
)
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
with cangjie_path.open("r", encoding="utf-8") as fp:
|
| 56 |
+
for entry in json.load(fp):
|
| 57 |
+
word, code = entry.split("\t")[:2]
|
| 58 |
+
self.word2cj[word] = code
|
| 59 |
+
self.cj2word.setdefault(code, []).append(word)
|
| 60 |
+
|
| 61 |
+
def _init_segmenter(self) -> None:
|
| 62 |
+
from spacy_pkuseg import pkuseg
|
| 63 |
+
|
| 64 |
+
self.segmenter = pkuseg()
|
| 65 |
+
|
| 66 |
+
def _cangjie_encode(self, glyph: str) -> str | None:
|
| 67 |
+
code = self.word2cj.get(glyph)
|
| 68 |
+
if code is None:
|
| 69 |
+
return None
|
| 70 |
+
index = self.cj2word[code].index(glyph)
|
| 71 |
+
return code + (str(index) if index > 0 else "")
|
| 72 |
+
|
| 73 |
+
def __call__(self, text: str) -> str:
|
| 74 |
+
full_text = " ".join(self.segmenter.cut(text))
|
| 75 |
+
output = []
|
| 76 |
+
for glyph in full_text:
|
| 77 |
+
if category(glyph) != "Lo":
|
| 78 |
+
output.append(glyph)
|
| 79 |
+
continue
|
| 80 |
+
|
| 81 |
+
cangjie = self._cangjie_encode(glyph)
|
| 82 |
+
if cangjie is None:
|
| 83 |
+
output.append(glyph)
|
| 84 |
+
continue
|
| 85 |
+
|
| 86 |
+
output.extend(f"[cj_{code}]" for code in cangjie)
|
| 87 |
+
output.append("[cj_.]")
|
| 88 |
+
return "".join(output)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _load_russian_stresser_class():
|
| 92 |
+
install_dir = str(RUSSIAN_STRESSER_INSTALL_DIR)
|
| 93 |
+
if RUSSIAN_STRESSER_INSTALL_DIR.exists() and install_dir not in sys.path:
|
| 94 |
+
sys.path.insert(0, install_dir)
|
| 95 |
+
|
| 96 |
+
try:
|
| 97 |
+
from russian_text_stresser.text_stresser import RussianTextStresser
|
| 98 |
+
return RussianTextStresser
|
| 99 |
+
except ImportError:
|
| 100 |
+
logger.info("Installing russian_text_stresser for the first Russian request")
|
| 101 |
+
RUSSIAN_STRESSER_INSTALL_DIR.mkdir(parents=True, exist_ok=True)
|
| 102 |
+
subprocess.check_call([
|
| 103 |
+
sys.executable,
|
| 104 |
+
"-m",
|
| 105 |
+
"pip",
|
| 106 |
+
"install",
|
| 107 |
+
"--no-cache-dir",
|
| 108 |
+
"--no-deps",
|
| 109 |
+
"--target",
|
| 110 |
+
install_dir,
|
| 111 |
+
RUSSIAN_STRESSER_PACKAGE,
|
| 112 |
+
])
|
| 113 |
+
if install_dir not in sys.path:
|
| 114 |
+
sys.path.insert(0, install_dir)
|
| 115 |
+
importlib.invalidate_caches()
|
| 116 |
+
from russian_text_stresser.text_stresser import RussianTextStresser
|
| 117 |
+
return RussianTextStresser
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def add_russian_stress(text: str) -> str:
|
| 121 |
+
"""Add stress marks to Russian text."""
|
| 122 |
+
try:
|
| 123 |
+
stresser = getattr(_russian_stresser_local, "instance", None)
|
| 124 |
+
if stresser is None:
|
| 125 |
+
with _russian_stresser_lock:
|
| 126 |
+
RussianTextStresser = _load_russian_stresser_class()
|
| 127 |
+
stresser = RussianTextStresser()
|
| 128 |
+
_russian_stresser_local.instance = stresser
|
| 129 |
+
|
| 130 |
+
return stresser.stress_text(text)
|
| 131 |
+
except Exception as exc:
|
| 132 |
+
raise RuntimeError("Russian stress labeling failed") from exc
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def initialize_russian_stresser() -> None:
|
| 136 |
+
"""Install, initialize, and verify Russian stress labeling."""
|
| 137 |
+
health_check_text = normalize("NFKD", "твои слова ничего не значат.")
|
| 138 |
+
stressed_text = add_russian_stress(health_check_text)
|
| 139 |
+
if "\u0301" not in stressed_text:
|
| 140 |
+
raise RuntimeError(
|
| 141 |
+
"Russian stresser health check failed: no stress marks were produced"
|
| 142 |
+
)
|
| 143 |
+
logger.info("Russian stresser health check passed")
|
| 144 |
+
|
| 145 |
+
|
| 146 |
class MTLTokenizer:
|
| 147 |
def __init__(self, vocab_file_path):
|
| 148 |
self.tokenizer: Tokenizer = Tokenizer.from_file(vocab_file_path)
|
| 149 |
+
self.cangjie_converter = ChineseCangjieConverter(Path(vocab_file_path).parent)
|
| 150 |
self.check_vocabset_sot_eot()
|
| 151 |
|
| 152 |
def check_vocabset_sot_eot(self):
|
|
|
|
| 154 |
assert SOT in voc
|
| 155 |
assert EOT in voc
|
| 156 |
|
| 157 |
+
def preprocess_text(
|
| 158 |
+
self,
|
| 159 |
+
raw_text: str,
|
| 160 |
+
language_id: str = None,
|
| 161 |
+
lowercase: bool = True,
|
| 162 |
+
nfkd_normalize: bool = True,
|
| 163 |
+
):
|
| 164 |
+
"""Apply the multilingual model's shared text preprocessing."""
|
| 165 |
+
preprocessed_text = raw_text
|
| 166 |
+
if language_id == "zh":
|
| 167 |
+
return preprocessed_text
|
| 168 |
+
if lowercase:
|
| 169 |
+
preprocessed_text = preprocessed_text.lower()
|
| 170 |
+
if nfkd_normalize:
|
| 171 |
+
preprocessed_text = normalize("NFKD", preprocessed_text)
|
| 172 |
+
return preprocessed_text
|
| 173 |
+
|
| 174 |
+
def text_to_tokens(
|
| 175 |
+
self,
|
| 176 |
+
text: str,
|
| 177 |
+
language_id: str = None,
|
| 178 |
+
lowercase: bool = True,
|
| 179 |
+
nfkd_normalize: bool = True,
|
| 180 |
+
):
|
| 181 |
+
text_tokens = self.encode(
|
| 182 |
+
text,
|
| 183 |
+
language_id=language_id,
|
| 184 |
+
lowercase=lowercase,
|
| 185 |
+
nfkd_normalize=nfkd_normalize,
|
| 186 |
+
)
|
| 187 |
text_tokens = torch.IntTensor(text_tokens).unsqueeze(0)
|
| 188 |
return text_tokens
|
| 189 |
|
| 190 |
+
def encode(
|
| 191 |
+
self,
|
| 192 |
+
txt: str,
|
| 193 |
+
language_id: str = None,
|
| 194 |
+
lowercase: bool = True,
|
| 195 |
+
nfkd_normalize: bool = True,
|
| 196 |
+
):
|
| 197 |
"""
|
| 198 |
+
preprocess text > language-specific processing > prepend lang_id > encode
|
| 199 |
+
|
| 200 |
The multilingual model expects language tokens like [en], [fr] to be prepended
|
| 201 |
to condition the synthesis for the appropriate language.
|
| 202 |
"""
|
| 203 |
+
txt = self.preprocess_text(
|
| 204 |
+
txt,
|
| 205 |
+
language_id=language_id,
|
| 206 |
+
lowercase=lowercase,
|
| 207 |
+
nfkd_normalize=nfkd_normalize,
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
if language_id == "zh":
|
| 211 |
+
txt = self.cangjie_converter(txt)
|
| 212 |
+
elif language_id == "ru":
|
| 213 |
+
txt = add_russian_stress(txt)
|
| 214 |
+
|
| 215 |
# Prepend language token if provided
|
| 216 |
if language_id:
|
| 217 |
lang_token = f"[{language_id.lower()}]"
|
| 218 |
txt = lang_token + txt
|
| 219 |
+
|
| 220 |
+
txt = txt.replace(" ", SPACE)
|
| 221 |
code = self.tokenizer.encode(txt)
|
| 222 |
ids = code.ids
|
| 223 |
return ids
|
|
|
|
| 226 |
if isinstance(seq, torch.Tensor):
|
| 227 |
seq = seq.cpu().numpy()
|
| 228 |
|
| 229 |
+
txt: str = self.tokenizer.decode(
|
| 230 |
+
seq,
|
| 231 |
+
skip_special_tokens=False
|
| 232 |
+
)
|
| 233 |
+
txt = txt.replace(" ", "")
|
| 234 |
+
txt = txt.replace(SPACE, " ")
|
| 235 |
+
txt = txt.replace(EOT, "")
|
| 236 |
+
txt = txt.replace(UNK, "")
|
| 237 |
return txt
|
chatterbox/src/chatterbox/tts.py
CHANGED
|
@@ -22,6 +22,7 @@ REPO_ID = "ResembleAI/chatterbox"
|
|
| 22 |
BASE_REPO_ID = "ResembleAI/chatterbox"
|
| 23 |
T3_FILENAME = "t3_mtl23ls_v3.safetensors"
|
| 24 |
TOKENIZER_FILENAME = "grapheme_mtl_merged_expanded_v1.json"
|
|
|
|
| 25 |
T3_TEXT_VOCAB_SIZE = 2454
|
| 26 |
|
| 27 |
|
|
@@ -60,7 +61,7 @@ def punc_norm(text: str) -> str:
|
|
| 60 |
|
| 61 |
# Add full stop if no ending punc
|
| 62 |
text = text.rstrip(" ")
|
| 63 |
-
sentence_enders = {".", "!", "?", "-", ","}
|
| 64 |
if not any(text.endswith(p) for p in sentence_enders):
|
| 65 |
text += "."
|
| 66 |
|
|
@@ -183,7 +184,7 @@ class ChatterboxTTS:
|
|
| 183 |
@classmethod
|
| 184 |
def from_pretrained(cls, device: torch.device) -> 'ChatterboxTTS':
|
| 185 |
token = os.getenv("HF_TOKEN")
|
| 186 |
-
base_files = ["ve.pt", "s3gen.pt", TOKENIZER_FILENAME, "conds.pt"]
|
| 187 |
base_dir = Path(
|
| 188 |
snapshot_download(
|
| 189 |
repo_id=BASE_REPO_ID,
|
|
|
|
| 22 |
BASE_REPO_ID = "ResembleAI/chatterbox"
|
| 23 |
T3_FILENAME = "t3_mtl23ls_v3.safetensors"
|
| 24 |
TOKENIZER_FILENAME = "grapheme_mtl_merged_expanded_v1.json"
|
| 25 |
+
CANGJIE_FILENAME = "Cangjie5_TC.json"
|
| 26 |
T3_TEXT_VOCAB_SIZE = 2454
|
| 27 |
|
| 28 |
|
|
|
|
| 61 |
|
| 62 |
# Add full stop if no ending punc
|
| 63 |
text = text.rstrip(" ")
|
| 64 |
+
sentence_enders = {".", "!", "?", "-", ",", "、", ",", "。", "?", "!"}
|
| 65 |
if not any(text.endswith(p) for p in sentence_enders):
|
| 66 |
text += "."
|
| 67 |
|
|
|
|
| 184 |
@classmethod
|
| 185 |
def from_pretrained(cls, device: torch.device) -> 'ChatterboxTTS':
|
| 186 |
token = os.getenv("HF_TOKEN")
|
| 187 |
+
base_files = ["ve.pt", "s3gen.pt", TOKENIZER_FILENAME, CANGJIE_FILENAME, "conds.pt"]
|
| 188 |
base_dir = Path(
|
| 189 |
snapshot_download(
|
| 190 |
repo_id=BASE_REPO_ID,
|
requirements.txt
CHANGED
|
@@ -15,3 +15,14 @@ resemble-perth==1.0.1
|
|
| 15 |
silero-vad==5.1.2
|
| 16 |
conformer==0.3.2
|
| 17 |
safetensors
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
silero-vad==5.1.2
|
| 16 |
conformer==0.3.2
|
| 17 |
safetensors
|
| 18 |
+
spacy_pkuseg==1.0.1
|
| 19 |
+
|
| 20 |
+
# Runtime dependencies for russian-text-stresser. The package itself is
|
| 21 |
+
# installed without its incompatible spaCy 3.6 pin on the first Russian request.
|
| 22 |
+
beautifulsoup4>=4.11.1
|
| 23 |
+
lxml>=4.9.1
|
| 24 |
+
pymorphy2>=0.9.1
|
| 25 |
+
pymorphy2-dicts-ru>=2.4.417127.4579844
|
| 26 |
+
spacy==3.8.11
|
| 27 |
+
stressed-cyrillic-tools>=0.1.10
|
| 28 |
+
transliterate>=1.10.2
|