Zihan428 commited on
Commit
a06f868
·
1 Parent(s): d8e60ae

Add multilingual text processors

Browse files
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
- DEFAULT_CONFIG = {
13
- "audio": 'https://storage.googleapis.com/chatterbox-demo-samples/mtl_prompts/en_f1.flac',
14
- "text": 'Last month, we reached a new milestone with two billion views on our YouTube channel.',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 DEFAULT_CONFIG.get("audio")
36
 
37
 
38
- def default_text_for_ui():
39
- return DEFAULT_CONFIG.get("text", "")
 
 
 
 
 
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 text_to_tokens(self, text: str, language_id: str = None):
27
- text_tokens = self.encode(text, language_id=language_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  text_tokens = torch.IntTensor(text_tokens).unsqueeze(0)
29
  return text_tokens
30
 
31
- def encode(self, txt: str, language_id: str = None, verbose=False):
 
 
 
 
 
 
32
  """
33
- clean_text > (prepend lang_id) > replace SPACE > encode text using Tokenizer
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(' ', SPACE)
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(seq,
53
- skip_special_tokens=False)
54
- txt = txt.replace(' ', '')
55
- txt = txt.replace(SPACE, ' ')
56
- txt = txt.replace(EOT, '')
57
- txt = txt.replace(UNK, '')
 
 
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