nexusbert commited on
Commit
3578a24
·
1 Parent(s): 933b4a7
Files changed (3) hide show
  1. Dockerfile +5 -16
  2. yarngpt/__init__.py +1 -0
  3. yarngpt/audiotokenizer.py +319 -0
Dockerfile CHANGED
@@ -33,13 +33,7 @@ RUN pip install --no-cache-dir huggingface-hub accelerate
33
  # Install additional dependencies
34
  RUN pip install --no-cache-dir outetts uroman gdown
35
 
36
- # Clone yarngpt repository and copy necessary files for AudioTokenizerV2
37
- RUN git clone https://github.com/saheedniyi02/yarngpt.git /tmp/yarngpt && \
38
- mkdir -p /code/yarngpt && \
39
- cp /tmp/yarngpt/audiotokenizer.py /code/yarngpt/ && \
40
- cp -r /tmp/yarngpt/default_speakers* /code/yarngpt/ && \
41
- echo '# yarngpt package' > /code/yarngpt/__init__.py && \
42
- rm -rf /tmp/yarngpt
43
 
44
  # Set Hugging Face cache inside container (persistent, not /tmp)
45
  ENV HF_HOME=/models/huggingface
@@ -58,18 +52,13 @@ RUN mkdir -p /models/huggingface && \
58
  # Note: Models will be downloaded lazily at runtime to save build storage
59
  # Pre-downloading is disabled to avoid storage limit issues
60
 
61
- # Download wavtokenizer configuration file (to both root and models directory)
62
- RUN wget -O /code/wavtokenizer_mediumdata_frame75_3s_nq1_code4096_dim512_kmeans200_attn.yaml \
63
- https://huggingface.co/novateur/WavTokenizer-medium-speech-75token/resolve/main/wavtokenizer_mediumdata_frame75_3s_nq1_code4096_dim512_kmeans200_attn.yaml && \
64
- cp /code/wavtokenizer_mediumdata_frame75_3s_nq1_code4096_dim512_kmeans200_attn.yaml /code/models/
65
-
66
- # Note: wavtokenizer checkpoint is too large for build-time download
67
- # Download it separately and provide via volume mount or runtime download
68
- # Example: gdown 1-ASeEkrn4HY49yZWHTASgfGFNXdVnLTt -O wavtokenizer_large_speech_320_24k.ckpt
69
- # Then mount: docker run -v $(pwd)/wavtokenizer_large_speech_320_24k.ckpt:/code/wavtokenizer_large_speech_320_24k.ckpt ...
70
 
71
  # Copy project files
72
  COPY . .
 
73
 
74
  # Expose FastAPI port
75
  EXPOSE 8000
 
33
  # Install additional dependencies
34
  RUN pip install --no-cache-dir outetts uroman gdown
35
 
36
+ ## Removed: clone before COPY to prefer vendored package
 
 
 
 
 
 
37
 
38
  # Set Hugging Face cache inside container (persistent, not /tmp)
39
  ENV HF_HOME=/models/huggingface
 
52
  # Note: Models will be downloaded lazily at runtime to save build storage
53
  # Pre-downloading is disabled to avoid storage limit issues
54
 
55
+ # Note: No downloads in Docker. Provide model assets via volume mount.
56
+ # Mount local models directory into the container at /code/models, e.g.:
57
+ # docker run -v $(pwd)/models:/code/models -p 8000:8000 IMAGE
 
 
 
 
 
 
58
 
59
  # Copy project files
60
  COPY . .
61
+ RUN if [ -d "/code/yarngpt" ] && [ -f "/code/yarngpt/audiotokenizer.py" ]; then echo "Using vendored yarngpt from project"; else echo "ERROR: vendored yarngpt missing" && exit 1; fi
62
 
63
  # Expose FastAPI port
64
  EXPOSE 8000
yarngpt/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # yarngpt package
yarngpt/audiotokenizer.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import torch
5
+ import inflect
6
+ import random
7
+ import uroman as ur
8
+ import numpy as np
9
+ import torchaudio
10
+ from transformers import AutoTokenizer
11
+ from outetts.wav_tokenizer.decoder import WavTokenizer
12
+ from outetts.wav_tokenizer.encoder.utils import convert_audio
13
+
14
+ class AudioTokenizer:
15
+
16
+ def __init__(self,tokenizer_path,wav_tokenizer_model_path,wav_tokenizer_config_path,):
17
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
18
+ self.text_prompt = "{bos}\n{text_start}{words}{text_end}\n{audio_start}\n"
19
+ self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
20
+ self.bos = "<|im_start|>"
21
+ self.eos = "<|im_end|>"
22
+ self.input_length=0
23
+ self.special_tokens = {
24
+ "audio_code": "<|{}|>",
25
+ "text_start": "<|text_start|>",
26
+ "text_end": "<|text_end|>",
27
+ "audio_start": "<|audio_start|>",
28
+ "audio_end": "<|audio_end|>",
29
+ "time": "<|t_{:.2f}|>",
30
+ "code_start": "<|code_start|>",
31
+ "code_end": "<|code_end|>",
32
+ "text_sep": "<|text_sep|>"
33
+ }
34
+ self.lec = inflect.engine()
35
+ #self.text_prompt = "{bos}\n{text_start}{words}{text_end}\n{audio_start}\n"
36
+ #self.config_path = "/content/wavtokenizer_mediumdata_frame75_3s_nq1_code4096_dim512_kmeans200_attn.yaml"
37
+ #self.model_path = "/content/wavtokenizer_large_speech_320_24k.ckpt"
38
+ self.wavtokenizer = WavTokenizer.from_pretrained0802(wav_tokenizer_config_path, wav_tokenizer_model_path)
39
+ self.wavtokenizer = self.wavtokenizer.to(self.device)
40
+ self.BASE_DIR = os.path.dirname(__file__)
41
+ self.DEFAULT_SPEAKERS_DIR = os.path.join(self.BASE_DIR, "default_speakers")
42
+ self.speakers=["idera","emma","onye","jude","osagie","tayo","zainab","joke","regina","remi","umar","chinenye"]
43
+
44
+ def get_speaker_path(self,speaker_name):
45
+ return os.path.join(self.DEFAULT_SPEAKERS_DIR, f"{speaker_name}.json")
46
+
47
+ def load_speaker(self, path: str):
48
+ with open(path, "r") as f:
49
+ return json.load(f)
50
+
51
+ def load_default_speaker(self, name: str):
52
+ name = name.lower().strip()
53
+ speaker_path=self.get_speaker_path(name)
54
+ return self.load_speaker(speaker_path)
55
+
56
+
57
+ def process_text(self, text: str):
58
+
59
+ text = re.sub(r'\d+(\.\d+)?', lambda x: self.lec.number_to_words(x.group()), text.lower())
60
+ text = re.sub(r'[-_/,\.\\]', ' ', text)
61
+ text = re.sub(r'[^a-z\s]', '', text)
62
+ text = re.sub(r'\s+', ' ', text).strip()
63
+ return text.split()
64
+
65
+ def create_audio_prompt(self,words: list) -> str:
66
+ prompt = []
67
+ for i in words:
68
+ word = i["word"]
69
+ duration = self.special_tokens["time"].format(float(i["duration"]))
70
+ tokens = "".join([self.special_tokens["audio_code"].format(c) for c in i["codes"]])
71
+ prompt.append(f'{word}{duration}{self.special_tokens["code_start"]}{tokens}{self.special_tokens["code_end"]}')
72
+ return "\n".join(prompt)
73
+
74
+ def create_prompt(self,text,speaker_name="idera"):
75
+ speaker=self.load_default_speaker(speaker_name)
76
+ input_words = self.process_text(speaker["text"]) + self.process_text(text)
77
+ #input_words = process_text(speaker["text"]) + input_words
78
+
79
+ inputs_words_strings = f"{self.special_tokens['text_sep']}".join([i.strip() for i in input_words])
80
+ prompt = self.text_prompt.format(
81
+ bos=self.bos,
82
+ text_start=self.special_tokens['text_start'],
83
+ words=inputs_words_strings,
84
+ text_end=self.special_tokens['text_end'],
85
+ audio_start=self.special_tokens['audio_start']
86
+ )
87
+ prompt += self.create_audio_prompt(speaker["words"])
88
+
89
+ return prompt
90
+
91
+ def tokenize_prompt(self, prompt):
92
+ input_ids = self.tokenizer.encode(
93
+ prompt,
94
+ add_special_tokens=False,
95
+ return_tensors="pt"
96
+ ).to(self.device)
97
+ self.input_length=input_ids.shape[1]
98
+ return input_ids.to(self.device)
99
+
100
+
101
+ def get_audio(self,discrete_code):
102
+ discrete_code=torch.tensor([[discrete_code]]).to(self.device)
103
+ features = self.wavtokenizer.codes_to_features(discrete_code).to(self.device)
104
+ bandwidth_id = torch.tensor([0]).to(self.device)
105
+ audio_out = self.wavtokenizer.decode(features, bandwidth_id=bandwidth_id)
106
+ return audio_out.to("cpu")
107
+
108
+ def extract_integers(self,s):
109
+ # Match integers enclosed in vertical bars |integer|
110
+ matches = re.findall(r'\|(-?\d+)\|', s)
111
+ # Convert matches to integers
112
+ return [int(match) for match in matches]
113
+
114
+ def get_codes(self, output):
115
+ new_output=self.tokenizer.decode(output[0][self.input_length:])
116
+ codes=self.extract_integers(new_output)
117
+ return codes
118
+
119
+
120
+ class AudioTokenizerForLocal(AudioTokenizer):
121
+
122
+ def __init__(self,tokenizer_path,wav_tokenizer_model_path,wav_tokenizer_config_path,):
123
+ super().__init__(tokenizer_path, wav_tokenizer_model_path, wav_tokenizer_config_path)
124
+ self.text_prompt = "{bos}\n{text_start}{words}{text_end}\n{lang}\n{audio_start}\n"
125
+ self.special_tokens = {
126
+ "audio_code": "<|{}|>",
127
+ "text_start": "<|text_start|>",
128
+ "text_end": "<|text_end|>",
129
+ "audio_start": "<|audio_start|>",
130
+ "audio_end": "<|audio_end|>",
131
+ "word_start": "<|word_start|>",
132
+ "word_end": "<|word_end|>",
133
+ "time": "<|t_{:.2f}|>",
134
+ "code_start": "<|code_start|>",
135
+ "code_end": "<|code_end|>",
136
+ "text_sep": "<|text_sep|>",
137
+ "hausa":"<|hausa|>",
138
+ "igbo":"<|igbo|>",
139
+ "yoruba":"<|yoruba|>",
140
+ }
141
+ self.uroman = ur.Uroman()
142
+ self.DEFAULT_SPEAKERS_DIR = os.path.join(self.BASE_DIR, "default_speakers_local")
143
+ self.speakers = [
144
+ "hausa_male1", "hausa_male2","yoruba_male1", "yoruba_male2","igbo_male2" #"igbo_male1", "igbo_male2",
145
+ "hausa_female1", "hausa_female2", "igbo_female1", "igbo_female2", "yoruba_female1", "yoruba_female2"
146
+ ]
147
+
148
+ def process_text(self, text: str):
149
+ text = self.uroman.romanize_string(text)
150
+ text = re.sub(r'\d+(\.\d+)?', lambda x: self.lec.number_to_words(x.group()), text.lower())
151
+ text = re.sub(r'[-_/,\.\\]', ' ', text)
152
+ text = re.sub(r'[^a-z\s]', '', text)
153
+ text = re.sub(r'\s+', ' ', text).strip()
154
+ return text.split()
155
+
156
+ def create_prompt(self,text,lang,speaker_name=None):
157
+ assert lang in ["hausa","igbo","yoruba"], f"Invalid language: {lang}, language must be one of ['hausa','igbo','yoruba']"
158
+ #if no speaker
159
+ if speaker_name is None:
160
+ if lang=="hausa":
161
+ speaker_name=random.choice(["hausa_male1","hausa_male2","hausa_female1","hausa_female2"])
162
+ elif lang=="igbo":
163
+ speaker_name=random.choice(["igbo_female1","igbo_female2","igbo_male2"])#"igbo_male1"])
164
+ else:
165
+ speaker_name=random.choice(["yoruba_male2","yoruba_female1","yoruba_female2"])
166
+ speaker=self.load_default_speaker(speaker_name)
167
+ input_words = self.process_text(speaker["text"]) + self.process_text(text)
168
+ #input_words = process_text(speaker["text"]) + input_words
169
+
170
+ inputs_words_strings = f"{self.special_tokens['text_sep']}".join([i.strip() for i in input_words])
171
+ prompt = self.text_prompt.format(
172
+ bos=self.bos,
173
+ text_start=self.special_tokens['text_start'],
174
+ words=inputs_words_strings,
175
+ text_end=self.special_tokens['text_end'],
176
+ lang=self.special_tokens[lang],
177
+ audio_start=self.special_tokens['audio_start']
178
+ )
179
+ prompt += self.create_audio_prompt(speaker["words"])
180
+
181
+ return prompt
182
+
183
+
184
+ class AudioTokenizerV2(AudioTokenizer):
185
+
186
+ def __init__(self,tokenizer_path,wav_tokenizer_model_path,wav_tokenizer_config_path,):
187
+ super().__init__(tokenizer_path, wav_tokenizer_model_path, wav_tokenizer_config_path)
188
+ self.text_prompt = "{bos}\n{text_start}{words}{text_end}\n{lang}\n{audio_start}\n"
189
+ self.asr_prompt="{bos}\n{code_start}{codes}{code_end}\n{asr}\n"
190
+ self.special_tokens = {
191
+ "audio_code": "<|{}|>",
192
+ "text_start": "<|text_start|>",
193
+ "text_end": "<|text_end|>",
194
+ "audio_start": "<|audio_start|>",
195
+ "audio_end": "<|audio_end|>",
196
+ "word_start": "<|word_start|>",
197
+ "word_end": "<|word_end|>",
198
+ "time": "<|t_{:.2f}|>",
199
+ "code_start": "<|code_start|>",
200
+ "code_end": "<|code_end|>",
201
+ "text_sep": "<|text_sep|>",
202
+ "hausa":"<|hausa|>",
203
+ "igbo":"<|igbo|>",
204
+ "yoruba":"<|yoruba|>",
205
+ "english":"<|english|>",#<|english|>
206
+ "asr":"<|asr|>"
207
+ }
208
+ self.uroman = ur.Uroman()
209
+ self.DEFAULT_SPEAKERS_DIR_LOCAL = os.path.join(self.BASE_DIR, "default_speakers_local")
210
+ self.DEFAULT_SPEAKERS_ENG = os.path.join(self.BASE_DIR, "default_speakers")
211
+ self.speakers_local = [
212
+ "hausa_male1", "hausa_male2","yoruba_male1", "yoruba_male2","igbo_male2" #"igbo_male1", "igbo_male2",
213
+ "hausa_female1", "hausa_female2", "igbo_female1", "igbo_female2", "yoruba_female1", "yoruba_female2"
214
+ ]
215
+ self.speakers_eng = ["idera","emma","onye","jude","osagie","tayo","zainab","joke","regina","remi","umar","chinenye","saheed"]
216
+ self.changed_tokens=[('<|1836|>', '<|453|><|453|>'),
217
+ ('<|1837|>', '<|1836|><|1836|>'),
218
+ ('<|1838|>', '<|1837|><|1837|>'),
219
+ ('<|1840|>', '<|244|><|167|>'),
220
+ ('<|1841|>', '<|235|><|219|>'),
221
+ ('<|1844|>', '<|453|><|244|>'),
222
+ ('<|1845|>', '<|1838|><|1838|>')]
223
+
224
+ def process_text(self, text: str):
225
+ text = self.uroman.romanize_string(text)
226
+ text = re.sub(r'\d+(\.\d+)?', lambda x: self.lec.number_to_words(x.group()), text.lower())
227
+ text = re.sub(r'[-_/,\.\\]', ' ', text)
228
+ text = re.sub(r'[^a-z\s]', '', text)
229
+ text = re.sub(r'\s+', ' ', text).strip()
230
+ return text.split()
231
+
232
+ def get_speaker_path(self,speaker_name,dir):
233
+ return os.path.join(dir, f"{speaker_name}.json")
234
+
235
+ def load_speaker(self, path: str):
236
+ with open(path, "r") as f:
237
+ return json.load(f)
238
+
239
+ def load_default_speaker(self, name: str,dir: str):
240
+ name = name.lower().strip()
241
+ speaker_path=self.get_speaker_path(name,dir)
242
+ return self.load_speaker(speaker_path)
243
+
244
+ def create_prompt(self,text,lang,speaker_name=None):
245
+ assert lang in ["hausa","igbo","yoruba","english"], f"Invalid language: {lang}, language must be one of ['hausa','igbo','yoruba','english']"
246
+ #if no speaker
247
+ dir=self.DEFAULT_SPEAKERS_DIR_LOCAL
248
+ if speaker_name is None:
249
+ if lang=="hausa":
250
+ speaker_name=random.choice(["hausa_male1","hausa_male2","hausa_female1","hausa_female2"])
251
+ elif lang=="igbo":
252
+ speaker_name=random.choice(["igbo_female1","igbo_female2","igbo_male2"])#"igbo_male1"])
253
+ elif lang=="yoruba":
254
+ speaker_name=random.choice(["yoruba_male2","yoruba_female1","yoruba_female2"])
255
+ else:
256
+ speaker_name=random.choice(self.speakers_eng)
257
+
258
+ if lang=="english":
259
+ dir=self.DEFAULT_SPEAKERS_ENG
260
+ speaker=self.load_default_speaker(speaker_name,dir)
261
+ input_words = self.process_text(speaker["text"]) + self.process_text(text)
262
+ #input_words = process_text(speaker["text"]) + input_words
263
+
264
+ inputs_words_strings = f"{self.special_tokens['text_sep']}".join([i.strip() for i in input_words])
265
+ prompt = self.text_prompt.format(
266
+ bos=self.bos,
267
+ text_start=self.special_tokens['text_start'],
268
+ words=inputs_words_strings,
269
+ text_end=self.special_tokens['text_end'],
270
+ lang=self.special_tokens[lang],
271
+ audio_start=self.special_tokens['audio_start']
272
+ )
273
+ prompt += self.create_audio_prompt(speaker["words"])
274
+
275
+ return prompt
276
+ def replace_tokens(text):
277
+ for pair in self.changed_tokens:
278
+ text=text.replace(pair[0],pair[-1])
279
+ return text
280
+
281
+ def resample(self,audio: np.ndarray, sr: int, target_sr: int):
282
+ audio = audio.to(dtype=torch.float32)
283
+ #.clone().detach()
284
+ audio = audio.unsqueeze(0)
285
+ # 1 as last arg corresponds to mono audio
286
+ resampled = convert_audio(audio, sr, target_sr, 1)
287
+ return resampled.to(self.device )
288
+
289
+ def quantize_wavtokenizer(self, path):
290
+ audio_data, sample_rate = torchaudio.load(path)
291
+ audio_data=audio_data.squeeze()
292
+ audio = self.resample(audio_data, sample_rate, 24000).to(self.device)
293
+ if audio.ndim==3:
294
+ audio=audio.squeeze(1)
295
+ bandwidth_id = torch.tensor([0]).to(self.device )
296
+ _, codes = self.wavtokenizer.encode_infer(audio, bandwidth_id=bandwidth_id)
297
+ codes = codes.squeeze(1).to(self.device)#+last_text_token
298
+ res=""
299
+ for code in codes[0].tolist():
300
+ res+=f"<|{code}|>"
301
+ return res
302
+
303
+ def create_asr_prompt(self,audio_path):
304
+ codes=self.quantize_wavtokenizer(audio_path)
305
+ prompt = self.asr_prompt.format(
306
+ bos=self.bos,
307
+ code_start=self.special_tokens['code_start'],
308
+ codes=codes,
309
+ code_end=self.special_tokens['code_end'],
310
+ asr=self.special_tokens["asr"],
311
+ )
312
+ return prompt
313
+
314
+ def get_asr_results(self,output):
315
+ res=""
316
+ for text in self.tokenizer.decode(output[0]).split("<|text_start|>")[-1].split("<|text_end|>")[0].split("\n"):
317
+ res+=text.split("<|word_start|>")[-1].split("<|word_end|>")[0]
318
+ res+=" "
319
+ return res.strip()