binhqd commited on
Commit
f58e011
·
1 Parent(s): 4be97a5

Add custom inference handler for Maya1 TTS

Browse files
Files changed (1) hide show
  1. handler.py +125 -49
handler.py CHANGED
@@ -4,6 +4,20 @@ import os
4
  import struct
5
  import wave
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  class EndpointHandler:
9
  def __init__(self, path=""):
@@ -126,6 +140,67 @@ class EndpointHandler:
126
  )
127
  self.sf = sf
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  def __call__(self, data):
130
  """
131
  HF Endpoints format:
@@ -164,7 +239,10 @@ class EndpointHandler:
164
 
165
  if not text:
166
  return {"error": f"No text provided. Received: {data}"}
167
- prompt = description + "\n" + text
 
 
 
168
 
169
  # If running in fake mode (quick smoke test), synthesize a sine tone
170
  if getattr(self, "snac", None) is None and getattr(self, "model", None) is None:
@@ -198,64 +276,64 @@ class EndpointHandler:
198
 
199
  return {"audio_base64": b64, "sampling_rate": sr}
200
 
201
- # Tokenize and generate
202
- tokenizer_inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
 
 
 
 
 
 
 
 
203
 
204
  # Set default generation args if not provided
205
  default_gen_args = {
206
- "max_length": 2048,
207
- "do_sample": True,
208
- "temperature": 0.7,
209
  "top_p": 0.9,
 
 
 
 
210
  }
211
  default_gen_args.update(generation_args)
212
 
213
- outputs = self.model.generate(**tokenizer_inputs, **default_gen_args)
214
- token_ids = outputs[0]
215
-
216
- # Maya1 generates SNAC audio codes in the output tokens
217
- # Extract only the generated tokens (excluding input prompt tokens)
218
- generated_ids = token_ids[tokenizer_inputs["input_ids"].shape[1] :]
219
-
220
- # Convert token IDs to SNAC codes
221
- # Maya1 outputs are structured as SNAC token IDs that need to be reshaped
222
- # SNAC expects codes in shape (batch, num_codebooks, seq_len)
223
- # Assuming Maya1 outputs tokens sequentially for each codebook
224
- import torch
225
 
226
- snac_codes = generated_ids.unsqueeze(0) # Add batch dimension
 
227
 
228
- # SNAC 24kHz uses 7 codebooks - reshape accordingly
229
- # The model outputs interleaved codes, so we need to split them
230
- num_codebooks = 7
231
- seq_len = len(generated_ids) // num_codebooks
232
-
233
- if len(generated_ids) >= num_codebooks:
234
- # Reshape to (1, num_codebooks, seq_len) for SNAC decoder
235
- snac_codes = (
236
- generated_ids[: seq_len * num_codebooks]
237
- .reshape(num_codebooks, seq_len)
238
- .unsqueeze(0)
239
- )
240
-
241
- # Decode using SNAC to synthesize waveform
242
- with torch.no_grad():
243
- waveform = self.snac.decode(snac_codes.to(self.device))
244
-
245
- # Extract audio and convert to numpy
246
- # SNAC outputs shape (batch, samples) or (batch, 1, samples)
247
- # Safely remove all size-1 dimensions
248
- waveform = waveform.squeeze() # Remove all dimensions of size 1
249
 
250
- # Ensure we have a 1D tensor
251
- if waveform.dim() > 1:
252
- waveform = waveform[0] # Take first item if still multi-dimensional
253
 
254
- waveform = waveform.cpu().numpy()
255
-
256
- # convert waveform to bytes (e.g. WAV) using soundfile loaded into self.sf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  buf = io.BytesIO()
258
- self.sf.write(buf, waveform, 24000, format="WAV")
259
  wav_bytes = buf.getvalue()
260
  b64 = base64.b64encode(wav_bytes).decode("utf-8")
261
 
@@ -263,8 +341,6 @@ class EndpointHandler:
263
  "audio_base64": b64,
264
  "sampling_rate": 24000,
265
  }
266
-
267
-
268
  # Module-level convenience functions for hosting platforms (Hugging Face Endpoints)
269
  # The platform typically expects top-level `init` and `predict` (or `run`) callables
270
  # so we provide thin wrappers around the EndpointHandler class.
 
4
  import struct
5
  import wave
6
 
7
+ # SNAC token constants for Maya1
8
+ CODE_START_TOKEN_ID = 128257
9
+ CODE_END_TOKEN_ID = 128258
10
+ CODE_TOKEN_OFFSET = 128266
11
+ SNAC_MIN_ID = 128266
12
+ SNAC_MAX_ID = 156937
13
+ SNAC_TOKENS_PER_FRAME = 7
14
+
15
+ SOH_ID = 128259
16
+ EOH_ID = 128260
17
+ SOA_ID = 128261
18
+ BOS_ID = 128000
19
+ TEXT_EOT_ID = 128009
20
+
21
 
22
  class EndpointHandler:
23
  def __init__(self, path=""):
 
140
  )
141
  self.sf = sf
142
 
143
+ def build_prompt(self, description: str, text: str) -> str:
144
+ """Build formatted prompt for Maya1."""
145
+ soh_token = self.tokenizer.decode([SOH_ID])
146
+ eoh_token = self.tokenizer.decode([EOH_ID])
147
+ soa_token = self.tokenizer.decode([SOA_ID])
148
+ sos_token = self.tokenizer.decode([CODE_START_TOKEN_ID])
149
+ eot_token = self.tokenizer.decode([TEXT_EOT_ID])
150
+ bos_token = self.tokenizer.bos_token
151
+
152
+ formatted_text = f'<description="{description}"> {text}'
153
+
154
+ prompt = (
155
+ soh_token + bos_token + formatted_text + eot_token +
156
+ eoh_token + soa_token + sos_token
157
+ )
158
+
159
+ return prompt
160
+
161
+ def extract_snac_codes(self, token_ids: list) -> list:
162
+ """Extract SNAC codes from generated tokens."""
163
+ try:
164
+ eos_idx = token_ids.index(CODE_END_TOKEN_ID)
165
+ except ValueError:
166
+ eos_idx = len(token_ids)
167
+
168
+ snac_codes = [
169
+ token_id for token_id in token_ids[:eos_idx]
170
+ if SNAC_MIN_ID <= token_id <= SNAC_MAX_ID
171
+ ]
172
+
173
+ return snac_codes
174
+
175
+ def unpack_snac_from_7(self, snac_tokens: list) -> list:
176
+ """Unpack 7-token SNAC frames to 3 hierarchical levels."""
177
+ if snac_tokens and snac_tokens[-1] == CODE_END_TOKEN_ID:
178
+ snac_tokens = snac_tokens[:-1]
179
+
180
+ frames = len(snac_tokens) // SNAC_TOKENS_PER_FRAME
181
+ snac_tokens = snac_tokens[:frames * SNAC_TOKENS_PER_FRAME]
182
+
183
+ if frames == 0:
184
+ return [[], [], []]
185
+
186
+ l1, l2, l3 = [], [], []
187
+
188
+ for i in range(frames):
189
+ slots = snac_tokens[i*7:(i+1)*7]
190
+ l1.append((slots[0] - CODE_TOKEN_OFFSET) % 4096)
191
+ l2.extend([
192
+ (slots[1] - CODE_TOKEN_OFFSET) % 4096,
193
+ (slots[4] - CODE_TOKEN_OFFSET) % 4096,
194
+ ])
195
+ l3.extend([
196
+ (slots[2] - CODE_TOKEN_OFFSET) % 4096,
197
+ (slots[3] - CODE_TOKEN_OFFSET) % 4096,
198
+ (slots[5] - CODE_TOKEN_OFFSET) % 4096,
199
+ (slots[6] - CODE_TOKEN_OFFSET) % 4096,
200
+ ])
201
+
202
+ return [l1, l2, l3]
203
+
204
  def __call__(self, data):
205
  """
206
  HF Endpoints format:
 
239
 
240
  if not text:
241
  return {"error": f"No text provided. Received: {data}"}
242
+
243
+ # Use default description if not provided
244
+ if not description:
245
+ description = "Realistic male voice in the 30s age with american accent. Normal pitch, warm timbre, conversational pacing."
246
 
247
  # If running in fake mode (quick smoke test), synthesize a sine tone
248
  if getattr(self, "snac", None) is None and getattr(self, "model", None) is None:
 
276
 
277
  return {"audio_base64": b64, "sampling_rate": sr}
278
 
279
+ # Build properly formatted prompt for Maya1
280
+ prompt = self.build_prompt(description, text)
281
+
282
+ # Tokenize
283
+ import torch
284
+ tokenizer_inputs = self.tokenizer(prompt, return_tensors="pt")
285
+ if torch.cuda.is_available():
286
+ tokenizer_inputs = {k: v.to(self.device) for k, v in tokenizer_inputs.items()}
287
+ else:
288
+ tokenizer_inputs = tokenizer_inputs.to(self.device)
289
 
290
  # Set default generation args if not provided
291
  default_gen_args = {
292
+ "max_new_tokens": 2048,
293
+ "min_new_tokens": 28,
294
+ "temperature": 0.4,
295
  "top_p": 0.9,
296
+ "repetition_penalty": 1.1,
297
+ "do_sample": True,
298
+ "eos_token_id": CODE_END_TOKEN_ID,
299
+ "pad_token_id": self.tokenizer.pad_token_id,
300
  }
301
  default_gen_args.update(generation_args)
302
 
303
+ # Generate tokens
304
+ with torch.inference_mode():
305
+ outputs = self.model.generate(**tokenizer_inputs, **default_gen_args)
 
 
 
 
 
 
 
 
 
306
 
307
+ # Extract generated tokens (everything after the input prompt)
308
+ generated_ids = outputs[0, tokenizer_inputs["input_ids"].shape[1]:].tolist()
309
 
310
+ # Extract SNAC audio tokens
311
+ snac_tokens = self.extract_snac_codes(generated_ids)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
+ if len(snac_tokens) < 7:
314
+ return {"error": f"Not enough SNAC tokens generated: {len(snac_tokens)}"}
 
315
 
316
+ # Unpack SNAC tokens to 3 hierarchical levels
317
+ levels = self.unpack_snac_from_7(snac_tokens)
318
+
319
+ # Convert to tensors
320
+ codes_tensor = [
321
+ torch.tensor(level, dtype=torch.long, device=self.device).unsqueeze(0)
322
+ for level in levels
323
+ ]
324
+
325
+ # Generate final audio with SNAC decoder
326
+ with torch.inference_mode():
327
+ z_q = self.snac.quantizer.from_codes(codes_tensor)
328
+ audio = self.snac.decoder(z_q)[0, 0].cpu().numpy()
329
+
330
+ # Trim warmup samples (first 2048 samples)
331
+ if len(audio) > 2048:
332
+ audio = audio[2048:]
333
+
334
+ # Save audio to WAV
335
  buf = io.BytesIO()
336
+ self.sf.write(buf, audio, 24000, format="WAV")
337
  wav_bytes = buf.getvalue()
338
  b64 = base64.b64encode(wav_bytes).decode("utf-8")
339
 
 
341
  "audio_base64": b64,
342
  "sampling_rate": 24000,
343
  }
 
 
344
  # Module-level convenience functions for hosting platforms (Hugging Face Endpoints)
345
  # The platform typically expects top-level `init` and `predict` (or `run`) callables
346
  # so we provide thin wrappers around the EndpointHandler class.