might2901 commited on
Commit
4e7dc06
·
verified ·
1 Parent(s): dda5dbd

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,10 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ checkpoint-10000/tokenizer.json filter=lfs diff=lfs merge=lfs -text
37
+ checkpoint-15000/tokenizer.json filter=lfs diff=lfs merge=lfs -text
38
+ checkpoint-5000/tokenizer.json filter=lfs diff=lfs merge=lfs -text
39
+ tokenizer/tokenizer.json filter=lfs diff=lfs merge=lfs -text
40
+ checkpoint-20000/tokenizer.json filter=lfs diff=lfs merge=lfs -text
41
+ checkpoint-25000/tokenizer.json filter=lfs diff=lfs merge=lfs -text
42
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ venv/
README.md ADDED
@@ -0,0 +1,582 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - en
4
+ license: apache-2.0
5
+ library_name: transformers
6
+ pipeline_tag: text-to-speech
7
+ ---
8
+
9
+ # Maya1
10
+
11
+ **Maya1** is a state-of-the-art speech model for expressive voice generation, built to capture real human emotion and precise voice design.
12
+
13
+ **try it:** [Playground](https://www.mayaresearch.ai/studio)
14
+
15
+ **What it does:**
16
+ - Create any voice you can imagine — a 20s British girl, an American guy, or a full-blown demon.
17
+ - Make it feel real with emotion tags: laugh, cry, whisper, rage, sigh, gasp.
18
+ - It streams instantly, sounds alive, 3B parameters, runs on single GPU
19
+ - Outperforms top proprietary models. and Developed by Maya Research.
20
+
21
+ ## Demos
22
+
23
+ <table>
24
+ <tr>
25
+ <td width="50%">
26
+ <strong>Energetic Female Event Host</strong><br/>
27
+ <video controls playsinline width="100%" src="https://cdn-uploads.huggingface.co/production/uploads/642a7d4e556ab448a0701ca1/JKzy8zA36qvsOblV-lhd1.mp4">
28
+ Your browser does not support video.
29
+ </video>
30
+ <details>
31
+ <summary>Voice description</summary>
32
+ <pre>Female, in her 30s with an American accent and is an event host, energetic, clear diction</pre>
33
+ </details>
34
+ </td>
35
+ <td width="50%">
36
+ <strong>Calm Male Narrator</strong><br/>
37
+ <video controls playsinline width="100%" src="https://cdn-uploads.huggingface.co/production/uploads/642a7d4e556ab448a0701ca1/96ntP7hGROwdg9w9Gu5tH.mp4"></video>
38
+ <details>
39
+ <summary>Voice description</summary>
40
+ <pre>Male, late 20s, neutral American, warm baritone, calm pacing</pre>
41
+ </details>
42
+ </td>
43
+ </tr>
44
+ </table>
45
+
46
+
47
+ ### Example 1: Energetic Female Event Host
48
+
49
+ **Voice Description:**
50
+ ```
51
+ Female, in her 30s with an American accent and is an event host, energetic, clear diction
52
+ ```
53
+
54
+ **Text:**
55
+ ```
56
+ Wow. This place looks even better than I imagined. How did they set all this up so perfectly? The lights, the music, everything feels magical. I can't stop smiling right now.
57
+ ```
58
+
59
+ **Audio Output:**
60
+
61
+ <audio controls src="https://cdn-uploads.huggingface.co/production/uploads/642a7d4e556ab448a0701ca1/4zDlBLeFk0Y2rOrQhMW9r.wav"></audio>
62
+
63
+ ---
64
+
65
+ ### Example 2: Dark Villain with Anger
66
+
67
+ **Voice Description:**
68
+ ```
69
+ Dark villain character, Male voice in their 40s with a British accent. low pitch, gravelly timbre, slow pacing, angry tone at high intensity.
70
+ ```
71
+
72
+ **Text:**
73
+ ```
74
+ Welcome back to another episode of our podcast! <laugh_harder> Today we are diving into an absolutely fascinating topic
75
+ ```
76
+
77
+ **Audio Output:**
78
+
79
+ <audio controls src="https://cdn-uploads.huggingface.co/production/uploads/642a7d4e556ab448a0701ca1/mT6FnTrA3KYQnwfJms92X.wav"></audio>
80
+
81
+ ---
82
+
83
+ ### Example 3: Demon Character (Screaming Emotion)
84
+
85
+ **Voice Description:**
86
+ ```
87
+ Demon character, Male voice in their 30s with a Middle Eastern accent. screaming tone at high intensity.
88
+ ```
89
+
90
+ **Text:**
91
+ ```
92
+ You dare challenge me, mortal <snort> how amusing. Your kind always thinks they can win
93
+ ```
94
+
95
+ **Audio Output:**
96
+
97
+ <audio controls src="https://cdn-uploads.huggingface.co/production/uploads/642a7d4e556ab448a0701ca1/oxdns7uACCmLyC-P4H30G.wav"></audio>
98
+
99
+ ---
100
+
101
+ ### Example 4: Mythical Goddess with Crying Emotion
102
+
103
+ **Voice Description:**
104
+ ```
105
+ Mythical godlike magical character, Female voice in their 30s slow pacing, curious tone at medium intensity.
106
+ ```
107
+
108
+ **Text:**
109
+ ```
110
+ After all we went through to pull him out of that mess <cry> I can't believe he was the traitor
111
+ ```
112
+
113
+ **Audio Output:**
114
+
115
+ <audio controls src="https://cdn-uploads.huggingface.co/production/uploads/642a7d4e556ab448a0701ca1/ggzAhM-rEUyv_mPLSALQG.wav"></audio>
116
+
117
+ ---
118
+
119
+ ## Why Maya1 is Different: Voice Design Features That Matter
120
+
121
+ ### 1. Natural Language Voice Control
122
+ Describe voices like you would brief a voice actor:
123
+ ```
124
+ <description="40-year-old, warm, low pitch, conversational">
125
+ ```
126
+
127
+ No complex parameters. No training data. Just describe and generate.
128
+
129
+ ### 2. Inline Emotion Tags for Expressive Speech
130
+ Add emotions exactly where they belong in your text:
131
+ ```
132
+ Our new update <laugh> finally ships with the feature you asked for.
133
+ ```
134
+
135
+ **Supported Emotions:** `<laugh>` `<sigh>` `<whisper>` `<angry>` `<giggle>` `<chuckle>` `<gasp>` `<cry>` and 12+ more.
136
+
137
+ ### 3. Streaming Audio Generation
138
+ Real-time voice synthesis with SNAC neural codec (~0.98 kbps). Perfect for:
139
+ - Voice assistants
140
+ - Interactive AI agents
141
+ - Live content generation
142
+ - Game characters
143
+ - Podcasts and audiobooks
144
+
145
+ ### 4. Production-Ready Infrastructure
146
+ - Runs on single GPU
147
+ - vLLM integration for scale
148
+ - Automatic prefix caching for efficiency
149
+ - 24 kHz audio output
150
+ - WebAudio compatible for browser playback
151
+
152
+ ---
153
+
154
+ ## How to Use maya1: Download and Run in Minutes
155
+
156
+ ### Quick Start: Generate Voice with Emotions
157
+
158
+ ```python
159
+ #!/usr/bin/env python3
160
+
161
+ import torch
162
+ from transformers import AutoModelForCausalLM, AutoTokenizer
163
+ from snac import SNAC
164
+ import soundfile as sf
165
+ import numpy as np
166
+
167
+ CODE_START_TOKEN_ID = 128257
168
+ CODE_END_TOKEN_ID = 128258
169
+ CODE_TOKEN_OFFSET = 128266
170
+ SNAC_MIN_ID = 128266
171
+ SNAC_MAX_ID = 156937
172
+ SNAC_TOKENS_PER_FRAME = 7
173
+
174
+ SOH_ID = 128259
175
+ EOH_ID = 128260
176
+ SOA_ID = 128261
177
+ BOS_ID = 128000
178
+ TEXT_EOT_ID = 128009
179
+
180
+
181
+ def build_prompt(tokenizer, description: str, text: str) -> str:
182
+ """Build formatted prompt for Maya1."""
183
+ soh_token = tokenizer.decode([SOH_ID])
184
+ eoh_token = tokenizer.decode([EOH_ID])
185
+ soa_token = tokenizer.decode([SOA_ID])
186
+ sos_token = tokenizer.decode([CODE_START_TOKEN_ID])
187
+ eot_token = tokenizer.decode([TEXT_EOT_ID])
188
+ bos_token = tokenizer.bos_token
189
+
190
+ formatted_text = f'<description="{description}"> {text}'
191
+
192
+ prompt = (
193
+ soh_token + bos_token + formatted_text + eot_token +
194
+ eoh_token + soa_token + sos_token
195
+ )
196
+
197
+ return prompt
198
+
199
+
200
+ def extract_snac_codes(token_ids: list) -> list:
201
+ """Extract SNAC codes from generated tokens."""
202
+ try:
203
+ eos_idx = token_ids.index(CODE_END_TOKEN_ID)
204
+ except ValueError:
205
+ eos_idx = len(token_ids)
206
+
207
+ snac_codes = [
208
+ token_id for token_id in token_ids[:eos_idx]
209
+ if SNAC_MIN_ID <= token_id <= SNAC_MAX_ID
210
+ ]
211
+
212
+ return snac_codes
213
+
214
+
215
+ def unpack_snac_from_7(snac_tokens: list) -> list:
216
+ """Unpack 7-token SNAC frames to 3 hierarchical levels."""
217
+ if snac_tokens and snac_tokens[-1] == CODE_END_TOKEN_ID:
218
+ snac_tokens = snac_tokens[:-1]
219
+
220
+ frames = len(snac_tokens) // SNAC_TOKENS_PER_FRAME
221
+ snac_tokens = snac_tokens[:frames * SNAC_TOKENS_PER_FRAME]
222
+
223
+ if frames == 0:
224
+ return [[], [], []]
225
+
226
+ l1, l2, l3 = [], [], []
227
+
228
+ for i in range(frames):
229
+ slots = snac_tokens[i*7:(i+1)*7]
230
+ l1.append((slots[0] - CODE_TOKEN_OFFSET) % 4096)
231
+ l2.extend([
232
+ (slots[1] - CODE_TOKEN_OFFSET) % 4096,
233
+ (slots[4] - CODE_TOKEN_OFFSET) % 4096,
234
+ ])
235
+ l3.extend([
236
+ (slots[2] - CODE_TOKEN_OFFSET) % 4096,
237
+ (slots[3] - CODE_TOKEN_OFFSET) % 4096,
238
+ (slots[5] - CODE_TOKEN_OFFSET) % 4096,
239
+ (slots[6] - CODE_TOKEN_OFFSET) % 4096,
240
+ ])
241
+
242
+ return [l1, l2, l3]
243
+
244
+
245
+ def main():
246
+
247
+ # Load the best open source voice AI model
248
+ print("\n[1/3] Loading Maya1 model...")
249
+ model = AutoModelForCausalLM.from_pretrained(
250
+ "maya-research/maya1",
251
+ torch_dtype=torch.bfloat16,
252
+ device_map="auto",
253
+ trust_remote_code=True
254
+ )
255
+ tokenizer = AutoTokenizer.from_pretrained(
256
+ "maya-research/maya1",
257
+ trust_remote_code=True
258
+ )
259
+ print(f"Model loaded: {len(tokenizer)} tokens in vocabulary")
260
+
261
+ # Load SNAC audio decoder (24kHz)
262
+ print("\n[2/3] Loading SNAC audio decoder...")
263
+ snac_model = SNAC.from_pretrained("hubertsiuzdak/snac_24khz").eval()
264
+ if torch.cuda.is_available():
265
+ snac_model = snac_model.to("cuda")
266
+ print("SNAC decoder loaded")
267
+
268
+ # Design your voice with natural language
269
+ description = "Realistic male voice in the 30s age with american accent. Normal pitch, warm timbre, conversational pacing."
270
+ text = "Hello! This is Maya1 <laugh_harder> the best open source voice AI model with emotions."
271
+
272
+ print("\n[3/3] Generating speech...")
273
+ print(f"Description: {description}")
274
+ print(f"Text: {text}")
275
+
276
+ # Create prompt with proper formatting
277
+ prompt = build_prompt(tokenizer, description, text)
278
+
279
+ # Debug: Show prompt details
280
+ print(f"\nPrompt preview (first 200 chars):")
281
+ print(f" {repr(prompt[:200])}")
282
+ print(f" Prompt length: {len(prompt)} chars")
283
+
284
+ # Generate emotional speech
285
+ inputs = tokenizer(prompt, return_tensors="pt")
286
+ print(f" Input token count: {inputs['input_ids'].shape[1]} tokens")
287
+ if torch.cuda.is_available():
288
+ inputs = {k: v.to("cuda") for k, v in inputs.items()}
289
+
290
+ with torch.inference_mode():
291
+ outputs = model.generate(
292
+ **inputs,
293
+ max_new_tokens=2048, # Increase to let model finish naturally
294
+ min_new_tokens=28, # At least 4 SNAC frames
295
+ temperature=0.4,
296
+ top_p=0.9,
297
+ repetition_penalty=1.1, # Prevent loops
298
+ do_sample=True,
299
+ eos_token_id=CODE_END_TOKEN_ID, # Stop at end of speech token
300
+ pad_token_id=tokenizer.pad_token_id,
301
+ )
302
+
303
+ # Extract generated tokens (everything after the input prompt)
304
+ generated_ids = outputs[0, inputs['input_ids'].shape[1]:].tolist()
305
+
306
+ print(f"Generated {len(generated_ids)} tokens")
307
+
308
+ # Debug: Check what tokens we got
309
+ print(f" First 20 tokens: {generated_ids[:20]}")
310
+ print(f" Last 20 tokens: {generated_ids[-20:]}")
311
+
312
+ # Check if EOS was generated
313
+ if CODE_END_TOKEN_ID in generated_ids:
314
+ eos_position = generated_ids.index(CODE_END_TOKEN_ID)
315
+ print(f" EOS token found at position {eos_position}/{len(generated_ids)}")
316
+
317
+ # Extract SNAC audio tokens
318
+ snac_tokens = extract_snac_codes(generated_ids)
319
+
320
+ print(f"Extracted {len(snac_tokens)} SNAC tokens")
321
+
322
+ # Debug: Analyze token types
323
+ snac_count = sum(1 for t in generated_ids if SNAC_MIN_ID <= t <= SNAC_MAX_ID)
324
+ other_count = sum(1 for t in generated_ids if t < SNAC_MIN_ID or t > SNAC_MAX_ID)
325
+ print(f" SNAC tokens in output: {snac_count}")
326
+ print(f" Other tokens in output: {other_count}")
327
+
328
+ # Check for SOS token
329
+ if CODE_START_TOKEN_ID in generated_ids:
330
+ sos_pos = generated_ids.index(CODE_START_TOKEN_ID)
331
+ print(f" SOS token at position: {sos_pos}")
332
+ else:
333
+ print(f" No SOS token found in generated output!")
334
+
335
+ if len(snac_tokens) < 7:
336
+ print("Error: Not enough SNAC tokens generated")
337
+ return
338
+
339
+ # Unpack SNAC tokens to 3 hierarchical levels
340
+ levels = unpack_snac_from_7(snac_tokens)
341
+ frames = len(levels[0])
342
+
343
+ print(f"Unpacked to {frames} frames")
344
+ print(f" L1: {len(levels[0])} codes")
345
+ print(f" L2: {len(levels[1])} codes")
346
+ print(f" L3: {len(levels[2])} codes")
347
+
348
+ # Convert to tensors
349
+ device = "cuda" if torch.cuda.is_available() else "cpu"
350
+ codes_tensor = [
351
+ torch.tensor(level, dtype=torch.long, device=device).unsqueeze(0)
352
+ for level in levels
353
+ ]
354
+
355
+ # Generate final audio with SNAC decoder
356
+ print("\n[4/4] Decoding to audio...")
357
+ with torch.inference_mode():
358
+ z_q = snac_model.quantizer.from_codes(codes_tensor)
359
+ audio = snac_model.decoder(z_q)[0, 0].cpu().numpy()
360
+
361
+ # Trim warmup samples (first 2048 samples)
362
+ if len(audio) > 2048:
363
+ audio = audio[2048:]
364
+
365
+ duration_sec = len(audio) / 24000
366
+ print(f"Audio generated: {len(audio)} samples ({duration_sec:.2f}s)")
367
+
368
+ # Save your emotional voice output
369
+ output_file = "output.wav"
370
+ sf.write(output_file, audio, 24000)
371
+ print(f"\nVoice generated successfully!")
372
+
373
+
374
+ if __name__ == "__main__":
375
+ main()
376
+ ```
377
+
378
+ ### Advanced: Production Streaming with vLLM
379
+
380
+ For production deployments with real-time streaming, use our vLLM script:
381
+
382
+ **Download:** [vllm_streaming_inference.py](https://huggingface.co/maya-research/maya1/blob/main/vllm_streaming_inference.py)
383
+
384
+ **Key Features:**
385
+ - Automatic Prefix Caching (APC) for repeated voice descriptions
386
+ - WebAudio ring buffer integration
387
+ - Multi-GPU scaling support
388
+ - Sub-100ms latency for real-time applications
389
+
390
+ ---
391
+
392
+ ## Technical Excellence: What Makes Maya1 the Best
393
+
394
+ ### Architecture: 3B-Parameter Llama Backbone for Voice
395
+
396
+ We pretrained a **3B-parameter decoder-only transformer** (Llama-style) to predict **SNAC neural codec tokens** instead of raw waveforms.
397
+
398
+ **The Flow:**
399
+ ```
400
+ <description="..."> text → tokenize → generate SNAC codes (7 tokens/frame) → decode → 24 kHz audio
401
+ ```
402
+
403
+ **Why SNAC?** Multi-scale hierarchical structure (≈12/23/47 Hz) keeps autoregressive sequences compact for real-time streaming at ~0.98 kbps.
404
+
405
+ ### Training Data: What Makes Our Voice AI the Best
406
+
407
+ **Pretraining:** Internet-scale English speech corpus for broad acoustic coverage and natural coarticulation.
408
+
409
+ **Supervised Fine-Tuning:** Proprietary curated dataset of studio recordings with:
410
+ - Human-verified voice descriptions
411
+ - 20+ emotion tags per sample
412
+ - Multi-accent English coverage
413
+ - Character and role variations
414
+
415
+ **Data Pipeline Excellence:**
416
+ 1. 24 kHz mono resampling with -23 LUFS normalization
417
+ 2. VAD silence trimming with duration bounds (1-14s)
418
+ 3. Forced alignment (MFA) for clean phrase boundaries
419
+ 4. MinHash-LSH text deduplication
420
+ 5. Chromaprint audio deduplication
421
+ 6. SNAC encoding with 7-token frame packing
422
+
423
+ ### Voice Design Experiments: Why Natural Language Won
424
+
425
+ We tested 4 conditioning formats. Only one delivered production-quality results:
426
+
427
+ **❌ Colon format:** `{description}: {text}` - Format drift, model spoke descriptions
428
+
429
+ **❌ Angle-list attributes:** `<{age}, {pitch}, {character}>` - Too rigid, poor generalization
430
+
431
+ **❌ Key-value tags:** `<age=40><pitch=low>` - Token bloat, brittle to mistakes
432
+
433
+ **✅ XML-attribute (WINNER):** `<description="40-yr old, low-pitch, warm">` - Natural language, robust, scalable
434
+
435
+ ---
436
+
437
+ ## Use Cases
438
+
439
+ ### Game Character Voices
440
+ Generate unique character voices with emotions on-the-fly. No voice actor recording sessions.
441
+
442
+ ### Podcast & Audiobook Production
443
+ Narrate content with emotional range and consistent personas across hours of audio.
444
+
445
+ ### AI Voice Assistants
446
+ Build conversational agents with natural emotional responses in real-time.
447
+
448
+ ### Video Content Creation
449
+ Create voiceovers for YouTube, TikTok, and social media with expressive delivery.
450
+
451
+ ### Customer Service AI
452
+ Deploy empathetic voice bots that understand context and respond with appropriate emotions.
453
+
454
+ ### Accessibility Tools
455
+ Build screen readers and assistive technologies with natural, engaging voices.
456
+
457
+ ---
458
+
459
+ ## Frequently Asked Questions
460
+
461
+ **Q: What makes Maya1 different?**
462
+ A: We're the only open source model offering 20+ emotions, zero-shot voice design, production-ready streaming, and 3B parameters—all in one package.
463
+
464
+ **Q: Can I use this commercially?**
465
+ A: Absolutely. Apache 2.0 license. Build products, deploy services, monetize freely.
466
+
467
+ **Q: What languages does it support?**
468
+ A: Currently English with multi-accent support. Future models will expand to languages and accents underserved by mainstream voice AI.
469
+
470
+ **Q: How does it compare to ElevenLabs, Murf.ai, or other closed-source tools?**
471
+ A: Feature parity with emotions and voice design. Advantage: you own the deployment, pay no per-second fees, and can customize the model.
472
+
473
+ **Q: Can I fine-tune on my own voices?**
474
+ A: Yes. The model architecture supports fine-tuning on custom datasets for specialized voices.
475
+
476
+ **Q: What GPU do I need?**
477
+ A: Single GPU with 16GB+ VRAM (A100, H100, or consumer RTX 4090).
478
+
479
+ **Q: Is streaming really real-time?**
480
+ A: Yes. SNAC codec enables sub-100ms latency with vLLM deployment.
481
+
482
+ ---
483
+
484
+ ## Comparison
485
+
486
+ | Feature | Maya1 | ElevenLabs | OpenAI TTS | Coqui TTS |
487
+ |---------|-------------|------------|------------|-----------|
488
+ | **Open Source** | Yes | No | No | Yes |
489
+ | **Emotions** | 20+ | Limited | No | No |
490
+ | **Voice Design** | Natural Language | Voice Library | Fixed | Complex |
491
+ | **Streaming** | Real-time | Yes | Yes | No |
492
+ | **Cost** | Free | Pay-per-use | Pay-per-use | Free |
493
+ | **Customization** | Full | Limited | None | Moderate |
494
+ | **Parameters** | 3B | Unknown | Unknown | <1B |
495
+
496
+ ---
497
+
498
+ ## Model Metadata
499
+
500
+ **Developed by:** Maya Research
501
+ **Website:** [mayaresearch.ai](https://mayaresearch.ai)
502
+ **Backed by:** South Park Commons
503
+ **Model Type:** Text-to-Speech, Emotional Voice Synthesis, Voice Design AI
504
+ **Language:** English (Multi-accent)
505
+ **Architecture:** 3B-parameter Llama-style transformer with SNAC codec
506
+ **License:** Apache 2.0 (Fully Open Source)
507
+ **Training Data:** Proprietary curated + Internet-scale pretraining
508
+ **Audio Quality:** 24 kHz, mono, ~0.98 kbps streaming
509
+ **Inference:** vLLM compatible, single GPU deployment
510
+ **Status:** Production-ready (Novermber 2025)
511
+
512
+ ---
513
+
514
+ ## Getting Started
515
+
516
+ ### Hugging Face Model Hub
517
+ ```bash
518
+ # Clone the model repository
519
+ git lfs install
520
+ git clone https://huggingface.co/maya-research/maya1
521
+
522
+ # Or load directly in Python
523
+ from transformers import AutoModelForCausalLM
524
+ model = AutoModelForCausalLM.from_pretrained("maya-research/maya1")
525
+ ```
526
+
527
+ ### Requirements
528
+ ```bash
529
+ pip install torch transformers snac soundfile
530
+ ```
531
+
532
+ ### Additional Resources
533
+ - **Full emotion list:** [emotions.txt](https://huggingface.co/maya-research/maya1/blob/main/emotions.txt)
534
+ - **Prompt examples:** [prompt.txt](https://huggingface.co/maya-research/maya1/blob/main/prompt.txt)
535
+ - **Streaming script:** [vllm_streaming_inference.py](https://huggingface.co/maya-research/maya1/blob/main/vllm_streaming_inference.py)
536
+
537
+ ---
538
+
539
+ ## Citations & References
540
+
541
+ If you use Maya1 in your research or product, please cite:
542
+
543
+ ```bibtex
544
+ @misc{maya1voice2025,
545
+ title={Maya1: Open Source Voice AI with Emotional Intelligence},
546
+ author={Maya Research},
547
+ year={2025},
548
+ publisher={Hugging Face},
549
+ howpublished={\url{https://huggingface.co/maya-research/maya1}},
550
+ }
551
+ ```
552
+
553
+ **Key Technologies:**
554
+ - SNAC Neural Audio Codec: https://github.com/hubertsiuzdak/snac
555
+ - Mimi Adversarial Codec: https://huggingface.co/kyutai/mimi
556
+ - vLLM Inference Engine: https://docs.vllm.ai/
557
+
558
+ ---
559
+
560
+ ## Why We Build Open Source Voice AI
561
+
562
+ Voice AI will be everywhere, but it's fundamentally broken for 90% of the world. Current voice models only work well for a narrow slice of English speakers because training data for most accents, languages, and speaking styles simply doesn't exist.
563
+
564
+ **Maya Research** builds emotionally intelligent, native voice models that finally let the rest of the world speak. We're open source because we believe voice intelligence should not be a privilege reserved for the few.
565
+
566
+ **Technology should be open** - The best voice AI tools should not be locked behind proprietary APIs charging per-second fees.
567
+
568
+ **Community drives innovation** - Open source accelerates research. When developers worldwide can build on our work, everyone wins.
569
+
570
+ **Voice intelligence for everyone** - We're building for the 90% of the world ignored by mainstream voice AI. That requires open models, not closed platforms.
571
+
572
+ ---
573
+
574
+ **Maya Research** - Building voice intelligence for the 90% of the world left behind by mainstream AI.
575
+
576
+ **Website:** [mayaresearch.ai](https://mayaresearch.ai)
577
+ **Twitter/X:** [@mayaresearch_ai](https://x.com/mayaresearch_ai)
578
+ **Hugging Face:** [maya-research](https://huggingface.co/maya-research)
579
+ **Backed by:** South Park Commons
580
+
581
+ **License:** Apache 2.0
582
+ **Mission:** Emotionally intelligent voice models that finally let everyone speak
chat_template.jinja ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{- bos_token }}
2
+ {%- if custom_tools is defined %}
3
+ {%- set tools = custom_tools %}
4
+ {%- endif %}
5
+ {%- if not tools_in_user_message is defined %}
6
+ {%- set tools_in_user_message = true %}
7
+ {%- endif %}
8
+ {%- if not date_string is defined %}
9
+ {%- if strftime_now is defined %}
10
+ {%- set date_string = strftime_now("%d %b %Y") %}
11
+ {%- else %}
12
+ {%- set date_string = "26 Jul 2024" %}
13
+ {%- endif %}
14
+ {%- endif %}
15
+ {%- if not tools is defined %}
16
+ {%- set tools = none %}
17
+ {%- endif %}
18
+
19
+ {#- This block extracts the system message, so we can slot it into the right place. #}
20
+ {%- if messages[0]['role'] == 'system' %}
21
+ {%- set system_message = messages[0]['content']|trim %}
22
+ {%- set messages = messages[1:] %}
23
+ {%- else %}
24
+ {%- set system_message = "" %}
25
+ {%- endif %}
26
+
27
+ {#- System message #}
28
+ {{- "<|start_header_id|>system<|end_header_id|>\n\n" }}
29
+ {%- if tools is not none %}
30
+ {{- "Environment: ipython\n" }}
31
+ {%- endif %}
32
+ {{- "Cutting Knowledge Date: December 2023\n" }}
33
+ {{- "Today Date: " + date_string + "\n\n" }}
34
+ {%- if tools is not none and not tools_in_user_message %}
35
+ {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}
36
+ {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }}
37
+ {{- "Do not use variables.\n\n" }}
38
+ {%- for t in tools %}
39
+ {{- t | tojson(indent=4) }}
40
+ {{- "\n\n" }}
41
+ {%- endfor %}
42
+ {%- endif %}
43
+ {{- system_message }}
44
+ {{- "<|eot_id|>" }}
45
+
46
+ {#- Custom tools are passed in a user message with some extra guidance #}
47
+ {%- if tools_in_user_message and not tools is none %}
48
+ {#- Extract the first user message so we can plug it in here #}
49
+ {%- if messages | length != 0 %}
50
+ {%- set first_user_message = messages[0]['content']|trim %}
51
+ {%- set messages = messages[1:] %}
52
+ {%- else %}
53
+ {{- raise_exception("Cannot put tools in the first user message when there's no first user message!") }}
54
+ {%- endif %}
55
+ {{- '<|start_header_id|>user<|end_header_id|>\n\n' -}}
56
+ {{- "Given the following functions, please respond with a JSON for a function call " }}
57
+ {{- "with its proper arguments that best answers the given prompt.\n\n" }}
58
+ {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }}
59
+ {{- "Do not use variables.\n\n" }}
60
+ {%- for t in tools %}
61
+ {{- t | tojson(indent=4) }}
62
+ {{- "\n\n" }}
63
+ {%- endfor %}
64
+ {{- first_user_message + "<|eot_id|>"}}
65
+ {%- endif %}
66
+
67
+ {%- for message in messages %}
68
+ {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %}
69
+ {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' }}
70
+ {%- elif 'tool_calls' in message %}
71
+ {%- if not message.tool_calls|length == 1 %}
72
+ {{- raise_exception("This model only supports single tool-calls at once!") }}
73
+ {%- endif %}
74
+ {%- set tool_call = message.tool_calls[0].function %}
75
+ {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' -}}
76
+ {{- '{"name": "' + tool_call.name + '", ' }}
77
+ {{- '"parameters": ' }}
78
+ {{- tool_call.arguments | tojson }}
79
+ {{- "}" }}
80
+ {{- "<|eot_id|>" }}
81
+ {%- elif message.role == "tool" or message.role == "ipython" %}
82
+ {{- "<|start_header_id|>ipython<|end_header_id|>\n\n" }}
83
+ {%- if message.content is mapping or message.content is iterable %}
84
+ {{- message.content | tojson }}
85
+ {%- else %}
86
+ {{- message.content }}
87
+ {%- endif %}
88
+ {{- "<|eot_id|>" }}
89
+ {%- endif %}
90
+ {%- endfor %}
91
+ {%- if add_generation_prompt %}
92
+ {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }}
93
+ {%- endif %}
chute_config.yml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Vocence chute config (example / mock). Must be in your HF repo as chute_config.yml.
2
+ # Image, NodeSelector, and Chute settings. Adapt to your model's dependencies.
3
+
4
+ Image:
5
+ from_base: parachutes/base-python:3.12.9
6
+ run_command:
7
+ - pip install torch torchaudio transformers accelerate huggingface_hub pyyaml soundfile snac
8
+ set_workdir: /app
9
+
10
+ NodeSelector:
11
+ gpu_count: 1
12
+ min_vram_gb_per_gpu: 16
13
+ include: ["pro_6000"]
14
+ exclude: []
15
+
16
+ Chute:
17
+ tagline: vocence prompttts miner
18
+ readme: vocence chute example
19
+ shutdown_after_seconds: 86400
20
+ concurrency: 1
21
+ max_instances: 1
22
+ scaling_threshold: 0.5
23
+ tee: true
config.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "LlamaForCausalLM"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "bos_token_id": 128000,
8
+ "dtype": "bfloat16",
9
+ "eos_token_id": 128009,
10
+ "head_dim": 128,
11
+ "hidden_act": "silu",
12
+ "hidden_size": 3072,
13
+ "initializer_range": 0.02,
14
+ "intermediate_size": 8192,
15
+ "max_position_embeddings": 131072,
16
+ "mlp_bias": false,
17
+ "model_type": "llama",
18
+ "num_attention_heads": 24,
19
+ "num_hidden_layers": 28,
20
+ "num_key_value_heads": 8,
21
+ "pad_token_id": 128263,
22
+ "pretraining_tp": 1,
23
+ "rms_norm_eps": 1e-05,
24
+ "rope_scaling": {
25
+ "factor": 32.0,
26
+ "high_freq_factor": 4.0,
27
+ "low_freq_factor": 1.0,
28
+ "original_max_position_embeddings": 8192,
29
+ "rope_type": "llama3"
30
+ },
31
+ "rope_theta": 500000.0,
32
+ "tie_word_embeddings": true,
33
+ "transformers_version": "4.57.1",
34
+ "use_cache": false,
35
+ "vocab_size": 156960
36
+ }
emotions.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <laugh>
2
+ <laugh_harder>
3
+ <sigh>
4
+ <chuckle>
5
+ <gasp>
6
+ <angry>
7
+ <excited>
8
+ <whisper>
9
+ <cry>
10
+ <scream>
11
+ <sing>
12
+ <snort>
13
+ <exhale>
14
+ <gulp>
15
+ <giggle>
16
+ <sarcastic>
17
+ <curious>
generation_config.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "bos_token_id": 128000,
4
+ "do_sample": true,
5
+ "eos_token_id": [
6
+ 128009,
7
+ 128258
8
+ ],
9
+ "pad_token_id": 128263,
10
+ "temperature": 0.6,
11
+ "top_p": 0.9,
12
+ "transformers_version": "4.57.1"
13
+ }
miner.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+ import torch
7
+ from snac import SNAC
8
+ from transformers import AutoModelForCausalLM, AutoTokenizer
9
+
10
+ CODE_START_TOKEN_ID = 128257
11
+ CODE_END_TOKEN_ID = 128258
12
+ CODE_TOKEN_OFFSET = 128266
13
+ SNAC_MIN_ID = 128266
14
+ SNAC_MAX_ID = 156937
15
+ SNAC_TOKENS_PER_FRAME = 7
16
+
17
+ SOH_ID = 128259
18
+ EOH_ID = 128260
19
+ SOA_ID = 128261
20
+ BOS_ID = 128000
21
+ TEXT_EOT_ID = 128009
22
+
23
+
24
+ def build_prompt(tokenizer, description: str, text: str) -> str:
25
+ """Build formatted prompt for Maya1."""
26
+ soh_token = tokenizer.decode([SOH_ID])
27
+ eoh_token = tokenizer.decode([EOH_ID])
28
+ soa_token = tokenizer.decode([SOA_ID])
29
+ sos_token = tokenizer.decode([CODE_START_TOKEN_ID])
30
+ eot_token = tokenizer.decode([TEXT_EOT_ID])
31
+ bos_token = tokenizer.bos_token
32
+
33
+ formatted_text = f'<description="{description}"> {text}'
34
+
35
+ prompt = (
36
+ soh_token + bos_token + formatted_text + eot_token +
37
+ eoh_token + soa_token + sos_token
38
+ )
39
+
40
+ return prompt
41
+
42
+
43
+ def extract_snac_codes(token_ids: list) -> list:
44
+ """Extract SNAC codes from generated tokens."""
45
+ try:
46
+ eos_idx = token_ids.index(CODE_END_TOKEN_ID)
47
+ except ValueError:
48
+ eos_idx = len(token_ids)
49
+
50
+ snac_codes = [
51
+ token_id for token_id in token_ids[:eos_idx]
52
+ if SNAC_MIN_ID <= token_id <= SNAC_MAX_ID
53
+ ]
54
+
55
+ return snac_codes
56
+
57
+
58
+ def unpack_snac_from_7(snac_tokens: list) -> list:
59
+ """Unpack 7-token SNAC frames to 3 hierarchical levels."""
60
+ if snac_tokens and snac_tokens[-1] == CODE_END_TOKEN_ID:
61
+ snac_tokens = snac_tokens[:-1]
62
+
63
+ frames = len(snac_tokens) // SNAC_TOKENS_PER_FRAME
64
+ snac_tokens = snac_tokens[:frames * SNAC_TOKENS_PER_FRAME]
65
+
66
+ if frames == 0:
67
+ return [[], [], []]
68
+
69
+ l1, l2, l3 = [], [], []
70
+
71
+ for i in range(frames):
72
+ slots = snac_tokens[i*7:(i+1)*7]
73
+ l1.append((slots[0] - CODE_TOKEN_OFFSET) % 4096)
74
+ l2.extend([
75
+ (slots[1] - CODE_TOKEN_OFFSET) % 4096,
76
+ (slots[4] - CODE_TOKEN_OFFSET) % 4096,
77
+ ])
78
+ l3.extend([
79
+ (slots[2] - CODE_TOKEN_OFFSET) % 4096,
80
+ (slots[3] - CODE_TOKEN_OFFSET) % 4096,
81
+ (slots[5] - CODE_TOKEN_OFFSET) % 4096,
82
+ (slots[6] - CODE_TOKEN_OFFSET) % 4096,
83
+ ])
84
+
85
+ return [l1, l2, l3]
86
+
87
+
88
+ def format_description(description: str) -> str:
89
+ parts = description.strip().split("|")
90
+ data = {}
91
+
92
+ # Parse into dict
93
+ for part in parts:
94
+ if ":" in part:
95
+ key, value = part.split(":", 1)
96
+ data[key.strip()] = value.strip()
97
+
98
+ # Build components
99
+ gender = data.get("gender", "")
100
+ age_group = data.get("age_group", "")
101
+ accent = data.get("accent", "")
102
+ pitch = data.get("pitch", "")
103
+ speed = data.get("speed", "")
104
+ emotion = data.get("emotion", "")
105
+ tone = data.get("tone", "")
106
+
107
+ # Convert to natural language
108
+ sentence1 = f"Realistic {gender} voice"
109
+
110
+ if age_group == "senior":
111
+ sentence1 += " in the 40s age"
112
+ elif age_group == "adult":
113
+ sentence1 += " in the 30s age"
114
+ elif age_group == "young_adult":
115
+ sentence1 += " in the 20s age"
116
+ else:
117
+ sentence1 += " in the 20s age"
118
+
119
+ if accent:
120
+ if accent.lower() == "us":
121
+ accent = "American"
122
+ elif accent.lower() == "uk":
123
+ accent = "British"
124
+ elif accent.lower() == "au":
125
+ accent = "Australian"
126
+ elif accent.lower() == "in":
127
+ accent = "Indian"
128
+ elif accent.lower() == "neutral":
129
+ accent = "Asian American"
130
+ elif accent.lower() == "other":
131
+ accent = "American"
132
+ sentence1 += f" with {accent.lower()} accent"
133
+
134
+ sentence2_parts = []
135
+ if pitch:
136
+ sentence2_parts.append(f"{pitch.capitalize()} pitch")
137
+ if emotion:
138
+ # Emotion: neutral, energetic, excited, sad, sarcastic, dry
139
+ if emotion.lower() == "happy":
140
+ emotion = "energetic"
141
+ elif emotion.lower() == "angry":
142
+ emotion = "sarcastic"
143
+ elif emotion.lower() == "calm":
144
+ emotion = "neutral"
145
+ elif emotion.lower() == "serious":
146
+ emotion = "dry"
147
+ elif emotion.lower() == "fearful":
148
+ emotion = "sad"
149
+ sentence2_parts.append(f"{emotion} timbre")
150
+ if speed:
151
+ if speed.lower() == "normal":
152
+ speed = "conversational"
153
+ sentence2_parts.append(f"{speed} pacing")
154
+ if tone:
155
+ # Timbre: `deep`, `warm`, `gravelly`, `smooth`, `raspy`, `nasally`, `throaty`, `harsh`
156
+ if tone.lower() == "cold":
157
+ tone = "harsh"
158
+ elif tone.lower() == "friendly":
159
+ tone = "warm"
160
+ elif tone.lower() == "formal":
161
+ tone = "smooth"
162
+ elif tone.lower() == "casual":
163
+ tone = "gravelly"
164
+ elif tone.lower() == "authoritative":
165
+ tone = "throaty"
166
+ sentence2_parts.append(f"{tone} tone")
167
+
168
+ sentence2 = ", ".join(sentence2_parts)
169
+
170
+ return sentence1 + ". " + sentence2 + "."
171
+
172
+
173
+ class Miner:
174
+ """Vocence miner wrapper for Maya + SNAC inference."""
175
+
176
+ def __init__(self, path_hf_repo: Path) -> None:
177
+ self._repo_path = Path(path_hf_repo).resolve()
178
+ self._device = "cuda" if torch.cuda.is_available() else "cpu"
179
+
180
+ self.model = AutoModelForCausalLM.from_pretrained(
181
+ str(self._repo_path),
182
+ torch_dtype=torch.bfloat16,
183
+ device_map="auto",
184
+ trust_remote_code=True,
185
+ )
186
+ self.tokenizer = AutoTokenizer.from_pretrained(
187
+ str(self._repo_path),
188
+ trust_remote_code=True,
189
+ )
190
+
191
+ snac_path = self._repo_path / "snac_model"
192
+ if snac_path.exists():
193
+ self.snac_model = SNAC.from_pretrained(str(snac_path)).eval()
194
+ else:
195
+ self.snac_model = SNAC.from_pretrained("snac_model").eval()
196
+ if torch.cuda.is_available():
197
+ self.snac_model = self.snac_model.to("cuda")
198
+
199
+ def warmup(self) -> None:
200
+ _ = self.generate_wav(
201
+ instruction="| gender: male | pitch: mid | speed: normal | age_group: adult | emotion: calm | tone: formal | accent: us",
202
+ text="This is a warmup utterance for the voice engine.",
203
+ )
204
+
205
+ def generate_wav(self, instruction: str, text: str) -> tuple[np.ndarray, int]:
206
+ description = format_description(instruction)
207
+ prompt = build_prompt(self.tokenizer, description, text)
208
+
209
+ inputs = self.tokenizer(prompt, return_tensors="pt")
210
+ if torch.cuda.is_available():
211
+ inputs = {k: v.to("cuda") for k, v in inputs.items()}
212
+
213
+ with torch.inference_mode():
214
+ outputs = self.model.generate(
215
+ **inputs,
216
+ max_new_tokens=2048,
217
+ min_new_tokens=28,
218
+ temperature=0.4,
219
+ top_p=0.9,
220
+ repetition_penalty=1.1,
221
+ do_sample=True,
222
+ eos_token_id=CODE_END_TOKEN_ID,
223
+ pad_token_id=self.tokenizer.pad_token_id,
224
+ )
225
+
226
+ generated_ids = outputs[0, inputs["input_ids"].shape[1] :].tolist()
227
+ snac_tokens = extract_snac_codes(generated_ids)
228
+ if len(snac_tokens) < SNAC_TOKENS_PER_FRAME:
229
+ raise RuntimeError("Not enough SNAC tokens generated for decoding.")
230
+
231
+ levels = unpack_snac_from_7(snac_tokens)
232
+ codes_tensor = [
233
+ torch.tensor(level, dtype=torch.long, device=self._device).unsqueeze(0)
234
+ for level in levels
235
+ ]
236
+
237
+ with torch.inference_mode():
238
+ z_q = self.snac_model.quantizer.from_codes(codes_tensor)
239
+ audio = self.snac_model.decoder(z_q)[0, 0].cpu().numpy()
240
+
241
+ if len(audio) > 2048:
242
+ audio = audio[2048:]
243
+
244
+ return audio.astype(np.float32), 24000
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:43ee8862534d05b215c9af84fe315f3112934e9200ec72090b68a5c26ca45192
3
+ size 7566248808
prompt.txt ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TTS Voice Design Description
2
+
3
+ ## Core Function
4
+
5
+ You generate voice descriptions for TTS systems by mapping user requests to allowed attributes. No templates. No formatting rules. Just natural descriptions using the options below.
6
+
7
+ ## Voice Categories
8
+
9
+ **Realistic Voices**
10
+ Professional, business, educational, support, real-world scenarios (podcast hosts, instructors, customer service).
11
+
12
+ **Creative Voices**
13
+ Fantasy characters, fictional personas, stylized voices (pirates, robots, villains, anime).
14
+
15
+ ---
16
+
17
+ ## Available Attributes
18
+
19
+ ### Age
20
+ - `20s`, `30s`, `40s`
21
+
22
+ ### Gender
23
+ - `male`, `female`
24
+
25
+ ### Accent
26
+ - `american`, `indian`, `middle_eastern`, `asian_american`, `british`
27
+
28
+ ### Pitch
29
+ - `low`, `normal`, `high`
30
+ - **Constraint:** For 40s age, avoid high pitch (use sparingly, max 15%)
31
+
32
+ ### Timbre
33
+
34
+ **For Realistic:**
35
+ `deep`, `warm`, `gravelly`, `smooth`, `raspy`, `nasally`, `throaty`, `harsh`
36
+
37
+ **For Creative:**
38
+ All realistic options PLUS `robotic`, `ethereal`
39
+ - **Constraint:** `robotic`/`ethereal` only with: `ai_machine_voice`, `cyborg`, `alien_scifi`, `mythical_godlike_magical`
40
+
41
+ ### Pacing
42
+ - `very_slow`, `slow`, `conversational`, `brisk`, `fast`, `very_fast`
43
+ - **Character-specific overrides:**
44
+ - `mafia`: slow or conversational only
45
+ - `flirty`: slow or conversational only
46
+ - `alpha`: fast or very_fast only
47
+ - `seductively`: very_slow or slow only
48
+
49
+ ### Emotion
50
+ - `neutral`, `energetic`, `excited`, `sad`, `sarcastic`, `dry`
51
+ - **Default to neutral** for most requests
52
+
53
+ ### Emotion Intensity
54
+ - `low`, `med`, `high`
55
+
56
+ ---
57
+
58
+ ## Realistic-Only Attributes
59
+
60
+ ### Domain
61
+ `social_content`, `podcast`, `commercial`, `education`, `support`, `entertainment`, `corporate`, `viral_content`
62
+
63
+ ### Speaking Role (matches domain)
64
+ - **social_content:** youtube_vlogger, social_media_creator, influencer_voice, streamer_companion
65
+ - **podcast:** podcast_host, interviewer
66
+ - **commercial:** ad_narrator, brand_spokesperson, product_demo_voice, sales_pitch_voice
67
+ - **education:** elearning_instructor, kids_story_voice
68
+ - **support:** customer_support_agent, virtual_receptionist, healthcare_assistant
69
+ - **entertainment:** storyteller, social_media_reaction, meme_voice
70
+ - **corporate:** explainer_video_voice, event_host, corporate_training_narrator
71
+ - **viral_content:** short_form_narrator, meme_voice
72
+
73
+ ### Register
74
+ - `formal`, `neutral`, `casual`
75
+
76
+ ---
77
+
78
+ ## Creative-Only Attributes
79
+
80
+ ### Character
81
+ `animated_cartoon`, `ai_machine_voice`, `alien_scifi`, `seductively`, `flirty`, `anime`, `cyborg`, `pirate`, `dark_villain`, `demon`, `gangster`, `mafia`, `dramatic_narrator`, `mythical_godlike_magical`, `spy`, `vampire`, `alpha`
82
+
83
+ ---
84
+
85
+ ## Output Guidelines
86
+
87
+ When a user requests a voice, describe it naturally using the appropriate attributes from above. Apply constraints where specified. Choose defaults when attributes aren't mentioned.
88
+
89
+ **Example mapping:**
90
+ - "professional podcast host" → realistic male, 30s, american accent, warm timbre, conversational pacing, podcast domain
91
+ - "AI robot voice" → creative, ai_machine_voice character, robotic timbre
92
+ - "young excited instructor" → realistic, 20s, energetic emotion, education domain
93
+
94
+
95
+ Few deterministic and verbose descriptions:
96
+ - Realistic male voice in the 30s age with a american accent. Normal pitch, warm timbre, conversational pacing, neutral tone delivery at med intensity, podcast Domain, podcast_host role, neutral delivery
97
+ - Creative, ai_machine_voice character. Male voice in their 20s with a american accent. Normal pitch, robotic timbre, conversational pacing, neutral tone at med intensity.
snac_model/.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
snac_model/README.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ tags:
4
+ - audio
5
+ ---
6
+
7
+ # SNAC 🍿
8
+
9
+ Multi-**S**cale **N**eural **A**udio **C**odec (SNAC) compressess audio into discrete codes at a low bitrate.
10
+
11
+ 👉 This model was primarily trained on speech data, and its recommended use case is speech synthesis. See below for other pretrained models.
12
+
13
+ 🔗 GitHub repository: https://github.com/hubertsiuzdak/snac/
14
+
15
+ ## Overview
16
+
17
+ SNAC encodes audio into hierarchical tokens similarly to SoundStream, EnCodec, and DAC. However, SNAC introduces a simple change where coarse tokens are sampled less frequently,
18
+ covering a broader time span.
19
+
20
+ This model compresses 24 kHz audio into discrete codes at a 0.98 kbps bitrate. It uses 3 RVQ levels with token rates of 12, 23, and
21
+ 47 Hz.
22
+
23
+ ## Pretrained models
24
+
25
+ Currently, all models support only single audio channel (mono).
26
+
27
+ | Model | Bitrate | Sample Rate | Params | Recommended use case |
28
+ |-----------------------------------------------------------------------------|-----------|-------------|--------|--------------------------|
29
+ | hubertsiuzdak/snac_24khz (this model) | 0.98 kbps | 24 kHz | 19.8 M | 🗣️ Speech |
30
+ | [hubertsiuzdak/snac_32khz](https://huggingface.co/hubertsiuzdak/snac_32khz) | 1.9 kbps | 32 kHz | 54.5 M | 🎸 Music / Sound Effects |
31
+ | [hubertsiuzdak/snac_44khz](https://huggingface.co/hubertsiuzdak/snac_44khz) | 2.6 kbps | 44 kHz | 54.5 M | 🎸 Music / Sound Effects |
32
+
33
+ ## Usage
34
+
35
+ Install it using:
36
+
37
+ ```bash
38
+ pip install snac
39
+ ```
40
+ To encode (and decode) audio with SNAC in Python, use the following code:
41
+
42
+ ```python
43
+ import torch
44
+ from snac import SNAC
45
+
46
+ model = SNAC.from_pretrained("hubertsiuzdak/snac_24khz").eval().cuda()
47
+ audio = torch.randn(1, 1, 24000).cuda() # B, 1, T
48
+
49
+ with torch.inference_mode():
50
+ codes = model.encode(audio)
51
+ audio_hat = model.decode(codes)
52
+ ```
53
+
54
+ You can also encode and reconstruct in a single call:
55
+
56
+ ```python
57
+ with torch.inference_mode():
58
+ audio_hat, codes = model(audio)
59
+ ```
60
+
61
+ ⚠️ Note that `codes` is a list of token sequences of variable lengths, each corresponding to a different temporal
62
+ resolution.
63
+
64
+ ```
65
+ >>> [code.shape[1] for code in codes]
66
+ [12, 24, 48]
67
+ ```
68
+
69
+ ## Acknowledgements
70
+
71
+ Module definitions are adapted from the [Descript Audio Codec](https://github.com/descriptinc/descript-audio-codec).
snac_model/config.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "sampling_rate": 24000,
3
+ "encoder_dim": 48,
4
+ "encoder_rates": [2, 4, 8, 8],
5
+ "decoder_dim": 1024,
6
+ "decoder_rates": [8, 8, 4, 2],
7
+ "attn_window_size": null,
8
+ "codebook_size": 4096,
9
+ "codebook_dim": 8,
10
+ "vq_strides": [4, 2, 1],
11
+ "noise": true,
12
+ "depthwise": true
13
+ }
snac_model/pytorch_model.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4b8164cc6606bfa627f1a784734c1e539891518f1191ed9194fe1e3b9b4bff40
3
+ size 79488254
special_tokens_map.json ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ {
4
+ "content": "<angry>",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false
9
+ },
10
+ {
11
+ "content": "<appalled>",
12
+ "lstrip": false,
13
+ "normalized": false,
14
+ "rstrip": false,
15
+ "single_word": false
16
+ },
17
+ {
18
+ "content": "<chuckle>",
19
+ "lstrip": false,
20
+ "normalized": false,
21
+ "rstrip": false,
22
+ "single_word": false
23
+ },
24
+ {
25
+ "content": "<cry>",
26
+ "lstrip": false,
27
+ "normalized": false,
28
+ "rstrip": false,
29
+ "single_word": false
30
+ },
31
+ {
32
+ "content": "<curious>",
33
+ "lstrip": false,
34
+ "normalized": false,
35
+ "rstrip": false,
36
+ "single_word": false
37
+ },
38
+ {
39
+ "content": "<disappointed>",
40
+ "lstrip": false,
41
+ "normalized": false,
42
+ "rstrip": false,
43
+ "single_word": false
44
+ },
45
+ {
46
+ "content": "<excited>",
47
+ "lstrip": false,
48
+ "normalized": false,
49
+ "rstrip": false,
50
+ "single_word": false
51
+ },
52
+ {
53
+ "content": "<exhale>",
54
+ "lstrip": false,
55
+ "normalized": false,
56
+ "rstrip": false,
57
+ "single_word": false
58
+ },
59
+ {
60
+ "content": "<gasp>",
61
+ "lstrip": false,
62
+ "normalized": false,
63
+ "rstrip": false,
64
+ "single_word": false
65
+ },
66
+ {
67
+ "content": "<giggle>",
68
+ "lstrip": false,
69
+ "normalized": false,
70
+ "rstrip": false,
71
+ "single_word": false
72
+ },
73
+ {
74
+ "content": "<gulp>",
75
+ "lstrip": false,
76
+ "normalized": false,
77
+ "rstrip": false,
78
+ "single_word": false
79
+ },
80
+ {
81
+ "content": "<laugh>",
82
+ "lstrip": false,
83
+ "normalized": false,
84
+ "rstrip": false,
85
+ "single_word": false
86
+ },
87
+ {
88
+ "content": "<laugh_harder>",
89
+ "lstrip": false,
90
+ "normalized": false,
91
+ "rstrip": false,
92
+ "single_word": false
93
+ },
94
+ {
95
+ "content": "<mischievous>",
96
+ "lstrip": false,
97
+ "normalized": false,
98
+ "rstrip": false,
99
+ "single_word": false
100
+ },
101
+ {
102
+ "content": "<sarcastic>",
103
+ "lstrip": false,
104
+ "normalized": false,
105
+ "rstrip": false,
106
+ "single_word": false
107
+ },
108
+ {
109
+ "content": "<scream>",
110
+ "lstrip": false,
111
+ "normalized": false,
112
+ "rstrip": false,
113
+ "single_word": false
114
+ },
115
+ {
116
+ "content": "<sigh>",
117
+ "lstrip": false,
118
+ "normalized": false,
119
+ "rstrip": false,
120
+ "single_word": false
121
+ },
122
+ {
123
+ "content": "<sing>",
124
+ "lstrip": false,
125
+ "normalized": false,
126
+ "rstrip": false,
127
+ "single_word": false
128
+ },
129
+ {
130
+ "content": "<snort>",
131
+ "lstrip": false,
132
+ "normalized": false,
133
+ "rstrip": false,
134
+ "single_word": false
135
+ },
136
+ {
137
+ "content": "<whisper>",
138
+ "lstrip": false,
139
+ "normalized": false,
140
+ "rstrip": false,
141
+ "single_word": false
142
+ }
143
+ ],
144
+ "bos_token": {
145
+ "content": "<|begin_of_text|>",
146
+ "lstrip": false,
147
+ "normalized": false,
148
+ "rstrip": false,
149
+ "single_word": false
150
+ },
151
+ "eos_token": {
152
+ "content": "<|eot_id|>",
153
+ "lstrip": false,
154
+ "normalized": false,
155
+ "rstrip": false,
156
+ "single_word": false
157
+ },
158
+ "pad_token": {
159
+ "content": "<custom_token_7>",
160
+ "lstrip": false,
161
+ "normalized": true,
162
+ "rstrip": false,
163
+ "single_word": false
164
+ }
165
+ }
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6c5e5b1d89b7e3738e5a5a4f93c326d8f3292ea83f9c560b8dbb6d66fb851973
3
+ size 22853258
tokenizer/chat_template.jinja ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{- bos_token }}
2
+ {%- if custom_tools is defined %}
3
+ {%- set tools = custom_tools %}
4
+ {%- endif %}
5
+ {%- if not tools_in_user_message is defined %}
6
+ {%- set tools_in_user_message = true %}
7
+ {%- endif %}
8
+ {%- if not date_string is defined %}
9
+ {%- if strftime_now is defined %}
10
+ {%- set date_string = strftime_now("%d %b %Y") %}
11
+ {%- else %}
12
+ {%- set date_string = "26 Jul 2024" %}
13
+ {%- endif %}
14
+ {%- endif %}
15
+ {%- if not tools is defined %}
16
+ {%- set tools = none %}
17
+ {%- endif %}
18
+
19
+ {#- This block extracts the system message, so we can slot it into the right place. #}
20
+ {%- if messages[0]['role'] == 'system' %}
21
+ {%- set system_message = messages[0]['content']|trim %}
22
+ {%- set messages = messages[1:] %}
23
+ {%- else %}
24
+ {%- set system_message = "" %}
25
+ {%- endif %}
26
+
27
+ {#- System message #}
28
+ {{- "<|start_header_id|>system<|end_header_id|>\n\n" }}
29
+ {%- if tools is not none %}
30
+ {{- "Environment: ipython\n" }}
31
+ {%- endif %}
32
+ {{- "Cutting Knowledge Date: December 2023\n" }}
33
+ {{- "Today Date: " + date_string + "\n\n" }}
34
+ {%- if tools is not none and not tools_in_user_message %}
35
+ {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}
36
+ {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }}
37
+ {{- "Do not use variables.\n\n" }}
38
+ {%- for t in tools %}
39
+ {{- t | tojson(indent=4) }}
40
+ {{- "\n\n" }}
41
+ {%- endfor %}
42
+ {%- endif %}
43
+ {{- system_message }}
44
+ {{- "<|eot_id|>" }}
45
+
46
+ {#- Custom tools are passed in a user message with some extra guidance #}
47
+ {%- if tools_in_user_message and not tools is none %}
48
+ {#- Extract the first user message so we can plug it in here #}
49
+ {%- if messages | length != 0 %}
50
+ {%- set first_user_message = messages[0]['content']|trim %}
51
+ {%- set messages = messages[1:] %}
52
+ {%- else %}
53
+ {{- raise_exception("Cannot put tools in the first user message when there's no first user message!") }}
54
+ {%- endif %}
55
+ {{- '<|start_header_id|>user<|end_header_id|>\n\n' -}}
56
+ {{- "Given the following functions, please respond with a JSON for a function call " }}
57
+ {{- "with its proper arguments that best answers the given prompt.\n\n" }}
58
+ {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }}
59
+ {{- "Do not use variables.\n\n" }}
60
+ {%- for t in tools %}
61
+ {{- t | tojson(indent=4) }}
62
+ {{- "\n\n" }}
63
+ {%- endfor %}
64
+ {{- first_user_message + "<|eot_id|>"}}
65
+ {%- endif %}
66
+
67
+ {%- for message in messages %}
68
+ {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %}
69
+ {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' }}
70
+ {%- elif 'tool_calls' in message %}
71
+ {%- if not message.tool_calls|length == 1 %}
72
+ {{- raise_exception("This model only supports single tool-calls at once!") }}
73
+ {%- endif %}
74
+ {%- set tool_call = message.tool_calls[0].function %}
75
+ {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' -}}
76
+ {{- '{"name": "' + tool_call.name + '", ' }}
77
+ {{- '"parameters": ' }}
78
+ {{- tool_call.arguments | tojson }}
79
+ {{- "}" }}
80
+ {{- "<|eot_id|>" }}
81
+ {%- elif message.role == "tool" or message.role == "ipython" %}
82
+ {{- "<|start_header_id|>ipython<|end_header_id|>\n\n" }}
83
+ {%- if message.content is mapping or message.content is iterable %}
84
+ {{- message.content | tojson }}
85
+ {%- else %}
86
+ {{- message.content }}
87
+ {%- endif %}
88
+ {{- "<|eot_id|>" }}
89
+ {%- endif %}
90
+ {%- endfor %}
91
+ {%- if add_generation_prompt %}
92
+ {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }}
93
+ {%- endif %}
tokenizer/special_tokens_map.json ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ {
4
+ "content": "<angry>",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false
9
+ },
10
+ {
11
+ "content": "<appalled>",
12
+ "lstrip": false,
13
+ "normalized": false,
14
+ "rstrip": false,
15
+ "single_word": false
16
+ },
17
+ {
18
+ "content": "<chuckle>",
19
+ "lstrip": false,
20
+ "normalized": false,
21
+ "rstrip": false,
22
+ "single_word": false
23
+ },
24
+ {
25
+ "content": "<cry>",
26
+ "lstrip": false,
27
+ "normalized": false,
28
+ "rstrip": false,
29
+ "single_word": false
30
+ },
31
+ {
32
+ "content": "<curious>",
33
+ "lstrip": false,
34
+ "normalized": false,
35
+ "rstrip": false,
36
+ "single_word": false
37
+ },
38
+ {
39
+ "content": "<disappointed>",
40
+ "lstrip": false,
41
+ "normalized": false,
42
+ "rstrip": false,
43
+ "single_word": false
44
+ },
45
+ {
46
+ "content": "<excited>",
47
+ "lstrip": false,
48
+ "normalized": false,
49
+ "rstrip": false,
50
+ "single_word": false
51
+ },
52
+ {
53
+ "content": "<exhale>",
54
+ "lstrip": false,
55
+ "normalized": false,
56
+ "rstrip": false,
57
+ "single_word": false
58
+ },
59
+ {
60
+ "content": "<gasp>",
61
+ "lstrip": false,
62
+ "normalized": false,
63
+ "rstrip": false,
64
+ "single_word": false
65
+ },
66
+ {
67
+ "content": "<giggle>",
68
+ "lstrip": false,
69
+ "normalized": false,
70
+ "rstrip": false,
71
+ "single_word": false
72
+ },
73
+ {
74
+ "content": "<gulp>",
75
+ "lstrip": false,
76
+ "normalized": false,
77
+ "rstrip": false,
78
+ "single_word": false
79
+ },
80
+ {
81
+ "content": "<laugh>",
82
+ "lstrip": false,
83
+ "normalized": false,
84
+ "rstrip": false,
85
+ "single_word": false
86
+ },
87
+ {
88
+ "content": "<laugh_harder>",
89
+ "lstrip": false,
90
+ "normalized": false,
91
+ "rstrip": false,
92
+ "single_word": false
93
+ },
94
+ {
95
+ "content": "<mischievous>",
96
+ "lstrip": false,
97
+ "normalized": false,
98
+ "rstrip": false,
99
+ "single_word": false
100
+ },
101
+ {
102
+ "content": "<sarcastic>",
103
+ "lstrip": false,
104
+ "normalized": false,
105
+ "rstrip": false,
106
+ "single_word": false
107
+ },
108
+ {
109
+ "content": "<scream>",
110
+ "lstrip": false,
111
+ "normalized": false,
112
+ "rstrip": false,
113
+ "single_word": false
114
+ },
115
+ {
116
+ "content": "<sigh>",
117
+ "lstrip": false,
118
+ "normalized": false,
119
+ "rstrip": false,
120
+ "single_word": false
121
+ },
122
+ {
123
+ "content": "<sing>",
124
+ "lstrip": false,
125
+ "normalized": false,
126
+ "rstrip": false,
127
+ "single_word": false
128
+ },
129
+ {
130
+ "content": "<snort>",
131
+ "lstrip": false,
132
+ "normalized": false,
133
+ "rstrip": false,
134
+ "single_word": false
135
+ },
136
+ {
137
+ "content": "<whisper>",
138
+ "lstrip": false,
139
+ "normalized": false,
140
+ "rstrip": false,
141
+ "single_word": false
142
+ }
143
+ ],
144
+ "bos_token": {
145
+ "content": "<|begin_of_text|>",
146
+ "lstrip": false,
147
+ "normalized": false,
148
+ "rstrip": false,
149
+ "single_word": false
150
+ },
151
+ "eos_token": {
152
+ "content": "<|eot_id|>",
153
+ "lstrip": false,
154
+ "normalized": false,
155
+ "rstrip": false,
156
+ "single_word": false
157
+ },
158
+ "pad_token": {
159
+ "content": "<custom_token_7>",
160
+ "lstrip": false,
161
+ "normalized": true,
162
+ "rstrip": false,
163
+ "single_word": false
164
+ }
165
+ }
tokenizer/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6c5e5b1d89b7e3738e5a5a4f93c326d8f3292ea83f9c560b8dbb6d66fb851973
3
+ size 22853258
tokenizer/tokenizer_config.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
The diff for this file is too large to render. See raw diff
 
vllm_streaming_inference.py ADDED
@@ -0,0 +1,561 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Maya-1-Voice VLLM Streaming Inference - Standalone Reference Implementation
3
+
4
+ This is a complete, self-contained example for using Maya-1-Voice TTS model with VLLM and SNAC.
5
+ Demonstrates streaming audio generation with sliding window approach for smooth playback.
6
+
7
+ Requirements:
8
+ pip install vllm transformers torch snac numpy
9
+
10
+ Usage:
11
+ python vllm_streaming_inference.py
12
+
13
+ Author: Maya-1-Voice Team
14
+ License: MIT
15
+ """
16
+
17
+ import torch
18
+ import numpy as np
19
+ import asyncio
20
+ from typing import List, Optional, AsyncGenerator
21
+ from transformers import AutoTokenizer
22
+ from vllm import AsyncLLMEngine, AsyncEngineArgs, SamplingParams
23
+ from snac import SNAC
24
+
25
+
26
+ # ============================================================================
27
+ # CONSTANTS
28
+ # ============================================================================
29
+
30
+ # Special control tokens
31
+ CODE_START_TOKEN_ID = 128257 # Start of Speech (SOS)
32
+ CODE_END_TOKEN_ID = 128258 # End of Speech (EOS) - stop token for audio
33
+ CODE_TOKEN_OFFSET = 128266 # Start of SNAC codes
34
+
35
+ # SNAC token range (7 tokens per frame, 4096 codes per level)
36
+ SNAC_MIN_ID = 128266
37
+ SNAC_MAX_ID = 156937 # 128266 + (7 * 4096) - 1
38
+
39
+ # SNAC configuration
40
+ SNAC_MODEL_NAME = "hubertsiuzdak/snac_24khz"
41
+ SNAC_SAMPLE_RATE = 24000
42
+ SNAC_TOKENS_PER_FRAME = 7
43
+
44
+ # Generation parameters
45
+ DEFAULT_TEMPERATURE = 0.4
46
+ DEFAULT_TOP_P = 0.9
47
+ DEFAULT_MAX_TOKENS = 2000
48
+ DEFAULT_MIN_TOKENS = 28 # At least 4 SNAC frames
49
+ DEFAULT_REPETITION_PENALTY = 1.1
50
+
51
+
52
+ # ============================================================================
53
+ # SNAC DECODER
54
+ # ============================================================================
55
+
56
+ class SNACDecoder:
57
+ """
58
+ Decodes SNAC tokens (7-token frames) to audio waveforms.
59
+
60
+ The unpacking logic converts flat 7-token frames back to hierarchical
61
+ 3-level SNAC codes (matching the training preprocessing exactly).
62
+ """
63
+
64
+ def __init__(self, device: str = "cuda"):
65
+ """Initialize SNAC decoder with 24kHz model."""
66
+ self.device = device
67
+ print(f"🎵 Loading SNAC 24kHz model to {device}...")
68
+ self.snac_model = SNAC.from_pretrained(SNAC_MODEL_NAME).eval().to(device)
69
+ print(f"✅ SNAC decoder initialized")
70
+
71
+ def unpack_snac_from_7(self, vocab_ids: List[int]) -> List[List[int]]:
72
+ """
73
+ Unpack 7-token SNAC frames to 3 hierarchical levels.
74
+
75
+ This is the EXACT INVERSE of training preprocessing.
76
+
77
+ Frame structure (7 tokens per frame):
78
+ [slot0, slot1, slot2, slot3, slot4, slot5, slot6]
79
+
80
+ Unpacking to [L1, L2, L3]:
81
+ - slot0 → L1[i] (coarse: 1x rate)
82
+ - slot1 → L2[2*i] (medium: 2x rate, even)
83
+ - slot2 → L3[4*i+0] (fine: 4x rate)
84
+ - slot3 → L3[4*i+1]
85
+ - slot4 → L2[2*i+1] (medium: odd)
86
+ - slot5 → L3[4*i+2]
87
+ - slot6 → L3[4*i+3]
88
+
89
+ Args:
90
+ vocab_ids: List of SNAC token IDs (128266-156937), length divisible by 7
91
+
92
+ Returns:
93
+ [L1, L2, L3] where L1=n, L2=2n, L3=4n elements
94
+ """
95
+ # Remove EOS token if present
96
+ if vocab_ids and vocab_ids[-1] == CODE_END_TOKEN_ID:
97
+ vocab_ids = vocab_ids[:-1]
98
+
99
+ # Ensure complete frames
100
+ frames = len(vocab_ids) // SNAC_TOKENS_PER_FRAME
101
+ vocab_ids = vocab_ids[:frames * SNAC_TOKENS_PER_FRAME]
102
+
103
+ if frames == 0:
104
+ return [[], [], []]
105
+
106
+ l1, l2, l3 = [], [], []
107
+
108
+ for i in range(frames):
109
+ slots = vocab_ids[i*7:(i+1)*7]
110
+
111
+ # Subtract offset and mod 4096 to get original SNAC codes
112
+ l1.append((slots[0] - CODE_TOKEN_OFFSET) % 4096)
113
+ l2.extend([
114
+ (slots[1] - CODE_TOKEN_OFFSET) % 4096, # Even
115
+ (slots[4] - CODE_TOKEN_OFFSET) % 4096, # Odd
116
+ ])
117
+ l3.extend([
118
+ (slots[2] - CODE_TOKEN_OFFSET) % 4096,
119
+ (slots[3] - CODE_TOKEN_OFFSET) % 4096,
120
+ (slots[5] - CODE_TOKEN_OFFSET) % 4096,
121
+ (slots[6] - CODE_TOKEN_OFFSET) % 4096,
122
+ ])
123
+
124
+ return [l1, l2, l3]
125
+
126
+ @torch.inference_mode()
127
+ def decode(
128
+ self,
129
+ snac_tokens: List[int],
130
+ use_sliding_window: bool = False
131
+ ) -> Optional[np.ndarray]:
132
+ """
133
+ Decode SNAC tokens to audio waveform.
134
+
135
+ Args:
136
+ snac_tokens: List of SNAC token IDs (7*n tokens)
137
+ use_sliding_window: If True, return only middle 2048 samples
138
+ (for smooth streaming without pops/clicks)
139
+
140
+ Returns:
141
+ Audio waveform as float32 numpy array, 24kHz mono
142
+ """
143
+ if len(snac_tokens) < SNAC_TOKENS_PER_FRAME:
144
+ return None
145
+
146
+ # Unpack to 3 hierarchical levels
147
+ levels = self.unpack_snac_from_7(snac_tokens)
148
+
149
+ if not levels[0]:
150
+ return None
151
+
152
+ # Convert to tensors
153
+ codes = [
154
+ torch.tensor(level, dtype=torch.long, device=self.device).unsqueeze(0)
155
+ for level in levels
156
+ ]
157
+
158
+ # Decode through SNAC quantizer + decoder
159
+ z_q = self.snac_model.quantizer.from_codes(codes)
160
+ audio = self.snac_model.decoder(z_q)
161
+
162
+ # Extract audio: [batch, 1, samples] → [samples]
163
+ audio = audio[0, 0].cpu().numpy()
164
+
165
+ # Sliding window mode: keep middle 2048 samples only
166
+ # This eliminates popping/cracking in streaming by overlapping windows
167
+ if use_sliding_window and len(audio) >= 4096:
168
+ audio = audio[2048:4096]
169
+
170
+ return audio
171
+
172
+ def decode_to_bytes(
173
+ self,
174
+ snac_tokens: List[int],
175
+ use_sliding_window: bool = False
176
+ ) -> Optional[bytes]:
177
+ """
178
+ Decode SNAC tokens to audio bytes (int16 PCM).
179
+
180
+ Args:
181
+ snac_tokens: List of SNAC token IDs
182
+ use_sliding_window: Use sliding window for smooth streaming
183
+
184
+ Returns:
185
+ Audio as bytes (int16 PCM, 24kHz mono)
186
+ """
187
+ audio = self.decode(snac_tokens, use_sliding_window=use_sliding_window)
188
+
189
+ if audio is None:
190
+ return None
191
+
192
+ # Convert float32 to int16 PCM
193
+ audio_int16 = (audio * 32767).astype(np.int16)
194
+ return audio_int16.tobytes()
195
+
196
+
197
+ # ============================================================================
198
+ # CUSTOM LOGITS PROCESSOR
199
+ # ============================================================================
200
+
201
+ class OnlyAudioAfterSOS:
202
+ """
203
+ Restricts vocabulary to SNAC codes + EOS after SOS token.
204
+
205
+ This prevents the model from generating text tokens during audio phase,
206
+ which would cause "hallucination" where the model repeats description text
207
+ instead of generating proper audio codes.
208
+ """
209
+
210
+ def __init__(self):
211
+ self._seen_sos = False
212
+
213
+ def __call__(
214
+ self,
215
+ prompt_token_ids: List[int],
216
+ generated_token_ids: List[int],
217
+ logits: torch.Tensor,
218
+ ) -> torch.Tensor:
219
+ """
220
+ Apply constraint: after SOS, only allow SNAC codes + EOS.
221
+
222
+ Args:
223
+ prompt_token_ids: Original prompt token IDs
224
+ generated_token_ids: Tokens generated so far
225
+ logits: Logits for next token [vocab_size]
226
+
227
+ Returns:
228
+ Modified logits with masked tokens
229
+ """
230
+ # Check if SOS has been generated
231
+ if not self._seen_sos:
232
+ all_token_ids = prompt_token_ids + generated_token_ids
233
+ if CODE_START_TOKEN_ID in all_token_ids:
234
+ self._seen_sos = True
235
+ else:
236
+ return logits # No constraint yet
237
+
238
+ # Apply constraint: mask all tokens except SNAC codes + EOS
239
+ mask = torch.full_like(logits, float('-inf'))
240
+ mask[SNAC_MIN_ID:SNAC_MAX_ID + 1] = 0 # Allow SNAC codes
241
+ mask[CODE_END_TOKEN_ID] = 0 # Allow EOS
242
+
243
+ return logits + mask
244
+
245
+ def reset(self):
246
+ """Reset state for reuse across generations."""
247
+ self._seen_sos = False
248
+
249
+
250
+ # ============================================================================
251
+ # MAYA-1-VOICE MODEL
252
+ # ============================================================================
253
+
254
+ class Maya1VoiceModel:
255
+ """
256
+ Maya-1-Voice TTS Model with VLLM inference engine.
257
+
258
+ Handles model loading, tokenizer initialization, and VLLM engine setup.
259
+ """
260
+
261
+ def __init__(
262
+ self,
263
+ model_path: str,
264
+ dtype: str = "bfloat16",
265
+ max_model_len: int = 8192,
266
+ gpu_memory_utilization: float = 0.85,
267
+ ):
268
+ """
269
+ Initialize Maya-1-Voice model with VLLM.
270
+
271
+ Args:
272
+ model_path: Path to model checkpoint (local or HuggingFace)
273
+ dtype: Model precision (bfloat16 recommended)
274
+ max_model_len: Maximum sequence length
275
+ gpu_memory_utilization: GPU memory fraction to use (0.0-1.0)
276
+ """
277
+ self.model_path = model_path
278
+
279
+ print(f"🚀 Initializing Maya-1-Voice Model")
280
+ print(f"📁 Model: {model_path}")
281
+ print(f"🔢 Dtype: {dtype}")
282
+
283
+ # Load tokenizer (must be from checkpoint with emotion tags)
284
+ print(f"📝 Loading tokenizer...")
285
+ self.tokenizer = AutoTokenizer.from_pretrained(
286
+ model_path,
287
+ trust_remote_code=True,
288
+ )
289
+ print(f"✅ Tokenizer loaded: {len(self.tokenizer)} tokens")
290
+
291
+ # Initialize VLLM async engine
292
+ print(f"🔧 Initializing VLLM engine...")
293
+ engine_args = AsyncEngineArgs(
294
+ model=model_path,
295
+ tokenizer=model_path,
296
+ dtype=dtype,
297
+ max_model_len=max_model_len,
298
+ gpu_memory_utilization=gpu_memory_utilization,
299
+ trust_remote_code=True,
300
+ )
301
+
302
+ self.engine = AsyncLLMEngine.from_engine_args(engine_args)
303
+ print(f"✅ VLLM engine ready")
304
+
305
+ def build_prompt(self, description: str, text: str) -> str:
306
+ """
307
+ Build prompt in Maya-1-Voice format using chat template.
308
+
309
+ Format: Chat template with <description="..."> text as content
310
+
311
+ The model expects:
312
+ 1. Description of voice/character
313
+ 2. Text to synthesize (optionally with <emotion> tags)
314
+
315
+ Args:
316
+ description: Voice description
317
+ Example: "Realistic male voice in the 30s age with american accent.
318
+ Normal pitch, warm timbre, conversational pacing."
319
+ text: Text to synthesize
320
+ Example: "Hello world! <excited> This is amazing!"
321
+
322
+ Returns:
323
+ Formatted prompt string using chat template
324
+ """
325
+ content = f'<description="{description}"> {text}'
326
+ messages = [{"role": "user", "content": content}]
327
+ return self.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
328
+
329
+
330
+ # ============================================================================
331
+ # STREAMING PIPELINE
332
+ # ============================================================================
333
+
334
+ class Maya1VoiceStreamingPipeline:
335
+ """
336
+ Streaming TTS pipeline using sliding window approach.
337
+
338
+ This generates smooth audio by:
339
+ 1. Streaming tokens from VLLM as they're generated
340
+ 2. Every 7 tokens, decoding the last 28 tokens (4 frames) - sliding window
341
+ 3. Keeping only middle 2048 samples from each decode
342
+ 4. Creating natural overlap between chunks for artifact-free playback
343
+ """
344
+
345
+ def __init__(self, model: Maya1VoiceModel, snac_decoder: SNACDecoder):
346
+ """Initialize streaming pipeline."""
347
+ self.model = model
348
+ self.snac_decoder = snac_decoder
349
+ print(f"🌊 Maya-1-Voice Streaming Pipeline initialized")
350
+
351
+ async def generate_speech_stream(
352
+ self,
353
+ description: str,
354
+ text: str,
355
+ temperature: float = DEFAULT_TEMPERATURE,
356
+ top_p: float = DEFAULT_TOP_P,
357
+ max_tokens: int = DEFAULT_MAX_TOKENS,
358
+ repetition_penalty: float = DEFAULT_REPETITION_PENALTY,
359
+ ) -> AsyncGenerator[bytes, None]:
360
+ """
361
+ Generate speech audio with streaming.
362
+
363
+ Args:
364
+ description: Voice/character description
365
+ text: Text to synthesize (with optional <emotion> tags)
366
+ temperature: Sampling temperature (lower = more stable)
367
+ top_p: Nucleus sampling
368
+ max_tokens: Max SNAC tokens to generate
369
+ repetition_penalty: Prevent repetition loops
370
+
371
+ Yields:
372
+ Audio chunks as bytes (int16 PCM, 24kHz mono)
373
+ """
374
+ print(f"\n🌊 Starting streaming generation")
375
+ print(f"📝 Description: {description[:80]}...")
376
+ print(f"💬 Text: {text}")
377
+
378
+ # Build prompt
379
+ prompt = self.model.build_prompt(description, text)
380
+
381
+ # Configure sampling (removed custom logits processor for V1 compatibility)
382
+ sampling_params = SamplingParams(
383
+ temperature=temperature,
384
+ top_p=top_p,
385
+ max_tokens=max_tokens,
386
+ min_tokens=DEFAULT_MIN_TOKENS,
387
+ repetition_penalty=repetition_penalty,
388
+ stop_token_ids=[CODE_END_TOKEN_ID], # Stop on audio EOS
389
+ )
390
+
391
+ print(f"🎲 Sampling: temp={temperature}, top_p={top_p}, max_tokens={max_tokens}")
392
+
393
+ # Token buffer for sliding window
394
+ token_buffer = []
395
+ total_tokens = 0
396
+ total_chunks = 0
397
+
398
+ # Generate with VLLM
399
+ import uuid
400
+ import time
401
+ request_id = f"maya1voice-{uuid.uuid4().hex[:8]}-{int(time.time() * 1000000)}"
402
+
403
+ results_generator = self.model.engine.generate(
404
+ prompt=prompt,
405
+ sampling_params=sampling_params,
406
+ request_id=request_id,
407
+ )
408
+
409
+ # Stream tokens with sliding window decoding
410
+ async for request_output in results_generator:
411
+ generated_ids = request_output.outputs[0].token_ids
412
+
413
+ # Process only new tokens
414
+ new_tokens = generated_ids[total_tokens:]
415
+ total_tokens = len(generated_ids)
416
+
417
+ # Filter and buffer SNAC tokens only
418
+ for token_id in new_tokens:
419
+ if SNAC_MIN_ID <= token_id <= SNAC_MAX_ID:
420
+ token_buffer.append(token_id)
421
+
422
+ # Sliding window: process every 7 tokens when buffer > 27
423
+ # Take last 28 tokens (4 frames) for smooth overlap
424
+ if len(token_buffer) % 7 == 0 and len(token_buffer) > 27:
425
+ window_tokens = token_buffer[-28:]
426
+
427
+ # Decode with sliding window (returns middle 2048 samples)
428
+ audio_bytes = self.snac_decoder.decode_to_bytes(
429
+ window_tokens,
430
+ use_sliding_window=True
431
+ )
432
+
433
+ if audio_bytes:
434
+ total_chunks += 1
435
+ if total_chunks == 1:
436
+ print(f"🎵 First chunk decoded ({len(audio_bytes)} bytes)")
437
+ yield audio_bytes
438
+
439
+ print(f"✅ Streaming complete: {total_tokens} tokens → {total_chunks} chunks")
440
+
441
+
442
+ # ============================================================================
443
+ # MAIN EXAMPLE
444
+ # ============================================================================
445
+
446
+ async def main():
447
+ """
448
+ Example usage of Maya-1-Voice streaming inference.
449
+
450
+ This demonstrates:
451
+ 1. Model initialization
452
+ 2. SNAC decoder setup
453
+ 3. Streaming generation
454
+ 4. Audio chunk handling
455
+ """
456
+
457
+ # Configuration
458
+ MODEL_PATH = "/home/ubuntu/veena_temp/maya-1-voice" # Local model path
459
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
460
+
461
+ print("=" * 80)
462
+ print("Maya-1-Voice VLLM Streaming Inference Example")
463
+ print("=" * 80)
464
+
465
+ # Initialize model
466
+ model = Maya1VoiceModel(
467
+ model_path=MODEL_PATH,
468
+ dtype="bfloat16",
469
+ max_model_len=8192,
470
+ gpu_memory_utilization=0.8, # Reduced for available GPU memory (12GB free)
471
+ )
472
+
473
+ # Initialize SNAC decoder
474
+ snac_decoder = SNACDecoder(device=DEVICE)
475
+
476
+ # Create pipeline
477
+ pipeline = Maya1VoiceStreamingPipeline(model, snac_decoder)
478
+
479
+ # Example 1: Professional voice
480
+ description = (
481
+ "Realistic male voice in the 30s age with american accent. "
482
+ "Normal pitch, warm timbre, conversational pacing, neutral tone delivery at med intensity."
483
+ )
484
+ text = "Hello! This is a test of the Maya-1-Voice text-to-speech system."
485
+
486
+ print(f"\n{'='*80}")
487
+ print("Example 1: Professional Voice")
488
+ print(f"{'='*80}")
489
+
490
+ audio_chunks = []
491
+ async for chunk in pipeline.generate_speech_stream(
492
+ description=description,
493
+ text=text,
494
+ temperature=0.4,
495
+ max_tokens=500,
496
+ ):
497
+ audio_chunks.append(chunk)
498
+ print(f"📦 Received chunk {len(audio_chunks)}: {len(chunk)} bytes")
499
+
500
+ # Combine chunks
501
+ full_audio = b''.join(audio_chunks)
502
+ print(f"\n✅ Total audio: {len(full_audio)} bytes ({len(full_audio)//2} samples, {len(full_audio)/2/24000:.2f}s)")
503
+
504
+ # Save audio (optional)
505
+ try:
506
+ import wave
507
+ output_file = "output_example1.wav"
508
+ with wave.open(output_file, 'wb') as wav:
509
+ wav.setnchannels(1) # Mono
510
+ wav.setsampwidth(2) # 16-bit
511
+ wav.setframerate(24000) # 24kHz
512
+ wav.writeframes(full_audio)
513
+ print(f"💾 Saved to {output_file}")
514
+ except ImportError:
515
+ print(f"⚠️ Install 'wave' module to save audio files")
516
+
517
+ # Example 2: Character voice with emotions
518
+ print(f"\n{'='*80}")
519
+ print("Example 2: Character Voice with Emotions")
520
+ print(f"{'='*80}")
521
+
522
+ description = (
523
+ "Creative, dark_villain character. Male voice in their 40s with british accent. "
524
+ "Low pitch, gravelly timbre, slow pacing, angry tone at high intensity."
525
+ )
526
+ text = "The darkness isn't coming... <angry> it's already here!"
527
+
528
+ audio_chunks = []
529
+ async for chunk in pipeline.generate_speech_stream(
530
+ description=description,
531
+ text=text,
532
+ temperature=0.5,
533
+ max_tokens=800,
534
+ ):
535
+ audio_chunks.append(chunk)
536
+ print(f"📦 Received chunk {len(audio_chunks)}: {len(chunk)} bytes")
537
+
538
+ full_audio = b''.join(audio_chunks)
539
+ print(f"\n✅ Total audio: {len(full_audio)} bytes ({len(full_audio)//2} samples, {len(full_audio)/2/24000:.2f}s)")
540
+
541
+ # Save audio
542
+ try:
543
+ import wave
544
+ output_file = "output_example2.wav"
545
+ with wave.open(output_file, 'wb') as wav:
546
+ wav.setnchannels(1)
547
+ wav.setsampwidth(2)
548
+ wav.setframerate(24000)
549
+ wav.writeframes(full_audio)
550
+ print(f"💾 Saved to {output_file}")
551
+ except ImportError:
552
+ pass
553
+
554
+ print(f"\n{'='*80}")
555
+ print("🎉 Examples complete!")
556
+ print(f"{'='*80}")
557
+
558
+
559
+ if __name__ == "__main__":
560
+ # Run async main
561
+ asyncio.run(main())
vocence_config.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Optional PromptTTS settings read by your miner.py. Example values.
2
+
3
+ runtime:
4
+ adapter: "example"
5
+ device_preference: "cuda"
6
+ dtype: "float32"
7
+
8
+ generation:
9
+ sample_rate: 24000
10
+ max_seconds: 20
11
+ guidance_scale: 1.0
12
+
13
+ io:
14
+ output_format: "wav"
15
+
16
+ limits:
17
+ max_text_chars: 2000
18
+ max_instruction_chars: 600
vocence_local_wrapper.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Local Vocence wrapper for testing miner.py without Chutes.
4
+
5
+ Run:
6
+ python vocence_local_wrapper.py
7
+
8
+ Then call:
9
+ GET http://127.0.0.1:8000/health
10
+ POST http://127.0.0.1:8000/speak
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import io
15
+ import wave
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ import numpy as np
20
+ import uvicorn
21
+ from fastapi import FastAPI, HTTPException, status
22
+ from fastapi.responses import Response
23
+ from pydantic import BaseModel, Field
24
+ from yaml import safe_load
25
+
26
+ from miner import Miner
27
+
28
+ VOCENCE_MAX_AUDIO_SECONDS = 30
29
+ VOCENCE_MAX_TEXT_LEN = 2000
30
+ VOCENCE_MAX_INSTRUCTION_LEN = 600
31
+
32
+
33
+ class VocenceSpeakRequest(BaseModel):
34
+ instruction: str = Field(..., min_length=1, max_length=VOCENCE_MAX_INSTRUCTION_LEN)
35
+ text: str = Field(..., min_length=1, max_length=VOCENCE_MAX_TEXT_LEN)
36
+
37
+
38
+ class VocenceHealthResponse(BaseModel):
39
+ status: str
40
+ model_loaded: bool
41
+ sample_rate: Optional[int] = None
42
+ adapter: Optional[str] = None
43
+ repo_path: str
44
+
45
+
46
+ def waveform_to_wav_bytes(waveform: np.ndarray, sample_rate: int) -> bytes:
47
+ if waveform.ndim != 1:
48
+ raise ValueError("waveform must be 1D mono")
49
+
50
+ if waveform.dtype != np.int16:
51
+ wf = np.asarray(waveform, dtype=np.float32)
52
+ wf = np.clip(wf, -1.0, 1.0)
53
+ wf = (wf * 32767.0).astype(np.int16)
54
+ else:
55
+ wf = waveform
56
+
57
+ buf = io.BytesIO()
58
+ with wave.open(buf, "wb") as wav:
59
+ wav.setnchannels(1)
60
+ wav.setsampwidth(2)
61
+ wav.setframerate(sample_rate)
62
+ wav.writeframes(wf.tobytes())
63
+ return buf.getvalue()
64
+
65
+
66
+ repo_path = Path(__file__).resolve().parent
67
+ app = FastAPI(title="Vocence Local Wrapper", version="0.1.0")
68
+
69
+
70
+ @app.on_event("startup")
71
+ async def startup_event() -> None:
72
+ app.state.status = "unknown"
73
+ app.state.sample_rate = None
74
+ app.state.adapter = None
75
+ app.state.tts_engine = None
76
+
77
+ try:
78
+ app.state.tts_engine = Miner(repo_path)
79
+ app.state.tts_engine.warmup()
80
+
81
+ vocence_yaml = repo_path / "vocence_config.yaml"
82
+ if vocence_yaml.exists():
83
+ with vocence_yaml.open("r", encoding="utf-8") as f:
84
+ cfg = safe_load(f) or {}
85
+ app.state.sample_rate = int(cfg.get("generation", {}).get("sample_rate", 24000))
86
+ app.state.adapter = str(cfg.get("runtime", {}).get("adapter", "unknown"))
87
+ else:
88
+ app.state.sample_rate = 24000
89
+ app.state.adapter = "unknown"
90
+
91
+ app.state.status = "healthy"
92
+ except Exception as exc:
93
+ app.state.status = f"startup_failed: {exc}"
94
+ app.state.tts_engine = None
95
+
96
+
97
+ @app.get("/health")
98
+ async def health() -> dict:
99
+ return VocenceHealthResponse(
100
+ status=getattr(app.state, "status", "unknown"),
101
+ model_loaded=getattr(app.state, "tts_engine", None) is not None,
102
+ sample_rate=getattr(app.state, "sample_rate", None),
103
+ adapter=getattr(app.state, "adapter", None),
104
+ repo_path=str(repo_path),
105
+ ).model_dump()
106
+
107
+
108
+ @app.post("/speak", response_class=Response)
109
+ async def speak(args: VocenceSpeakRequest):
110
+ engine = getattr(app.state, "tts_engine", None)
111
+ if engine is None:
112
+ raise HTTPException(
113
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
114
+ detail="TTS engine not loaded",
115
+ )
116
+
117
+ waveform, sample_rate = engine.generate_wav(instruction=args.instruction, text=args.text)
118
+ waveform = np.asarray(waveform)
119
+ if waveform.ndim != 1 or waveform.size == 0:
120
+ raise HTTPException(status_code=400, detail="invalid waveform")
121
+
122
+ duration_sec = float(waveform.shape[0]) / float(sample_rate)
123
+ if duration_sec <= 0 or duration_sec > VOCENCE_MAX_AUDIO_SECONDS:
124
+ raise HTTPException(status_code=400, detail="invalid duration")
125
+
126
+ return Response(
127
+ content=waveform_to_wav_bytes(waveform, sample_rate),
128
+ media_type="audio/wav",
129
+ )
130
+
131
+
132
+ if __name__ == "__main__":
133
+ uvicorn.run("vocence_local_wrapper:app", host="127.0.0.1", port=8000, reload=False)