alakxender commited on
Commit
249b0a6
·
1 Parent(s): f45cc8e
Files changed (9) hide show
  1. app.py +5 -209
  2. csm1b_dv.py +508 -0
  3. dia/__init__.py +6 -0
  4. dia/audio.py +280 -0
  5. dia/config.py +206 -0
  6. dia/layers.py +909 -0
  7. dia/model.py +460 -0
  8. dia_1_6B_dv.py +390 -0
  9. example.txt +4 -0
app.py CHANGED
@@ -8,6 +8,8 @@ import tempfile
8
  import os
9
  import re
10
  import spaces
 
 
11
 
12
  # Global variables to persist across requests
13
  device = "cuda" if torch.cuda.is_available() else "cpu"
@@ -465,214 +467,8 @@ with gr.Blocks(
465
  }
466
  """
467
  ) as app:
468
- with gr.Tab("🎙️ CSM-1B"):
469
- gr.Markdown("# 🎙️ CSM-1B Text-to-Speech Synthesis")
470
- gr.Markdown("**CSM (Conversational Speech Model)** is a speech generation model from [Sesame](sesame.com) that generates **RVQ audio codes** from text and audio inputs. The model architecture employs a [Llama](https://www.llama.com/) backbone and a smaller audio decoder that produces [Mimi](https://huggingface.co/kyutai/mimi) audio codes. This demo uses a **fine-tuned version** of the model for **Dhivehi speech synthesis**.")
471
- # Model selection
472
- with gr.Row():
473
- model_dropdown = gr.Dropdown(
474
- choices=list(MODELS.keys()),
475
- value=list(MODELS.keys())[0],
476
- label="🤖 Select Model"
477
- )
478
- model_info = gr.Textbox(
479
- value="No model loaded - select a model to load",
480
- label="Model Status",
481
- interactive=False
482
- )
483
-
484
- with gr.Tabs():
485
- # Simple Generation
486
- with gr.TabItem("🎯 Simple Generation"):
487
- gr.Markdown("### Generate speech from text without context")
488
-
489
- with gr.Row():
490
- with gr.Column():
491
- simple_text = gr.Textbox(
492
- label="Text to Generate (Dhivehi)",
493
- placeholder="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
494
- value="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
495
- lines=3,
496
- elem_classes=["dhivehi-text"]
497
- )
498
- # Generate speaker choices from current model's speaker prompts
499
- current_prompts = get_current_speaker_prompts()
500
- speaker_choices = [(f"{prompt_data['speaker_id']}: {prompt_data['name']}", prompt_data['speaker_id'])
501
- for prompt_key, prompt_data in current_prompts.items()]
502
- simple_speaker = gr.Radio(
503
- choices=[choice[0] for choice in speaker_choices],
504
- label="Speaker",
505
- value=speaker_choices[0][0] if speaker_choices else "0: Speaker"
506
- )
507
- simple_btn = gr.Button("🎵 Generate", variant="primary")
508
-
509
- with gr.Column():
510
- simple_audio = gr.Audio(label="Generated Audio")
511
- simple_status = gr.Textbox(label="Status", interactive=False)
512
-
513
- def simple_generate_with_mapping(text, speaker_display, selected_model):
514
- # Extract speaker ID from display text (e.g., "0: Female Speaker 01" -> "0")
515
- speaker_id = speaker_display.split(":")[0]
516
- return generate_simple_audio(text, speaker_id, selected_model)
517
-
518
- simple_btn.click(
519
- simple_generate_with_mapping,
520
- inputs=[simple_text, simple_speaker, model_dropdown],
521
- outputs=[simple_audio, simple_status]
522
- )
523
-
524
- # Context Generation
525
- with gr.TabItem("🎭 Context Generation"):
526
- gr.Markdown("### Generate speech with voice prompt")
527
-
528
- with gr.Row():
529
- with gr.Column():
530
- context_text = gr.Textbox(
531
- label="Speaker prompt",
532
- placeholder="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
533
- value="",
534
- lines=2,
535
- elem_classes=["dhivehi-text"]
536
- )
537
- context_audio = gr.Audio(
538
- label="Speaker Prompt",
539
- type="filepath"
540
- )
541
- target_text = gr.Textbox(
542
- label="Text to Generate",
543
- placeholder="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
544
- value="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
545
- lines=3,
546
- elem_classes=["dhivehi-text"]
547
- )
548
- # Generate speaker choices for context generation
549
- context_speaker = gr.Radio(
550
- choices=[choice[0] for choice in speaker_choices],
551
- label="Speaker",
552
- value=speaker_choices[0][0] if speaker_choices else "0: Speaker"
553
- )
554
- context_btn = gr.Button("🎵 Generate with Context", variant="primary")
555
-
556
- with gr.Column():
557
- context_audio_out = gr.Audio(label="Generated Audio")
558
- context_status = gr.Textbox(label="Status", interactive=False)
559
-
560
- def context_generate_with_mapping(text, speaker_display, context_text_val, context_audio_val, selected_model):
561
- # Extract speaker ID from display text
562
- speaker_id = speaker_display.split(":")[0]
563
- return generate_context_audio(text, speaker_id, context_text_val, context_audio_val, selected_model)
564
-
565
- context_btn.click(
566
- context_generate_with_mapping,
567
- inputs=[target_text, context_speaker, context_text, context_audio, model_dropdown],
568
- outputs=[context_audio_out, context_status]
569
- )
570
-
571
- # Conversation Generation
572
- with gr.TabItem("💬 Conversation"):
573
- gr.Markdown("### Generate dual-speaker conversations")
574
-
575
- with gr.Row():
576
- speaker_a = gr.Dropdown(
577
- choices=get_speaker_choices(),
578
- label="Speaker A",
579
- value=get_speaker_choices()[0]
580
- )
581
- speaker_b = gr.Dropdown(
582
- choices=get_speaker_choices(),
583
- label="Speaker B",
584
- value=get_speaker_choices()[1] if len(get_speaker_choices()) > 1 else get_speaker_choices()[0]
585
- )
586
-
587
- with gr.Accordion("🎵 Audio Style References", open=False):
588
- with gr.Row():
589
- with gr.Column():
590
- gr.Markdown("**Speaker A Prompt**")
591
- speaker_a_audio = gr.Audio(
592
- type="filepath",
593
- label="Speaker A Audio Style"
594
- )
595
- speaker_a_text = gr.Textbox(
596
- label="Speaker A Prompt Text",
597
- lines=2,
598
- placeholder="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
599
- elem_classes=["dhivehi-text"]
600
- )
601
-
602
- with gr.Column():
603
- gr.Markdown("**Speaker B Prompt**")
604
- speaker_b_audio = gr.Audio(
605
- type="filepath",
606
- label="Speaker B Speaker Prompt"
607
- )
608
- speaker_b_text = gr.Textbox(
609
- label="Speaker B Speaker Prompt",
610
- lines=2,
611
- placeholder="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
612
- elem_classes=["dhivehi-text"]
613
- )
614
-
615
- with gr.Accordion("⚙️ Options", open=False):
616
- use_style = gr.Checkbox(
617
- label="Use audio style references",
618
- value=False
619
- )
620
- split_sentences_checkbox = gr.Checkbox(
621
- label="Split sentences",
622
- value=True
623
- )
624
-
625
- dialogue_text = gr.Textbox(
626
- lines=6,
627
- placeholder="ދަރިފުޅު މިއަދު ހާދަ ލަސްތިވީ.. މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ...... ކޮބާ ތިޔަ ރިޕޯޓް ފޮތް؟\nދަބަހުގަ އެބައޮތް!.\nވަދެބަލަ އެތެރެއަށް!... ދީބަލަ ރިޕޯޓްފޮތް މަންމަ ބަލައިލަން!",
628
- value="ދަރިފުޅު މިއަދު ހާދަ ލަސްތިވީ.. މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ...... ކޮބާ ތިޔަ ރިޕޯޓް ފޮތް؟\nދަބަހުގަ އެބައޮތް!.\nވަދެބަލަ އެތެރެއަށް!... ދީބަލަ ރިޕޯޓްފޮތް މަންމަ ބަލައިލަން!",
629
- label="Dialogue Lines (one per line)",
630
- elem_classes=["dhivehi-text"]
631
- )
632
-
633
- conv_btn = gr.Button("🎵 Generate Conversation", variant="primary")
634
- conv_audio = gr.Audio(label="Generated Conversation")
635
- conv_status = gr.Textbox(label="Status", interactive=False)
636
-
637
- def conversation_generate_with_model(speaker_a_val, speaker_b_val, speaker_a_audio_val, speaker_a_text_val,
638
- speaker_b_audio_val, speaker_b_text_val, dialogue_text_val,
639
- split_sentences_flag, use_style_flag, selected_model):
640
- return generate_conversation(speaker_a_val, speaker_b_val, speaker_a_audio_val, speaker_a_text_val,
641
- speaker_b_audio_val, speaker_b_text_val, dialogue_text_val,
642
- split_sentences_flag, use_style_flag, selected_model)
643
-
644
- conv_btn.click(
645
- conversation_generate_with_model,
646
- inputs=[speaker_a, speaker_b, speaker_a_audio, speaker_a_text,
647
- speaker_b_audio, speaker_b_text, dialogue_text,
648
- split_sentences_checkbox, use_style, model_dropdown],
649
- outputs=[conv_audio, conv_status]
650
- )
651
-
652
- # Wire up model change
653
- model_dropdown.change(
654
- change_model_and_update_ui,
655
- inputs=[model_dropdown],
656
- outputs=[model_info, speaker_a, speaker_b, simple_speaker, context_speaker]
657
- )
658
-
659
- gr.Markdown("""
660
- ---
661
- **Tips:**
662
- - Simple: Basic text-to-speech
663
- - Context: Use reference audio for voice consistency
664
- - Conversation: Multi-speaker dialogues with style control
665
-
666
- **Issues:**
667
- - Context: Context breaks sometimes. Adding multiple context audio seems to make it work, or adding previous generation to the context helps.
668
- - Audio: Sometimes the generated audio is not in sync with the text.
669
- - Long sentences: Generated long sentences seems sped up.
670
- - Repeating words: Generated text sometimes repeats words.
671
- """)
672
- with gr.Tab("🎙️ Dia-1.6B"):
673
- gr.Markdown("# 🎙️ Dia-1.6B Text-to-Speech Synthesis")
674
- gr.Markdown("Dia is a 1.6B parameter text to speech model created by [Nari Labs](https://huggingface.co/nari-labs/Dia-1.6B). This demo uses a fine-tuned version of the model with **Dhivehi (Thaana) voices**. Also supports mixed languages and can generate non-verbal, dialogue and voice-clone.")
675
- gr.Markdown("Check back later...")
676
 
677
  if __name__ == "__main__":
678
- app.launch(share=False)
 
8
  import os
9
  import re
10
  import spaces
11
+ from csm1b_dv import get_csm1b_tab
12
+ from dia_1_6B_dv import get_dia_1_6B_tab
13
 
14
  # Global variables to persist across requests
15
  device = "cuda" if torch.cuda.is_available() else "cpu"
 
467
  }
468
  """
469
  ) as app:
470
+ get_csm1b_tab()
471
+ get_dia_1_6B_tab()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
 
473
  if __name__ == "__main__":
474
+ app.launch(share=False,server_name="172.20.0.246",server_port=7860)
csm1b_dv.py ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import torch
3
+ from transformers import AutoProcessor, CsmForConditionalGeneration
4
+ import librosa
5
+ import numpy as np
6
+ import soundfile as sf
7
+ import tempfile
8
+ import os
9
+ import re
10
+ import spaces
11
+
12
+ # Global variables to persist across requests
13
+ device = "cuda" if torch.cuda.is_available() else "cpu"
14
+ model = None
15
+ processor = None
16
+ current_model_id = None
17
+
18
+ # Model configuration
19
+ MODELS = {
20
+ "CSM 1B Dhivehi 2-Speakers": "alakxender/csm-1b-dhivehi-2-speakers",
21
+ "CSM 1B Dhivehi 5-Speaker": "alakxender/csm-1b-dhivehi-5-spk-pv",
22
+ }
23
+
24
+ # Model-specific speaker prompts
25
+ MODEL_SPEAKER_PROMPTS = {
26
+ "alakxender/csm-1b-dhivehi-2-speakers": {
27
+ "female_01": {
28
+ "text": None,
29
+ "audio": None,
30
+ "speaker_id": "0",
31
+ "name": "Female Speaker 01"
32
+ },
33
+ "male_01": {
34
+ "text": None,
35
+ "audio": None,
36
+ "speaker_id": "1",
37
+ "name": "Male Speaker 01"
38
+ }
39
+ },
40
+ "alakxender/csm-1b-dhivehi-5-spk-pv": {
41
+ "female_01": {
42
+ "text": None,
43
+ "audio": None,
44
+ "speaker_id": "0",
45
+ "name": "Female Speaker 01"
46
+ },
47
+ "male_01": {
48
+ "text": None,
49
+ "audio": None,
50
+ "speaker_id": "1",
51
+ "name": "Male Speaker 01"
52
+ },
53
+ "female_02": {
54
+ "text": None,
55
+ "audio": None,
56
+ "speaker_id": "2",
57
+ "name": "Female Speaker 02"
58
+ },
59
+ "male_02": {
60
+ "text": None,
61
+ "audio": None,
62
+ "speaker_id": "4",
63
+ "name": "Male Speaker 02"
64
+ },
65
+ "female_03": {
66
+ "text": None,
67
+ "audio": None,
68
+ "speaker_id": "3",
69
+ "name": "Female Speaker 03"
70
+ }
71
+ }
72
+ }
73
+
74
+ @spaces.GPU
75
+ def load_model(model_name):
76
+ global model, processor, current_model_id
77
+ if model_name not in MODELS:
78
+ return False
79
+ model_id = MODELS[model_name]
80
+ if current_model_id == model_id:
81
+ return True
82
+ try:
83
+ if model is not None:
84
+ del model
85
+ if torch.cuda.is_available():
86
+ torch.cuda.empty_cache()
87
+ processor = AutoProcessor.from_pretrained(model_id)
88
+ if hasattr(processor.tokenizer, "init_kwargs"):
89
+ processor.tokenizer.init_kwargs.pop("pad_to_multiple_of", None)
90
+ model = CsmForConditionalGeneration.from_pretrained(
91
+ model_id,
92
+ device_map=device,
93
+ torch_dtype=torch.float32
94
+ )
95
+ current_model_id = model_id
96
+ return True
97
+ except Exception:
98
+ return False
99
+
100
+ def get_current_speaker_prompts():
101
+ if current_model_id in MODEL_SPEAKER_PROMPTS:
102
+ return MODEL_SPEAKER_PROMPTS[current_model_id]
103
+ return {
104
+ "female_01": {"text": "", "audio": None, "speaker_id": "0", "name": "Female Speaker 01"},
105
+ "male_01": {"text": "", "audio": None, "speaker_id": "1", "name": "Male Speaker 01"}
106
+ }
107
+
108
+ def get_model_info():
109
+ if current_model_id:
110
+ model_name = next((name for name, id in MODELS.items() if id == current_model_id), "Unknown")
111
+ return f"Current Model: {model_name}"
112
+ return "No model loaded"
113
+
114
+ def get_speaker_choices():
115
+ prompts = get_current_speaker_prompts()
116
+ return list(prompts.keys())
117
+
118
+ def load_audio_file(filepath, target_sr=24000):
119
+ if filepath is None or not os.path.exists(filepath):
120
+ return None
121
+ try:
122
+ audio, sr = librosa.load(filepath, sr=target_sr)
123
+ return audio.astype(np.float32)
124
+ except Exception:
125
+ return None
126
+
127
+ @spaces.GPU
128
+ def generate_simple_audio(text, speaker_id, model_name=None):
129
+ global model, processor
130
+ if not text.strip():
131
+ return None, "Please enter some text."
132
+ if model_name and model_name in MODELS:
133
+ load_model(model_name)
134
+ elif model is None or processor is None:
135
+ load_model(list(MODELS.keys())[0])
136
+ try:
137
+ formatted_text = f"[{speaker_id}]{text}"
138
+ inputs = processor(formatted_text, add_special_tokens=True).to(device)
139
+ with torch.no_grad():
140
+ audio = model.generate(**inputs, output_audio=True)
141
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
142
+ processor.save_audio(audio, temp_file.name)
143
+ return temp_file.name, "Audio generated successfully!"
144
+ except Exception as e:
145
+ return None, f"Error: {str(e)}"
146
+
147
+ @spaces.GPU
148
+ def generate_context_audio(text, speaker_id, context_text, context_audio_file, model_name=None):
149
+ global model, processor
150
+ if not text.strip():
151
+ return None, "Please enter text to generate."
152
+ if not context_text.strip() or context_audio_file is None:
153
+ return None, "Please provide both context text and audio."
154
+ if model_name and model_name in MODELS:
155
+ load_model(model_name)
156
+ elif model is None or processor is None:
157
+ load_model(list(MODELS.keys())[0])
158
+ try:
159
+ context_audio = load_audio_file(context_audio_file)
160
+ if context_audio is None:
161
+ return None, "Failed to load context audio."
162
+ conversation = [
163
+ {
164
+ "role": str(speaker_id),
165
+ "content": [
166
+ {"type": "text", "text": context_text},
167
+ {"type": "audio", "path": context_audio}
168
+ ]
169
+ },
170
+ {
171
+ "role": str(speaker_id),
172
+ "content": [{"type": "text", "text": text}]
173
+ }
174
+ ]
175
+ inputs = processor.apply_chat_template(
176
+ conversation, tokenize=True, return_dict=True
177
+ ).to(device)
178
+ with torch.no_grad():
179
+ audio = model.generate(**inputs, output_audio=True)
180
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
181
+ processor.save_audio(audio, temp_file.name)
182
+ return temp_file.name, "Audio generated with context!"
183
+ except Exception as e:
184
+ return None, f"Error: {str(e)}"
185
+
186
+ def trim_silence(audio_array, top_db=20):
187
+ try:
188
+ trimmed, _ = librosa.effects.trim(audio_array, top_db=top_db)
189
+ return trimmed
190
+ except:
191
+ return audio_array
192
+
193
+ def split_sentences(text):
194
+ sentences = re.split(r'[.؟!،]', text)
195
+ return [s.strip() for s in sentences if s.strip()]
196
+
197
+ def extract_audio_output(audio_output):
198
+ global processor
199
+ try:
200
+ if hasattr(audio_output, 'audio_values'):
201
+ return audio_output.audio_values.cpu().squeeze().numpy()
202
+ elif isinstance(audio_output, torch.Tensor):
203
+ return audio_output.cpu().squeeze().numpy()
204
+ else:
205
+ temp_file = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
206
+ processor.save_audio(audio_output, temp_file.name)
207
+ audio, _ = librosa.load(temp_file.name, sr=24000)
208
+ os.unlink(temp_file.name)
209
+ return audio
210
+ except Exception:
211
+ return np.array([])
212
+
213
+ @spaces.GPU
214
+ def generate_conversation(speaker_a, speaker_b, speaker_a_audio, speaker_a_text,
215
+ speaker_b_audio, speaker_b_text, dialogue_text,
216
+ split_sentences_flag, use_style, model_name=None, progress=gr.Progress()):
217
+ global model, processor
218
+ if model_name and model_name in MODELS:
219
+ load_model(model_name)
220
+ elif model is None or processor is None:
221
+ load_model(list(MODELS.keys())[0])
222
+ try:
223
+ lines = [line.strip() for line in dialogue_text.strip().split("\n") if line.strip()]
224
+ if not lines:
225
+ return None, "Please enter dialogue text."
226
+ current_prompts = get_current_speaker_prompts()
227
+ speaker_a_id = current_prompts.get(speaker_a, {}).get("speaker_id", "0")
228
+ speaker_b_id = current_prompts.get(speaker_b, {}).get("speaker_id", "1")
229
+ speakers = [speaker_a_id, speaker_b_id]
230
+ progress(0, desc="Preparing...")
231
+ speaker_contexts = {}
232
+ if use_style:
233
+ if speaker_a in current_prompts and speaker_a_audio and speaker_a_text:
234
+ audio_data = load_audio_file(speaker_a_audio)
235
+ if audio_data is not None:
236
+ speaker_contexts[speaker_a_id] = {
237
+ "audio": trim_silence(audio_data),
238
+ "text": speaker_a_text.strip()
239
+ }
240
+ if speaker_b in current_prompts and speaker_b_audio and speaker_b_text:
241
+ audio_data = load_audio_file(speaker_b_audio)
242
+ if audio_data is not None:
243
+ speaker_contexts[speaker_b_id] = {
244
+ "audio": trim_silence(audio_data),
245
+ "text": speaker_b_text.strip()
246
+ }
247
+ text_units = []
248
+ if split_sentences_flag:
249
+ for i, line in enumerate(lines):
250
+ speaker_role = speakers[i % 2]
251
+ sentences = split_sentences(line)
252
+ for sentence in sentences:
253
+ text_units.append((sentence, speaker_role))
254
+ else:
255
+ for i, line in enumerate(lines):
256
+ speaker_role = speakers[i % 2]
257
+ text_units.append((line, speaker_role))
258
+ if not text_units:
259
+ return None, "No text to generate."
260
+ audio_segments = []
261
+ progress(0.1, desc="Generating audio...")
262
+ for i, (text, role) in enumerate(text_units):
263
+ progress(0.1 + (i / len(text_units)) * 0.8,
264
+ desc=f"Generating {i+1}/{len(text_units)}: Speaker {role}")
265
+ conversation = []
266
+ if use_style and role in speaker_contexts:
267
+ is_first = True
268
+ for prev_i in range(i):
269
+ if text_units[prev_i][1] == role:
270
+ is_first = False
271
+ break
272
+ if is_first:
273
+ context = speaker_contexts[role]
274
+ conversation.append({
275
+ "role": role,
276
+ "content": [
277
+ {"type": "text", "text": context["text"]},
278
+ {"type": "audio", "path": context["audio"]}
279
+ ]
280
+ })
281
+ conversation.append({
282
+ "role": role,
283
+ "content": [{"type": "text", "text": text}]
284
+ })
285
+ inputs = processor.apply_chat_template(
286
+ conversation, tokenize=True, return_dict=True
287
+ ).to(device)
288
+ with torch.no_grad():
289
+ audio_output = model.generate(**inputs, output_audio=True)
290
+ audio_segment = extract_audio_output(audio_output)
291
+ if len(audio_segment) > 0:
292
+ audio_segment = trim_silence(audio_segment)
293
+ audio_segments.append(audio_segment)
294
+ if not audio_segments:
295
+ return None, "No audio generated."
296
+ progress(0.9, desc="Finalizing...")
297
+ if len(audio_segments) == 1:
298
+ final_audio = audio_segments[0]
299
+ else:
300
+ final_audio = np.concatenate(audio_segments)
301
+ if np.max(np.abs(final_audio)) > 0:
302
+ final_audio = final_audio / np.max(np.abs(final_audio)) * 0.95
303
+ output_file = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
304
+ sf.write(output_file.name, final_audio, 24000)
305
+ duration = len(final_audio) / 24000
306
+ progress(1.0, desc="Complete!")
307
+ return output_file.name, f"Generated {len(text_units)} segments, {duration:.1f}s total"
308
+ except Exception as e:
309
+ return None, f"Error: {str(e)}"
310
+
311
+ def change_model_and_update_ui(model_name):
312
+ success = load_model(model_name)
313
+ if not success:
314
+ return (
315
+ gr.update(value="Error loading model"),
316
+ gr.update(),
317
+ gr.update(),
318
+ gr.update(),
319
+ gr.update()
320
+ )
321
+ choices = get_speaker_choices()
322
+ current_prompts = get_current_speaker_prompts()
323
+ new_speaker_choices = [(f"{prompt_data['speaker_id']}: {prompt_data['name']}", prompt_data['speaker_id'])
324
+ for prompt_key, prompt_data in current_prompts.items()]
325
+ display_choices = [choice[0] for choice in new_speaker_choices]
326
+ return (
327
+ gr.update(value=get_model_info()),
328
+ gr.update(choices=choices, value=choices[0]),
329
+ gr.update(choices=choices, value=choices[1] if len(choices) > 1 else choices[0]),
330
+ gr.update(choices=display_choices, value=display_choices[0] if display_choices else "0: Speaker"),
331
+ gr.update(choices=display_choices, value=display_choices[0] if display_choices else "0: Speaker")
332
+ )
333
+
334
+ def get_csm1b_tab():
335
+ with gr.Tab("🎙️ CSM-1B"):
336
+ gr.Markdown("# 🎙️ CSM-1B Text-to-Speech Synthesis")
337
+ gr.Markdown("**CSM (Conversational Speech Model)** is a speech generation model from [Sesame](sesame.com) that generates **RVQ audio codes** from text and audio inputs. The model architecture employs a [Llama](https://www.llama.com/) backbone and a smaller audio decoder that produces [Mimi](https://huggingface.co/kyutai/mimi) audio codes. This demo uses a **fine-tuned version** of the model for **Dhivehi speech synthesis**.")
338
+ with gr.Row():
339
+ model_dropdown = gr.Dropdown(
340
+ choices=list(MODELS.keys()),
341
+ value=list(MODELS.keys())[0],
342
+ label="🤖 Select Model"
343
+ )
344
+ model_info = gr.Textbox(
345
+ value="No model loaded - select a model to load",
346
+ label="Model Status",
347
+ interactive=False
348
+ )
349
+ with gr.Tabs():
350
+ with gr.TabItem("🎯 Simple Generation"):
351
+ gr.Markdown("### Generate speech from text without context")
352
+ with gr.Row():
353
+ with gr.Column():
354
+ simple_text = gr.Textbox(
355
+ label="Text to Generate (Dhivehi)",
356
+ placeholder="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
357
+ value="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
358
+ lines=3,
359
+ elem_classes=["dhivehi-text"]
360
+ )
361
+ current_prompts = get_current_speaker_prompts()
362
+ speaker_choices = [(f"{prompt_data['speaker_id']}: {prompt_data['name']}", prompt_data['speaker_id'])
363
+ for prompt_key, prompt_data in current_prompts.items()]
364
+ simple_speaker = gr.Radio(
365
+ choices=[choice[0] for choice in speaker_choices],
366
+ label="Speaker",
367
+ value=speaker_choices[0][0] if speaker_choices else "0: Speaker"
368
+ )
369
+ simple_btn = gr.Button("🎵 Generate", variant="primary")
370
+ with gr.Column():
371
+ simple_audio = gr.Audio(label="Generated Audio")
372
+ simple_status = gr.Textbox(label="Status", interactive=False)
373
+ def simple_generate_with_mapping(text, speaker_display, selected_model):
374
+ speaker_id = speaker_display.split(":")[0]
375
+ return generate_simple_audio(text, speaker_id, selected_model)
376
+ simple_btn.click(
377
+ simple_generate_with_mapping,
378
+ inputs=[simple_text, simple_speaker, model_dropdown],
379
+ outputs=[simple_audio, simple_status]
380
+ )
381
+ with gr.TabItem("🎭 Context Generation"):
382
+ gr.Markdown("### Generate speech with voice prompt")
383
+ with gr.Row():
384
+ with gr.Column():
385
+ context_text = gr.Textbox(
386
+ label="Speaker prompt",
387
+ placeholder="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
388
+ value="",
389
+ lines=2,
390
+ elem_classes=["dhivehi-text"]
391
+ )
392
+ context_audio = gr.Audio(
393
+ label="Speaker Prompt",
394
+ type="filepath"
395
+ )
396
+ target_text = gr.Textbox(
397
+ label="Text to Generate",
398
+ placeholder="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
399
+ value="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
400
+ lines=3,
401
+ elem_classes=["dhivehi-text"]
402
+ )
403
+ context_speaker = gr.Radio(
404
+ choices=[choice[0] for choice in speaker_choices],
405
+ label="Speaker",
406
+ value=speaker_choices[0][0] if speaker_choices else "0: Speaker"
407
+ )
408
+ context_btn = gr.Button("🎵 Generate with Context", variant="primary")
409
+ with gr.Column():
410
+ context_audio_out = gr.Audio(label="Generated Audio")
411
+ context_status = gr.Textbox(label="Status", interactive=False)
412
+ def context_generate_with_mapping(text, speaker_display, context_text_val, context_audio_val, selected_model):
413
+ speaker_id = speaker_display.split(":")[0]
414
+ return generate_context_audio(text, speaker_id, context_text_val, context_audio_val, selected_model)
415
+ context_btn.click(
416
+ context_generate_with_mapping,
417
+ inputs=[target_text, context_speaker, context_text, context_audio, model_dropdown],
418
+ outputs=[context_audio_out, context_status]
419
+ )
420
+ with gr.TabItem("💬 Conversation"):
421
+ gr.Markdown("### Generate dual-speaker conversations")
422
+ with gr.Row():
423
+ speaker_a = gr.Dropdown(
424
+ choices=get_speaker_choices(),
425
+ label="Speaker A",
426
+ value=get_speaker_choices()[0]
427
+ )
428
+ speaker_b = gr.Dropdown(
429
+ choices=get_speaker_choices(),
430
+ label="Speaker B",
431
+ value=get_speaker_choices()[1] if len(get_speaker_choices()) > 1 else get_speaker_choices()[0]
432
+ )
433
+ with gr.Accordion("🎵 Audio Style References", open=False):
434
+ with gr.Row():
435
+ with gr.Column():
436
+ gr.Markdown("**Speaker A Prompt**")
437
+ speaker_a_audio = gr.Audio(
438
+ type="filepath",
439
+ label="Speaker A Audio Style"
440
+ )
441
+ speaker_a_text = gr.Textbox(
442
+ label="Speaker A Prompt Text",
443
+ lines=2,
444
+ placeholder="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
445
+ elem_classes=["dhivehi-text"]
446
+ )
447
+ with gr.Column():
448
+ gr.Markdown("**Speaker B Prompt**")
449
+ speaker_b_audio = gr.Audio(
450
+ type="filepath",
451
+ label="Speaker B Speaker Prompt"
452
+ )
453
+ speaker_b_text = gr.Textbox(
454
+ label="Speaker B Speaker Prompt",
455
+ lines=2,
456
+ placeholder="މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ",
457
+ elem_classes=["dhivehi-text"]
458
+ )
459
+ with gr.Accordion("⚙️ Options", open=False):
460
+ use_style = gr.Checkbox(
461
+ label="Use audio style references",
462
+ value=False
463
+ )
464
+ split_sentences_checkbox = gr.Checkbox(
465
+ label="Split sentences",
466
+ value=True
467
+ )
468
+ dialogue_text = gr.Textbox(
469
+ lines=6,
470
+ placeholder="ދަރިފުޅު މިއަދު ހާދަ ލަސްތިވީ.. މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ...... ކޮބާ ތިޔަ ރިޕޯޓް ފޮތް؟\nދަބަހުގަ އެބައޮތް!.",
471
+ value="ދަރިފުޅު މިއަދު ހާދަ ލަސްތިވީ.. މަންމަ ކިހާއިރެއް ދަރިފުޅުގެ އިންތިޒާރުގަ އިންނަތާ...... ކޮބާ ތިޔަ ރިޕޯޓް ފޮތް؟\nދަބަހުގަ އެބައޮތް!.",
472
+ label="Dialogue Lines (one per line)",
473
+ elem_classes=["dhivehi-text"]
474
+ )
475
+ conv_btn = gr.Button("🎵 Generate Conversation", variant="primary")
476
+ conv_audio = gr.Audio(label="Generated Conversation")
477
+ conv_status = gr.Textbox(label="Status", interactive=False)
478
+ def conversation_generate_with_model(speaker_a_val, speaker_b_val, speaker_a_audio_val, speaker_a_text_val,
479
+ speaker_b_audio_val, speaker_b_text_val, dialogue_text_val,
480
+ split_sentences_flag, use_style_flag, selected_model):
481
+ return generate_conversation(speaker_a_val, speaker_b_val, speaker_a_audio_val, speaker_a_text_val,
482
+ speaker_b_audio_val, speaker_b_text_val, dialogue_text_val,
483
+ split_sentences_flag, use_style_flag, selected_model)
484
+ conv_btn.click(
485
+ conversation_generate_with_model,
486
+ inputs=[speaker_a, speaker_b, speaker_a_audio, speaker_a_text,
487
+ speaker_b_audio, speaker_b_text, dialogue_text,
488
+ split_sentences_checkbox, use_style, model_dropdown],
489
+ outputs=[conv_audio, conv_status]
490
+ )
491
+ model_dropdown.change(
492
+ change_model_and_update_ui,
493
+ inputs=[model_dropdown],
494
+ outputs=[model_info, speaker_a, speaker_b, simple_speaker, context_speaker]
495
+ )
496
+ gr.Markdown("""
497
+ ---
498
+ **Tips:**
499
+ - Simple: Basic text-to-speech
500
+ - Context: Use reference audio for voice consistency
501
+ - Conversation: Multi-speaker dialogues with style control
502
+ **Issues:**
503
+ - Context: Context breaks sometimes. Adding multiple context audio seems to make it work, or adding previous generation to the context helps.
504
+ - Audio: Sometimes the generated audio is not in sync with the text.
505
+ - Long sentences: Generated long sentences seems sped up.
506
+ - Repeating words: Generated text sometimes repeats words.
507
+ """)
508
+ # No explicit return needed for context manager pattern
dia/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from .model import Dia
2
+
3
+
4
+ __all__ = [
5
+ "Dia",
6
+ ]
dia/audio.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import typing as tp
2
+
3
+ import torch
4
+
5
+ from .config import DataConfig
6
+
7
+
8
+ def build_delay_indices(B: int, T: int, C: int, delay_pattern: tp.List[int]) -> tp.Tuple[torch.Tensor, torch.Tensor]:
9
+ """
10
+ Precompute (t_idx_BxTxC, indices_BTCx3) so that out[t, c] = in[t - delay[c], c].
11
+ Negative t_idx => BOS; t_idx >= T => PAD.
12
+ """
13
+ delay_arr = torch.tensor(delay_pattern, dtype=torch.int32)
14
+
15
+ t_idx_BxT = torch.broadcast_to(
16
+ torch.arange(T, dtype=torch.int32)[None, :],
17
+ [B, T],
18
+ )
19
+ t_idx_BxTx1 = t_idx_BxT[..., None]
20
+ t_idx_BxTxC = t_idx_BxTx1 - delay_arr.view(1, 1, C)
21
+
22
+ b_idx_BxTxC = torch.broadcast_to(
23
+ torch.arange(B, dtype=torch.int32).view(B, 1, 1),
24
+ [B, T, C],
25
+ )
26
+ c_idx_BxTxC = torch.broadcast_to(
27
+ torch.arange(C, dtype=torch.int32).view(1, 1, C),
28
+ [B, T, C],
29
+ )
30
+
31
+ # We must clamp time indices to [0..T-1] so gather_nd equivalent won't fail
32
+ t_clamped_BxTxC = torch.clamp(t_idx_BxTxC, 0, T - 1)
33
+
34
+ indices_BTCx3 = torch.stack(
35
+ [
36
+ b_idx_BxTxC.reshape(-1),
37
+ t_clamped_BxTxC.reshape(-1),
38
+ c_idx_BxTxC.reshape(-1),
39
+ ],
40
+ dim=1,
41
+ ).long() # Ensure indices are long type for indexing
42
+
43
+ return t_idx_BxTxC, indices_BTCx3
44
+
45
+
46
+ def apply_audio_delay(
47
+ audio_BxTxC: torch.Tensor,
48
+ pad_value: int,
49
+ bos_value: int,
50
+ precomp: tp.Tuple[torch.Tensor, torch.Tensor],
51
+ ) -> torch.Tensor:
52
+ """
53
+ Applies the delay pattern to batched audio tokens using precomputed indices,
54
+ inserting BOS where t_idx < 0 and PAD where t_idx >= T.
55
+
56
+ Args:
57
+ audio_BxTxC: [B, T, C] int16 audio tokens (or int32/float)
58
+ pad_value: the padding token
59
+ bos_value: the BOS token
60
+ precomp: (t_idx_BxTxC, indices_BTCx3) from build_delay_indices
61
+
62
+ Returns:
63
+ result_BxTxC: [B, T, C] delayed audio tokens
64
+ """
65
+ device = audio_BxTxC.device # Get device from input tensor
66
+ t_idx_BxTxC, indices_BTCx3 = precomp
67
+ t_idx_BxTxC = t_idx_BxTxC.to(device) # Move precomputed indices to device
68
+ indices_BTCx3 = indices_BTCx3.to(device)
69
+
70
+ # Equivalent of tf.gather_nd using advanced indexing
71
+ # Ensure indices are long type if not already (build_delay_indices should handle this)
72
+ gathered_flat = audio_BxTxC[indices_BTCx3[:, 0], indices_BTCx3[:, 1], indices_BTCx3[:, 2]]
73
+ gathered_BxTxC = gathered_flat.view(audio_BxTxC.shape)
74
+
75
+ # Create masks on the correct device
76
+ mask_bos = t_idx_BxTxC < 0 # => place bos_value
77
+ mask_pad = t_idx_BxTxC >= audio_BxTxC.shape[1] # => place pad_value
78
+
79
+ # Create scalar tensors on the correct device
80
+ bos_tensor = torch.tensor(bos_value, dtype=audio_BxTxC.dtype, device=device)
81
+ pad_tensor = torch.tensor(pad_value, dtype=audio_BxTxC.dtype, device=device)
82
+
83
+ # If mask_bos, BOS; else if mask_pad, PAD; else original gather
84
+ # All tensors should now be on the same device
85
+ result_BxTxC = torch.where(mask_bos, bos_tensor, torch.where(mask_pad, pad_tensor, gathered_BxTxC))
86
+
87
+ return result_BxTxC
88
+
89
+
90
+ @torch.no_grad()
91
+ @torch.inference_mode()
92
+ def audio_to_codebook(
93
+ model,
94
+ input_values,
95
+ data_config: DataConfig,
96
+ padding_mask=None,
97
+ sample_rate=44100,
98
+ ):
99
+ """
100
+ Encodes the input audio waveform into discrete codes.
101
+
102
+ Args:
103
+ model: The model to use for encoding.
104
+ input_values (`torch.Tensor` of shape `(batch_size, channels, sequence_length)`):
105
+ Float values of the input audio waveform.
106
+ padding_mask (`torch.Tensor` of shape `(batch_size, channels, sequence_length)`):
107
+ Padding mask used to pad the `input_values`.
108
+ sample_rate (`int`, *optional*) :
109
+ Signal sampling_rate
110
+
111
+ Returns:
112
+ A list of frames containing the discrete encoded codes for the input audio waveform, along with rescaling
113
+ factors for each chunk when `normalize` is True. Each frames is a tuple `(codebook, scale)`, with
114
+ `codebook` of shape `[batch_size, num_codebooks, frames]`.
115
+ Scale is not used here.
116
+
117
+ """
118
+ audio_data = model.preprocess(input_values, sample_rate)
119
+
120
+ if padding_mask is None:
121
+ padding_mask = torch.ones_like(input_values).bool()
122
+
123
+ _, encoded_frame, _, _, _ = model.encode(audio_data, n_quantizers=None) # 1, C, T
124
+ seq_length = encoded_frame.shape[2]
125
+
126
+ t_idx_BxTxC, indices_BTCx3 = build_delay_indices(
127
+ B=1,
128
+ T=seq_length,
129
+ C=data_config.channels,
130
+ delay_pattern=data_config.delay_pattern,
131
+ )
132
+
133
+ encoded_frame = apply_audio_delay(
134
+ audio_BxTxC=encoded_frame.transpose(1, 2), # 1, T, C
135
+ pad_value=data_config.audio_pad_value,
136
+ bos_value=data_config.audio_bos_value,
137
+ precomp=(t_idx_BxTxC, indices_BTCx3),
138
+ )
139
+
140
+ return encoded_frame
141
+
142
+
143
+ def build_revert_indices(B: int, T: int, C: int, delay_pattern: tp.List[int]) -> tp.Tuple[torch.Tensor, torch.Tensor]:
144
+ """
145
+ Precompute indices for the revert operation using PyTorch.
146
+
147
+ Returns:
148
+ A tuple (t_idx_BxTxC, indices_BTCx3) where:
149
+ - t_idx_BxTxC is a tensor of shape [B, T, C] computed as time indices plus the delay.
150
+ - indices_BTCx3 is a tensor of shape [B*T*C, 3] used for gathering, computed from:
151
+ batch indices, clamped time indices, and channel indices.
152
+ """
153
+ # Use default device unless specified otherwise; assumes inputs might define device later
154
+ device = None # Or determine dynamically if needed, e.g., from a model parameter
155
+
156
+ delay_arr = torch.tensor(delay_pattern, dtype=torch.int32, device=device)
157
+
158
+ t_idx_BT1 = torch.broadcast_to(torch.arange(T, device=device).unsqueeze(0), [B, T])
159
+ t_idx_BT1 = t_idx_BT1.unsqueeze(-1)
160
+
161
+ t_idx_BxTxC = torch.minimum(
162
+ t_idx_BT1 + delay_arr.view(1, 1, C),
163
+ torch.tensor(T - 1, device=device),
164
+ )
165
+ b_idx_BxTxC = torch.broadcast_to(torch.arange(B, device=device).view(B, 1, 1), [B, T, C])
166
+ c_idx_BxTxC = torch.broadcast_to(torch.arange(C, device=device).view(1, 1, C), [B, T, C])
167
+
168
+ indices_BTCx3 = torch.stack(
169
+ [
170
+ b_idx_BxTxC.reshape(-1),
171
+ t_idx_BxTxC.reshape(-1),
172
+ c_idx_BxTxC.reshape(-1),
173
+ ],
174
+ axis=1,
175
+ ).long() # Ensure indices are long type
176
+
177
+ return t_idx_BxTxC, indices_BTCx3
178
+
179
+
180
+ def revert_audio_delay(
181
+ audio_BxTxC: torch.Tensor,
182
+ pad_value: int,
183
+ precomp: tp.Tuple[torch.Tensor, torch.Tensor],
184
+ T: int,
185
+ ) -> torch.Tensor:
186
+ """
187
+ Reverts a delay pattern from batched audio tokens using precomputed indices (PyTorch version).
188
+
189
+ Args:
190
+ audio_BxTxC: Input delayed audio tensor
191
+ pad_value: Padding value for out-of-bounds indices
192
+ precomp: Precomputed revert indices tuple containing:
193
+ - t_idx_BxTxC: Time offset indices tensor
194
+ - indices_BTCx3: Gather indices tensor for original audio
195
+ T: Original sequence length before padding
196
+
197
+ Returns:
198
+ Reverted audio tensor with same shape as input
199
+ """
200
+ t_idx_BxTxC, indices_BTCx3 = precomp
201
+ device = audio_BxTxC.device # Get device from input tensor
202
+
203
+ # Move precomputed indices to the same device as audio_BxTxC if they aren't already
204
+ t_idx_BxTxC = t_idx_BxTxC.to(device)
205
+ indices_BTCx3 = indices_BTCx3.to(device)
206
+
207
+ # Using PyTorch advanced indexing (equivalent to tf.gather_nd or np equivalent)
208
+ gathered_flat = audio_BxTxC[indices_BTCx3[:, 0], indices_BTCx3[:, 1], indices_BTCx3[:, 2]]
209
+ gathered_BxTxC = gathered_flat.view(audio_BxTxC.size()) # Use .size() for robust reshaping
210
+
211
+ # Create pad_tensor on the correct device
212
+ pad_tensor = torch.tensor(pad_value, dtype=audio_BxTxC.dtype, device=device)
213
+ # Create T tensor on the correct device for comparison
214
+ T_tensor = torch.tensor(T, device=device)
215
+
216
+ result_BxTxC = torch.where(t_idx_BxTxC >= T_tensor, pad_tensor, gathered_BxTxC) # Changed np.where to torch.where
217
+
218
+ return result_BxTxC
219
+
220
+
221
+ @torch.no_grad()
222
+ @torch.inference_mode()
223
+ def decode(
224
+ model,
225
+ audio_codes,
226
+ ):
227
+ """
228
+ Decodes the given frames into an output audio waveform
229
+ """
230
+ if len(audio_codes) != 1:
231
+ raise ValueError(f"Expected one frame, got {len(audio_codes)}")
232
+
233
+ try:
234
+ audio_values = model.quantizer.from_codes(audio_codes)
235
+ audio_values = model.decode(audio_values[0])
236
+
237
+ return audio_values
238
+ except Exception as e:
239
+ print(f"Error in decode method: {str(e)}")
240
+ raise
241
+
242
+
243
+ def codebook_to_audio(generated_codes: torch.Tensor, model, delay_pattern, B=1, T=2600, C=9):
244
+ """Process a single codebook file to generate audio"""
245
+ # Remove BOS token
246
+ generated_codes = generated_codes[:, 1:]
247
+
248
+ if generated_codes.shape[1] > T:
249
+ generated_codes = generated_codes[:, :T]
250
+
251
+ seq_length = generated_codes.shape[1]
252
+
253
+ # Build revert indices
254
+ t_idx_BxTxC, indices_BTCx3 = build_revert_indices(B=B, T=seq_length, C=C, delay_pattern=delay_pattern)
255
+
256
+ # Transpose and add batch dimension
257
+ audio_BxTxC = generated_codes.transpose(1, 0).unsqueeze(0)
258
+ reverted_codebook = revert_audio_delay(
259
+ audio_BxTxC=audio_BxTxC,
260
+ pad_value=0,
261
+ precomp=(t_idx_BxTxC, indices_BTCx3),
262
+ T=seq_length,
263
+ )
264
+ reverted_codebook = reverted_codebook[:, :-30, :]
265
+
266
+ codebook = reverted_codebook.transpose(1, 2)
267
+
268
+ min_valid_index = 0
269
+ max_valid_index = 1023
270
+ invalid_mask = (codebook < min_valid_index) | (codebook > max_valid_index)
271
+
272
+ num_invalid = torch.sum(invalid_mask).item()
273
+ if num_invalid > 0:
274
+ print(f"Warning: Clamping {num_invalid} indices outside range [{min_valid_index}, {max_valid_index}] to 0.")
275
+
276
+ # Set invalid values to 0 (modify the tensor in-place)
277
+ codebook[invalid_mask] = 0
278
+ audio_array = decode(model, codebook)
279
+
280
+ return audio_array
dia/config.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuration management module for the Dia model.
2
+
3
+ This module provides comprehensive configuration management for the Dia model,
4
+ utilizing Pydantic for validation. It defines configurations for data processing,
5
+ model architecture (encoder and decoder), and training settings.
6
+
7
+ Key components:
8
+ - DataConfig: Parameters for data loading and preprocessing.
9
+ - EncoderConfig: Architecture details for the encoder module.
10
+ - DecoderConfig: Architecture details for the decoder module.
11
+ - ModelConfig: Combined model architecture settings.
12
+ - TrainingConfig: Training hyperparameters and settings.
13
+ - DiaConfig: Master configuration combining all components.
14
+ """
15
+
16
+ import os
17
+ from typing import Annotated
18
+
19
+ from pydantic import BaseModel, BeforeValidator, Field
20
+
21
+
22
+ class DataConfig(BaseModel, frozen=True):
23
+ """Configuration for data loading and preprocessing.
24
+
25
+ Attributes:
26
+ text_length: Maximum length of text sequences (must be multiple of 128).
27
+ audio_length: Maximum length of audio sequences (must be multiple of 128).
28
+ channels: Number of audio channels.
29
+ text_pad_value: Value used for padding text sequences.
30
+ audio_eos_value: Value representing the end of audio sequences.
31
+ audio_bos_value: Value representing the beginning of audio sequences.
32
+ audio_pad_value: Value used for padding audio sequences.
33
+ delay_pattern: List of delay values for each audio channel.
34
+ """
35
+
36
+ text_length: Annotated[int, BeforeValidator(lambda x: (x + 127) // 128 * 128)] = Field(gt=0, multiple_of=128)
37
+ audio_length: Annotated[int, BeforeValidator(lambda x: (x + 127) // 128 * 128)] = Field(gt=0, multiple_of=128)
38
+ channels: int = Field(default=9, gt=0, multiple_of=1)
39
+ text_pad_value: int = Field(default=0)
40
+ audio_eos_value: int = Field(default=1024)
41
+ audio_pad_value: int = Field(default=1025)
42
+ audio_bos_value: int = Field(default=1026)
43
+ delay_pattern: list[Annotated[int, Field(ge=0)]] = Field(default_factory=lambda: [0, 8, 9, 10, 11, 12, 13, 14, 15])
44
+
45
+ def __hash__(self) -> int:
46
+ """Generate a hash based on all fields of the config."""
47
+ return hash(
48
+ (
49
+ self.text_length,
50
+ self.audio_length,
51
+ self.channels,
52
+ self.text_pad_value,
53
+ self.audio_pad_value,
54
+ self.audio_bos_value,
55
+ self.audio_eos_value,
56
+ tuple(self.delay_pattern),
57
+ )
58
+ )
59
+
60
+
61
+ class EncoderConfig(BaseModel, frozen=True):
62
+ """Configuration for the encoder component of the Dia model.
63
+
64
+ Attributes:
65
+ n_layer: Number of transformer layers.
66
+ n_embd: Embedding dimension.
67
+ n_hidden: Hidden dimension size in the MLP layers.
68
+ n_head: Number of attention heads.
69
+ head_dim: Dimension per attention head.
70
+ mlp_activations: List of activation functions for the MLP layers.
71
+ use_pre_norm: Whether to use pre-normalization (LayerNorm before attention/MLP).
72
+ """
73
+
74
+ n_layer: int = Field(gt=0)
75
+ n_embd: int = Field(gt=0)
76
+ n_hidden: int = Field(gt=0)
77
+ n_head: int = Field(gt=0)
78
+ head_dim: int = Field(gt=0)
79
+ mlp_activations: list[str] = Field(default=["silu", "linear"])
80
+ use_pre_norm: bool = Field(default=False)
81
+
82
+
83
+ class DecoderConfig(BaseModel, frozen=True):
84
+ """Configuration for the decoder component of the Dia model.
85
+
86
+ Attributes:
87
+ n_layer: Number of transformer layers.
88
+ n_embd: Embedding dimension.
89
+ n_hidden: Hidden dimension size in the MLP layers.
90
+ gqa_query_heads: Number of query heads for grouped-query self-attention.
91
+ kv_heads: Number of key/value heads for grouped-query self-attention.
92
+ gqa_head_dim: Dimension per query head for grouped-query self-attention.
93
+ cross_query_heads: Number of query heads for cross-attention.
94
+ cross_head_dim: Dimension per cross-attention head.
95
+ mlp_activations: List of activation functions for the MLP layers.
96
+ use_pre_norm: Whether to use pre-normalization.
97
+ """
98
+
99
+ n_layer: int = Field(gt=0)
100
+ n_embd: int = Field(gt=0)
101
+ n_hidden: int = Field(gt=0)
102
+ gqa_query_heads: int = Field(gt=0)
103
+ kv_heads: int = Field(gt=0)
104
+ gqa_head_dim: int = Field(gt=0)
105
+ cross_query_heads: int = Field(gt=0)
106
+ cross_head_dim: int = Field(gt=0)
107
+ mlp_activations: list[str] = Field(default=["silu", "linear"])
108
+ use_pre_norm: bool = Field(default=False)
109
+
110
+
111
+ class ModelConfig(BaseModel, frozen=True):
112
+ """Main configuration container for the Dia model architecture.
113
+
114
+ Attributes:
115
+ encoder: Configuration for the encoder component.
116
+ decoder: Configuration for the decoder component.
117
+ src_vocab_size: Size of the source (text) vocabulary.
118
+ tgt_vocab_size: Size of the target (audio code) vocabulary.
119
+ dropout: Dropout probability applied within the model.
120
+ normalization_layer_epsilon: Epsilon value for normalization layers (e.g., LayerNorm).
121
+ weight_dtype: Data type for model weights (e.g., "float32", "bfloat16").
122
+ rope_min_timescale: Minimum timescale for Rotary Positional Embeddings (RoPE).
123
+ rope_max_timescale: Maximum timescale for Rotary Positional Embeddings (RoPE).
124
+ """
125
+
126
+ encoder: EncoderConfig
127
+ decoder: DecoderConfig
128
+ src_vocab_size: int = Field(default=128, gt=0)
129
+ tgt_vocab_size: int = Field(default=1028, gt=0)
130
+ dropout: float = Field(default=0.0, ge=0.0, lt=1.0)
131
+ normalization_layer_epsilon: float = Field(default=1.0e-5, ge=0.0)
132
+ weight_dtype: str = Field(default="float32", description="Weight precision")
133
+ rope_min_timescale: int = Field(default=1, description="Timescale For global Attention")
134
+ rope_max_timescale: int = Field(default=10_000, description="Timescale For global Attention")
135
+
136
+
137
+ class TrainingConfig(BaseModel, frozen=True):
138
+ """Training process configuration and hyperparameters.
139
+
140
+ Note: This configuration currently only includes precision settings.
141
+ Other training parameters (like batch size, learning rate, optimizer settings)
142
+ are assumed to be handled externally.
143
+
144
+ Attributes:
145
+ dtype: Data type for activations during training (e.g., "bfloat16", "float32").
146
+ logits_dot_in_fp32: Whether to compute the final logits dot product in fp32 for stability.
147
+ """
148
+
149
+ dtype: str = Field(default="bfloat16", description="Activation precision")
150
+ logits_dot_in_fp32: bool = Field(default=False)
151
+
152
+
153
+ class DiaConfig(BaseModel, frozen=True):
154
+ """Master configuration for the Dia model.
155
+
156
+ Combines all sub-configurations into a single validated object.
157
+
158
+ Attributes:
159
+ version: Configuration version string.
160
+ model: Model architecture configuration.
161
+ training: Training process configuration (precision settings).
162
+ data: Data loading and processing configuration.
163
+ """
164
+
165
+ version: str = Field(default="1.0")
166
+ model: ModelConfig
167
+ training: TrainingConfig
168
+ data: DataConfig
169
+
170
+ def save(self, path: str) -> None:
171
+ """Save the current configuration instance to a JSON file.
172
+
173
+ Ensures the parent directory exists and the file has a .json extension.
174
+
175
+ Args:
176
+ path: The target file path to save the configuration.
177
+
178
+ Raises:
179
+ ValueError: If the path is not a file with a .json extension.
180
+ """
181
+ os.makedirs(os.path.dirname(path), exist_ok=True)
182
+ config_json = self.model_dump_json(indent=2)
183
+ with open(path, "w") as f:
184
+ f.write(config_json)
185
+
186
+ @classmethod
187
+ def load(cls, path: str) -> "DiaConfig | None":
188
+ """Load and validate a Dia configuration from a JSON file.
189
+
190
+ Args:
191
+ path: The path to the configuration file.
192
+
193
+ Returns:
194
+ A validated DiaConfig instance if the file exists and is valid,
195
+ otherwise None if the file is not found.
196
+
197
+ Raises:
198
+ ValueError: If the path does not point to an existing .json file.
199
+ pydantic.ValidationError: If the JSON content fails validation against the DiaConfig schema.
200
+ """
201
+ try:
202
+ with open(path, "r") as f:
203
+ content = f.read()
204
+ return cls.model_validate_json(content)
205
+ except FileNotFoundError:
206
+ return None
dia/layers.py ADDED
@@ -0,0 +1,909 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ from torch import Tensor
7
+ from torch.nn import RMSNorm
8
+
9
+ from .config import DiaConfig
10
+
11
+
12
+ def _normalize_axes(axes: tuple[int, ...], ndim: int) -> tuple[int, ...]:
13
+ return tuple(ax if ax >= 0 else ndim + ax for ax in axes)
14
+
15
+
16
+ def _str_to_dtype(dtype_str: str) -> torch.dtype | None:
17
+ # Allow None for default behavior
18
+ if dtype_str is None or dtype_str.lower() == "none":
19
+ return None
20
+ if dtype_str == "float32":
21
+ return torch.float32
22
+ elif dtype_str == "float16":
23
+ return torch.float16
24
+ elif dtype_str == "bfloat16":
25
+ return torch.bfloat16
26
+ else:
27
+ raise ValueError(f"Unsupported dtype string: {dtype_str}")
28
+
29
+
30
+ class DenseGeneral(nn.Module):
31
+ """
32
+ PyTorch equivalent of flax.linen.DenseGeneral with shapes defined at init.
33
+
34
+ Stores weights (`kernel`) in the same layout as Jax and uses torch.tensordot
35
+ for the generalized matrix multiplication. Weight/bias shapes are calculated
36
+ and parameters created during initialization based on config.
37
+ `load_weights` validates shapes and copies data.
38
+
39
+ Attributes:
40
+ axis (Tuple[int, ...]): Input axis or axes to contract.
41
+ in_shapes (Tuple[int, ...]): Sizes of the input dimensions specified by `axis`.
42
+ out_features (Tuple[int, ...]): Shape of the output features (non-contracted dims).
43
+ use_bias (bool): Whether to add a bias term.
44
+ weight (nn.Parameter): The kernel parameter.
45
+ bias (Optional[nn.Parameter]): The bias parameter (if use_bias=True).
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ in_shapes: tuple[int, ...],
51
+ out_features: tuple[int, ...],
52
+ axis: tuple[int, ...] = (-1,),
53
+ dtype: torch.dtype | None = None,
54
+ weight_dtype: torch.dtype | None = None,
55
+ device: torch.device | None = None,
56
+ ):
57
+ super().__init__()
58
+ self.in_shapes = in_shapes
59
+ self.out_features = out_features
60
+ self.axis = axis
61
+ self.dtype = dtype
62
+ self.kernel_shape = self.in_shapes + self.out_features
63
+
64
+ factory_kwargs = {"device": device, "dtype": weight_dtype}
65
+ self.weight = nn.Parameter(torch.empty(self.kernel_shape, **factory_kwargs))
66
+ self.register_parameter("bias", None)
67
+
68
+ def forward(self, inputs: Tensor) -> Tensor:
69
+ norm_axis = _normalize_axes(self.axis, inputs.ndim)
70
+ kernel_contract_axes = tuple(range(len(norm_axis)))
71
+
72
+ output = torch.tensordot(
73
+ inputs.float(),
74
+ self.weight.float(),
75
+ dims=(norm_axis, kernel_contract_axes),
76
+ ).to(inputs.dtype)
77
+ return output
78
+
79
+
80
+ def get_activation_fn(activation_string: str) -> nn.Module: # Return Module instance
81
+ """Maps activation string to PyTorch activation function module."""
82
+ if activation_string == "gelu":
83
+ return nn.GELU()
84
+ elif activation_string == "relu":
85
+ return nn.ReLU()
86
+ elif activation_string == "silu" or activation_string == "swish":
87
+ return nn.SiLU()
88
+ elif activation_string == "linear":
89
+ return nn.Identity()
90
+ else:
91
+ raise ValueError(f"Unsupported activation function: {activation_string}")
92
+
93
+
94
+ class MlpBlock(nn.Module):
95
+ """MLP block using DenseGeneral."""
96
+
97
+ def __init__(
98
+ self,
99
+ config: DiaConfig,
100
+ embed_dim: int,
101
+ intermediate_dim: int,
102
+ dropout_rate: float,
103
+ activations: list[str] = ["silu", "linear"],
104
+ use_pre_norm: bool = False,
105
+ ):
106
+ super().__init__()
107
+ self.use_pre_norm = use_pre_norm
108
+ num_activations = len(activations)
109
+ compute_dtype = _str_to_dtype(config.training.dtype)
110
+ weight_dtype = _str_to_dtype(config.model.weight_dtype)
111
+ self.dtype = compute_dtype
112
+ # Assume default device for now, could be passed in config
113
+
114
+ if use_pre_norm:
115
+ self.pre_norm = RMSNorm(
116
+ embed_dim,
117
+ eps=config.model.normalization_layer_epsilon,
118
+ dtype=torch.float32,
119
+ )
120
+
121
+ self.wi_fused = DenseGeneral(
122
+ in_shapes=(embed_dim,),
123
+ out_features=(
124
+ num_activations,
125
+ intermediate_dim,
126
+ ),
127
+ axis=(-1,),
128
+ dtype=compute_dtype,
129
+ weight_dtype=weight_dtype,
130
+ )
131
+
132
+ self.activation_fn_0 = get_activation_fn(activations[0]) # silu
133
+ self.activation_fn_1 = get_activation_fn(activations[1]) # linear
134
+
135
+ self.dropout = nn.Dropout(dropout_rate)
136
+
137
+ # Output layer using DenseGeneral
138
+ self.wo = DenseGeneral(
139
+ in_shapes=(intermediate_dim,),
140
+ out_features=(embed_dim,),
141
+ axis=(-1,),
142
+ dtype=compute_dtype,
143
+ weight_dtype=weight_dtype,
144
+ )
145
+
146
+ def forward(self, x: torch.Tensor, deterministic: bool) -> torch.Tensor:
147
+ """Forward pass."""
148
+ if self.use_pre_norm and hasattr(self, "pre_norm"):
149
+ x = self.pre_norm(x)
150
+
151
+ fused_x = self.wi_fused(x)
152
+
153
+ gate_input = fused_x[..., 0, :]
154
+ up_input = fused_x[..., 1, :]
155
+
156
+ gate = self.activation_fn_0(gate_input)
157
+ up = self.activation_fn_1(up_input)
158
+ hidden = torch.mul(gate, up).to(self.dtype)
159
+
160
+ if not deterministic:
161
+ hidden = self.dropout(hidden)
162
+
163
+ output = self.wo(hidden)
164
+ return output
165
+
166
+
167
+ class RotaryEmbedding(nn.Module):
168
+ """Rotary Position Embedding (RoPE) implementation in PyTorch."""
169
+
170
+ def __init__(
171
+ self,
172
+ embedding_dims: int,
173
+ min_timescale: int = 1,
174
+ max_timescale: int = 10000,
175
+ dtype: torch.dtype = torch.float32,
176
+ ):
177
+ super().__init__()
178
+ if embedding_dims % 2 != 0:
179
+ raise ValueError("Embedding dim must be even for RoPE.")
180
+ self.embedding_dims = embedding_dims
181
+ self.min_timescale = min_timescale
182
+ self.max_timescale = max_timescale
183
+ self.dtype = dtype
184
+
185
+ half_embedding_dim = embedding_dims // 2
186
+ fraction = (2.0 * torch.arange(0, half_embedding_dim)) / embedding_dims
187
+ self.register_buffer(
188
+ "timescale",
189
+ self.min_timescale * (self.max_timescale / self.min_timescale) ** fraction,
190
+ persistent=False,
191
+ )
192
+
193
+ def extra_repr(self) -> str:
194
+ s = f"{self.timescale.shape}"
195
+ return s
196
+
197
+ def forward(self, inputs: torch.Tensor, position: torch.Tensor):
198
+ """Applies RoPE."""
199
+ position = position.unsqueeze(-1).unsqueeze(-1)
200
+ timescale = self.timescale.to(inputs.device)
201
+ sinusoid_inp = position / timescale
202
+ sin = torch.sin(sinusoid_inp).to(inputs.dtype)
203
+ cos = torch.cos(sinusoid_inp).to(inputs.dtype)
204
+ first_half, second_half = torch.chunk(inputs, 2, dim=-1)
205
+ first_part = first_half * cos - second_half * sin
206
+ second_part = second_half * cos + first_half * sin
207
+ return torch.cat((first_part, second_part), dim=-1)
208
+
209
+
210
+ class KVCache:
211
+ def __init__(self, num_heads, max_len, head_dim, device, k=None, v=None):
212
+ self.k = torch.zeros((2, num_heads, max_len, head_dim), device=device) if k is None else k
213
+ self.v = torch.zeros((2, num_heads, max_len, head_dim), device=device) if v is None else v
214
+ self.current_idx = 0
215
+ self.max_len = max_len
216
+
217
+ def get_kv_for_attention(self, current_k, current_v):
218
+ if self.current_idx == 0:
219
+ return current_k, current_v
220
+ else:
221
+ past_k = self.k[:, :, : self.current_idx, :]
222
+ past_v = self.v[:, :, : self.current_idx, :]
223
+ attn_k = torch.cat((past_k, current_k), dim=2)
224
+ attn_v = torch.cat((past_v, current_v), dim=2)
225
+ return attn_k, attn_v
226
+
227
+ def update_cache(self, k, v):
228
+ assert self.current_idx < self.max_len
229
+ self.k[:, :, self.current_idx : self.current_idx + 1, :] = k
230
+ self.v[:, :, self.current_idx : self.current_idx + 1, :] = v
231
+ self.current_idx += 1
232
+
233
+ def prefill_kv(self, k, v):
234
+ prefill_len = k.shape[2]
235
+ assert prefill_len <= self.max_len
236
+ self.k[:, :, :prefill_len, :] = k
237
+ self.v[:, :, :prefill_len, :] = v
238
+ self.current_idx = prefill_len
239
+
240
+
241
+ class Attention(nn.Module):
242
+ """Attention using DenseGeneral."""
243
+
244
+ def __init__(
245
+ self,
246
+ config: DiaConfig,
247
+ q_embed_dim: int,
248
+ kv_embed_dim: int,
249
+ num_query_heads: int,
250
+ num_kv_heads: int,
251
+ head_dim: int,
252
+ dropout_rate: float,
253
+ is_cross_attn: bool = False,
254
+ out_embed_dim: int | None = None,
255
+ ):
256
+ super().__init__()
257
+ self.num_query_heads = num_query_heads
258
+ self.num_kv_heads = num_kv_heads
259
+ self.head_dim = head_dim
260
+ self.is_cross_attn = is_cross_attn
261
+ self.dropout_rate = dropout_rate
262
+ compute_dtype = _str_to_dtype(config.training.dtype)
263
+ weight_dtype = _str_to_dtype(config.model.weight_dtype)
264
+ self.output_dim = out_embed_dim if out_embed_dim is not None else q_embed_dim
265
+ self.projected_query_dim = num_query_heads * head_dim
266
+ if num_query_heads % num_kv_heads != 0:
267
+ raise ValueError(f"num_query_heads ({num_query_heads}) must be divisible by num_kv_heads ({num_kv_heads})")
268
+ self.num_gqa_groups = num_query_heads // num_kv_heads
269
+
270
+ # --- Projection Layers using DenseGeneral ---
271
+ self.q_proj = DenseGeneral(
272
+ in_shapes=(q_embed_dim,),
273
+ out_features=(num_query_heads, head_dim),
274
+ axis=(-1,),
275
+ dtype=compute_dtype,
276
+ weight_dtype=weight_dtype,
277
+ )
278
+ self.k_proj = DenseGeneral(
279
+ in_shapes=(kv_embed_dim,),
280
+ out_features=(num_kv_heads, head_dim),
281
+ axis=(-1,),
282
+ dtype=compute_dtype,
283
+ weight_dtype=weight_dtype,
284
+ )
285
+ self.v_proj = DenseGeneral(
286
+ in_shapes=(kv_embed_dim,),
287
+ out_features=(num_kv_heads, head_dim),
288
+ axis=(-1,),
289
+ dtype=compute_dtype,
290
+ weight_dtype=weight_dtype,
291
+ )
292
+ self.o_proj = DenseGeneral(
293
+ in_shapes=(num_query_heads, head_dim),
294
+ out_features=(self.output_dim,),
295
+ axis=(-2, -1),
296
+ dtype=compute_dtype,
297
+ weight_dtype=weight_dtype,
298
+ )
299
+
300
+ # --- Rotary Embedding ---
301
+ self.rotary_emb = RotaryEmbedding(
302
+ embedding_dims=self.head_dim,
303
+ min_timescale=config.model.rope_min_timescale,
304
+ max_timescale=config.model.rope_max_timescale,
305
+ dtype=compute_dtype,
306
+ )
307
+
308
+ def forward(
309
+ self,
310
+ Xq: torch.Tensor, # (B, T, D) T = 1 in AR generation
311
+ Xkv: torch.Tensor, # (B, S, E) S = 1 in AR generation
312
+ q_positions: torch.Tensor, # (B, T)
313
+ kv_positions: torch.Tensor | None = None, # (B, S)
314
+ deterministic: bool = True,
315
+ attn_mask: torch.Tensor | None = None, # None in Decoder Self Attention, Valid mask in Others
316
+ cache: KVCache | None = None, # None in Encoder, KVCache in Decoder
317
+ prefill: bool = False, # True only when prefilling KV Cache
318
+ ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor] | None]:
319
+ """
320
+ Performs attention calculation with optional KV caching.
321
+
322
+ Args:
323
+ Xq: Query tensor (B, T, D). T=1 during single-step decoding.
324
+ Xkv: Key/Value source tensor (B, S, E). S=1 during single-step decoding for self-attn.
325
+ q_positions: Positions for queries (B, T).
326
+ kv_positions: Positions for keys/values (B, S). If None, uses q_positions.
327
+ deterministic: If True, disable dropout.
328
+ attn_mask: Attention mask.
329
+ cache: KVCache.
330
+ prefill: If True, use prefill mode.
331
+
332
+ Returns:
333
+ A tuple containing:
334
+ - output: The attention output tensor (B, T, output_dim).
335
+ - present_kv: The K/V state to be cached for the next step ((B, N, S_new, H), (B, N, S_new, H)). For self-attn, S_new = S_past + S. For cross-attn, S_new = S_kv.
336
+ """
337
+ if kv_positions is None:
338
+ kv_positions = q_positions
339
+ original_dtype = Xq.dtype
340
+
341
+ Xq_BxTxNxH = self.q_proj(Xq)
342
+ Xq_BxTxNxH = self.rotary_emb(Xq_BxTxNxH, position=q_positions)
343
+ Xq_BxNxTxH = Xq_BxTxNxH.transpose(1, 2)
344
+
345
+ # Input values into attention calculation
346
+ attn_k: torch.Tensor | None = None
347
+ attn_v: torch.Tensor | None = None
348
+ new_kv_cache: tuple[torch.Tensor, torch.Tensor] | None = None
349
+
350
+ # Decoder Cross Attention
351
+ if self.is_cross_attn:
352
+ # Directly use cache (no need to check index)
353
+ attn_k, attn_v = cache.k, cache.v
354
+ if attn_k.shape[1] != self.num_query_heads or attn_v.shape[1] != self.num_query_heads:
355
+ raise ValueError(
356
+ f"Cross-attention cache head dimension ({attn_k.shape[1]}) "
357
+ f"does not match num_query_heads ({self.num_query_heads}). "
358
+ "Cache should be pre-repeated for GQA."
359
+ )
360
+ # Self Attention
361
+ else:
362
+ Xk_BxSxKxH = self.k_proj(Xkv) # (B, S, K, H)
363
+ Xv_BxSxKxH = self.v_proj(Xkv) # (B, S, K, H)
364
+ Xk_BxSxKxH = self.rotary_emb(Xk_BxSxKxH, position=kv_positions) # (B, S, K, H)
365
+
366
+ Xk_BxKxSxH = Xk_BxSxKxH.transpose(1, 2) # (B, K, S, H)
367
+ Xv_BxKxSxH = Xv_BxSxKxH.transpose(1, 2) # (B, K, S, H)
368
+ # S=1 for Decode Step
369
+
370
+ if self.num_gqa_groups > 1:
371
+ Xk_BxNxSxH = Xk_BxKxSxH.repeat_interleave(self.num_gqa_groups, dim=1)
372
+ Xv_BxNxSxH = Xv_BxKxSxH.repeat_interleave(self.num_gqa_groups, dim=1)
373
+ else:
374
+ Xk_BxNxSxH = Xk_BxKxSxH
375
+ Xv_BxNxSxH = Xv_BxKxSxH
376
+
377
+ # Encoder Self Attention
378
+ if cache is None:
379
+ attn_k = Xk_BxNxSxH
380
+ attn_v = Xv_BxNxSxH
381
+ # Decoder Self Attention
382
+ else:
383
+ # In prefill mode, we fill in cache until prefill length
384
+ if prefill:
385
+ attn_k, attn_v = Xk_BxNxSxH, Xv_BxNxSxH
386
+ cache.prefill_kv(attn_k, attn_v)
387
+ # In decode step, we add current K/V to cache step by step
388
+ else:
389
+ new_kv_cache = Xk_BxNxSxH, Xv_BxNxSxH
390
+ attn_k, attn_v = cache.get_kv_for_attention(Xk_BxNxSxH, Xv_BxNxSxH)
391
+
392
+ attn_output = F.scaled_dot_product_attention(
393
+ Xq_BxNxTxH,
394
+ attn_k,
395
+ attn_v,
396
+ attn_mask=attn_mask,
397
+ dropout_p=self.dropout_rate if not deterministic else 0.0,
398
+ scale=1.0,
399
+ )
400
+
401
+ attn_output = attn_output.transpose(1, 2).contiguous() # (B, T, N, H)
402
+ output = self.o_proj(attn_output)
403
+
404
+ return output.to(original_dtype), new_kv_cache
405
+
406
+
407
+ class EncoderLayer(nn.Module):
408
+ """Transformer Encoder Layer using DenseGeneral."""
409
+
410
+ def __init__(self, config: DiaConfig):
411
+ super().__init__()
412
+ self.config = config
413
+ model_config = config.model
414
+ enc_config = config.model.encoder
415
+ embed_dim = enc_config.n_embd
416
+
417
+ self.pre_sa_norm = RMSNorm(
418
+ embed_dim,
419
+ eps=model_config.normalization_layer_epsilon,
420
+ dtype=torch.float32,
421
+ )
422
+ self.self_attention = Attention(
423
+ config=config,
424
+ q_embed_dim=embed_dim,
425
+ kv_embed_dim=embed_dim,
426
+ num_query_heads=enc_config.n_head,
427
+ num_kv_heads=enc_config.n_head,
428
+ head_dim=enc_config.head_dim,
429
+ dropout_rate=model_config.dropout,
430
+ is_cross_attn=False,
431
+ out_embed_dim=embed_dim,
432
+ )
433
+ self.post_sa_norm = RMSNorm(
434
+ embed_dim,
435
+ eps=model_config.normalization_layer_epsilon,
436
+ dtype=torch.float32,
437
+ )
438
+ self.mlp = MlpBlock(
439
+ config=config,
440
+ embed_dim=embed_dim,
441
+ intermediate_dim=enc_config.n_hidden,
442
+ activations=enc_config.mlp_activations,
443
+ dropout_rate=model_config.dropout,
444
+ use_pre_norm=enc_config.use_pre_norm,
445
+ )
446
+ self.dropout = nn.Dropout(model_config.dropout)
447
+
448
+ def forward(
449
+ self,
450
+ x: torch.Tensor,
451
+ src_positions: torch.Tensor | None = None,
452
+ deterministic: bool = True,
453
+ attn_mask: torch.Tensor | None = None,
454
+ ) -> torch.Tensor:
455
+ residual = x
456
+ x_norm = self.pre_sa_norm(x)
457
+
458
+ sa_out, _ = self.self_attention(
459
+ Xq=x_norm,
460
+ Xkv=x_norm,
461
+ q_positions=src_positions,
462
+ kv_positions=src_positions,
463
+ deterministic=deterministic,
464
+ attn_mask=attn_mask,
465
+ )
466
+ x = residual + sa_out
467
+
468
+ residual = x
469
+ x_norm = self.post_sa_norm(x)
470
+ mlp_out = self.mlp(x_norm, deterministic=deterministic)
471
+ x = residual + mlp_out
472
+
473
+ if not deterministic:
474
+ x = self.dropout(x)
475
+ return x
476
+
477
+
478
+ class Encoder(nn.Module):
479
+ """Transformer Encoder Stack using DenseGeneral."""
480
+
481
+ def __init__(self, config: DiaConfig):
482
+ super().__init__()
483
+ self.config = config
484
+ model_config = config.model
485
+ enc_config = config.model.encoder
486
+ compute_dtype = _str_to_dtype(config.training.dtype)
487
+
488
+ self.embedding = nn.Embedding(
489
+ model_config.src_vocab_size,
490
+ enc_config.n_embd,
491
+ dtype=compute_dtype,
492
+ )
493
+ self.dropout = nn.Dropout(model_config.dropout)
494
+ self.layers = nn.ModuleList([EncoderLayer(config=config) for _ in range(enc_config.n_layer)])
495
+ self.norm = RMSNorm(
496
+ enc_config.n_embd,
497
+ eps=model_config.normalization_layer_epsilon,
498
+ dtype=torch.float32,
499
+ )
500
+
501
+ def forward(
502
+ self,
503
+ x_ids: torch.Tensor,
504
+ src_positions: torch.Tensor | None = None,
505
+ deterministic: bool = True,
506
+ attn_mask: torch.Tensor | None = None,
507
+ ) -> torch.Tensor:
508
+ x = self.embedding(x_ids)
509
+
510
+ if not deterministic:
511
+ x = self.dropout(x)
512
+
513
+ for layer in self.layers:
514
+ x = layer(
515
+ x,
516
+ src_positions=src_positions,
517
+ deterministic=deterministic,
518
+ attn_mask=attn_mask,
519
+ )
520
+ x = self.norm(x)
521
+ if not deterministic:
522
+ x = self.dropout(x)
523
+ return x
524
+
525
+
526
+ class DecoderLayer(nn.Module):
527
+ """Transformer Decoder Layer using DenseGeneral."""
528
+
529
+ def __init__(self, config: DiaConfig):
530
+ super().__init__()
531
+ self.config = config
532
+ model_config = config.model
533
+ dec_config = config.model.decoder
534
+ enc_config = config.model.encoder
535
+ dec_embed_dim = dec_config.n_embd
536
+ enc_embed_dim = enc_config.n_embd
537
+
538
+ # Norms
539
+ self.pre_sa_norm = RMSNorm(
540
+ dec_embed_dim,
541
+ eps=model_config.normalization_layer_epsilon,
542
+ dtype=torch.float32,
543
+ )
544
+ self.pre_ca_norm = RMSNorm(
545
+ dec_embed_dim,
546
+ eps=model_config.normalization_layer_epsilon,
547
+ dtype=torch.float32,
548
+ )
549
+ self.pre_mlp_norm = RMSNorm(
550
+ dec_embed_dim,
551
+ eps=model_config.normalization_layer_epsilon,
552
+ dtype=torch.float32,
553
+ )
554
+
555
+ # Self-Attention (GQA) with Causal Masking
556
+ self.self_attention = Attention(
557
+ config=config,
558
+ q_embed_dim=dec_embed_dim,
559
+ kv_embed_dim=dec_embed_dim,
560
+ num_query_heads=dec_config.gqa_query_heads,
561
+ num_kv_heads=dec_config.kv_heads,
562
+ head_dim=dec_config.gqa_head_dim,
563
+ dropout_rate=model_config.dropout,
564
+ is_cross_attn=False,
565
+ out_embed_dim=dec_embed_dim,
566
+ )
567
+ # Cross-Attention (MHA)
568
+ self.cross_attention = Attention(
569
+ config=config,
570
+ q_embed_dim=dec_embed_dim,
571
+ kv_embed_dim=enc_embed_dim, # Note kv_embed_dim
572
+ num_query_heads=dec_config.cross_query_heads,
573
+ num_kv_heads=dec_config.cross_query_heads,
574
+ head_dim=dec_config.cross_head_dim,
575
+ dropout_rate=model_config.dropout,
576
+ is_cross_attn=True,
577
+ out_embed_dim=dec_embed_dim,
578
+ )
579
+ # MLP
580
+ self.mlp = MlpBlock(
581
+ config=config,
582
+ embed_dim=dec_embed_dim,
583
+ intermediate_dim=dec_config.n_hidden,
584
+ activations=dec_config.mlp_activations,
585
+ dropout_rate=model_config.dropout,
586
+ use_pre_norm=dec_config.use_pre_norm,
587
+ )
588
+
589
+ def forward(
590
+ self,
591
+ x: torch.Tensor,
592
+ encoder_out: torch.Tensor,
593
+ tgt_positions: torch.Tensor,
594
+ src_positions: torch.Tensor | None,
595
+ deterministic: bool,
596
+ self_attn_mask: torch.Tensor,
597
+ cross_attn_mask: torch.Tensor,
598
+ self_attn_cache: KVCache,
599
+ cross_attn_cache: KVCache,
600
+ prefill: bool = False,
601
+ ) -> torch.Tensor:
602
+ residual = x
603
+ x_norm = self.pre_sa_norm(x)
604
+
605
+ sa_out, new_kv_cache = self.self_attention(
606
+ Xq=x_norm, # (2, 1, D)
607
+ Xkv=x_norm, # (2, 1, D)
608
+ q_positions=tgt_positions, # (2, 1)
609
+ kv_positions=tgt_positions, # (2, 1)
610
+ deterministic=deterministic,
611
+ attn_mask=self_attn_mask, # (2, 1, 1, S_max)
612
+ cache=self_attn_cache,
613
+ prefill=prefill,
614
+ )
615
+
616
+ x = residual + sa_out
617
+
618
+ # 2. Cross-Attention
619
+ residual = x
620
+ x_norm = self.pre_ca_norm(x)
621
+ ca_out, _ = self.cross_attention(
622
+ Xq=x_norm,
623
+ Xkv=encoder_out,
624
+ q_positions=tgt_positions,
625
+ kv_positions=src_positions,
626
+ deterministic=deterministic,
627
+ attn_mask=cross_attn_mask,
628
+ cache=cross_attn_cache,
629
+ )
630
+ x = residual + ca_out
631
+
632
+ # 3. MLP
633
+ residual = x
634
+ x_norm = self.pre_mlp_norm(x)
635
+ mlp_out = self.mlp(x_norm, deterministic=deterministic)
636
+ x = residual + mlp_out
637
+
638
+ return x, new_kv_cache
639
+
640
+
641
+ class Decoder(nn.Module):
642
+ """Transformer Decoder Stack using DenseGeneral."""
643
+
644
+ def __init__(self, config: DiaConfig):
645
+ super().__init__()
646
+ self.config = config
647
+ model_config = config.model
648
+ dec_config = config.model.decoder
649
+ train_config = config.training
650
+ data_config = config.data
651
+ compute_dtype = _str_to_dtype(config.training.dtype)
652
+ weight_dtype = _str_to_dtype(config.model.weight_dtype)
653
+ self.num_channels = data_config.channels
654
+ self.num_layers = dec_config.n_layer
655
+
656
+ self.embeddings = nn.ModuleList(
657
+ [
658
+ nn.Embedding(model_config.tgt_vocab_size, dec_config.n_embd, dtype=compute_dtype)
659
+ for _ in range(self.num_channels)
660
+ ]
661
+ )
662
+ self.dropout = nn.Dropout(model_config.dropout)
663
+ self.layers = nn.ModuleList([DecoderLayer(config=config) for _ in range(self.num_layers)])
664
+ self.norm = RMSNorm(
665
+ dec_config.n_embd,
666
+ eps=model_config.normalization_layer_epsilon,
667
+ dtype=torch.float32,
668
+ )
669
+
670
+ # Final Logits Projection using DenseGeneral
671
+ self.logits_dense = DenseGeneral(
672
+ in_shapes=(dec_config.n_embd,),
673
+ out_features=(self.num_channels, model_config.tgt_vocab_size),
674
+ axis=(-1,),
675
+ dtype=(torch.float32 if train_config.logits_dot_in_fp32 else compute_dtype),
676
+ weight_dtype=weight_dtype,
677
+ )
678
+ self.logits_in_fp32 = train_config.logits_dot_in_fp32
679
+
680
+ def precompute_cross_attention_kv(
681
+ self,
682
+ max_len: int,
683
+ encoder_out: torch.Tensor, # (B, S, E)
684
+ src_positions: torch.Tensor | None, # (B, S)
685
+ ) -> list[KVCache]:
686
+ """
687
+ Computes the Key and Value tensors for cross-attention for each layer from the encoder output.
688
+ """
689
+ per_layer_kv_cache: list[KVCache] = []
690
+
691
+ for layer in self.layers:
692
+ cross_attn_module = layer.cross_attention
693
+ k_proj = cross_attn_module.k_proj(encoder_out)
694
+ v_proj = cross_attn_module.v_proj(encoder_out)
695
+
696
+ k_proj = cross_attn_module.rotary_emb(k_proj, position=src_positions)
697
+ k = k_proj.transpose(1, 2)
698
+ v = v_proj.transpose(1, 2)
699
+
700
+ per_layer_kv_cache.append(
701
+ KVCache(
702
+ cross_attn_module.num_kv_heads,
703
+ max_len,
704
+ cross_attn_module.head_dim,
705
+ k.device,
706
+ k=k,
707
+ v=v,
708
+ )
709
+ )
710
+
711
+ return per_layer_kv_cache
712
+
713
+ def decode_step(
714
+ self,
715
+ tgt_ids_Bx1xC: torch.Tensor, # [B, 1, C]
716
+ tgt_pos_Bx1: torch.Tensor, # [B, 1]
717
+ encoder_out: torch.Tensor, # [B, S, E]
718
+ self_attn_mask: Any, # None
719
+ cross_attn_mask: torch.Tensor, # [B, 1, 1, S]
720
+ self_attention_cache: list[KVCache],
721
+ cross_attention_cache: list[KVCache],
722
+ ) -> torch.Tensor:
723
+ """
724
+ Performs a single decoding step, managing KV caches layer by layer.
725
+
726
+ Returns:
727
+ A tuple containing:
728
+ - logits_Bx1xCV: The final output logits for the current step (B, 1, C*V), cast to float32.
729
+ """
730
+ assert self_attn_mask is None, "Self-attention mask should be None, kept for pattern"
731
+
732
+ x = None
733
+ for i in range(self.num_channels):
734
+ channel_tokens = tgt_ids_Bx1xC[..., i]
735
+ channel_embed = self.embeddings[i](channel_tokens)
736
+ x = channel_embed if x is None else x + channel_embed
737
+
738
+ new_cache = []
739
+
740
+ for i, layer in enumerate(self.layers):
741
+ self_cache = self_attention_cache[i]
742
+ cross_cache = cross_attention_cache[i]
743
+ x, new_kv_cache = layer(
744
+ x, # (2, 1, D)
745
+ encoder_out, # (2, S, E)
746
+ src_positions=None, # CA KV is already computed
747
+ tgt_positions=tgt_pos_Bx1, # (2, 1)
748
+ deterministic=True,
749
+ self_attn_mask=None,
750
+ cross_attn_mask=cross_attn_mask,
751
+ self_attn_cache=self_cache,
752
+ cross_attn_cache=cross_cache,
753
+ )
754
+ new_cache.append(new_kv_cache)
755
+
756
+ x = self.norm(x)
757
+ logits_Bx1xCxV = self.logits_dense(x)
758
+
759
+ return logits_Bx1xCxV.to(torch.float32), new_cache
760
+
761
+ def forward(
762
+ self,
763
+ tgt_ids_BxTxC: torch.Tensor,
764
+ encoder_out: torch.Tensor,
765
+ tgt_positions: torch.Tensor,
766
+ src_positions: torch.Tensor,
767
+ deterministic: bool,
768
+ self_attn_mask: torch.Tensor,
769
+ cross_attn_mask: torch.Tensor,
770
+ self_attention_cache: list[KVCache],
771
+ cross_attention_cache: list[KVCache],
772
+ ) -> torch.Tensor:
773
+ """
774
+ Forward pass for the Decoder stack, managing KV caches.
775
+
776
+ Args:
777
+ tgt_ids_BxTxC: Target token IDs (B, T, C).
778
+ encoder_out: Output from the encoder (B, S, E).
779
+ tgt_positions: Positions for target sequence (B, T).
780
+ src_positions: Positions for source sequence (B, S).
781
+ deterministic: Disable dropout if True.
782
+ self_attn_mask: Mask for self-attention.
783
+ cross_attn_mask: Mask for cross-attention.
784
+ past_key_values: List containing the self-attention KV cache for each layer
785
+ from the previous decoding step. `len(past_key_values)` should
786
+ equal `num_layers`.
787
+ precomputed_cross_attn_kv: A single tuple containing the pre-computed K/V cache
788
+ derived from `encoder_out`. This is passed identically
789
+ to all layers.
790
+
791
+ Returns:
792
+ A tuple containing:
793
+ - logits: The final output logits (B, T, C * V), cast to float32.
794
+ - present_key_values: A list containing the updated self-attention KV cache
795
+ for each layer for the *current* decoding step.
796
+ """
797
+ _, _, num_channels_in = tgt_ids_BxTxC.shape
798
+ assert num_channels_in == self.num_channels, "Input channels mismatch"
799
+
800
+ # Embeddings
801
+ x = None
802
+ for i in range(self.num_channels):
803
+ channel_tokens = tgt_ids_BxTxC[..., i]
804
+ channel_embed = self.embeddings[i](channel_tokens)
805
+ x = channel_embed if x is None else x + channel_embed
806
+
807
+ if not deterministic:
808
+ x = self.dropout(x)
809
+
810
+ for i, layer in enumerate(self.layers):
811
+ x, _ = layer(
812
+ x,
813
+ encoder_out,
814
+ tgt_positions=tgt_positions,
815
+ src_positions=src_positions,
816
+ deterministic=deterministic,
817
+ self_attn_mask=self_attn_mask,
818
+ cross_attn_mask=cross_attn_mask,
819
+ self_attn_cache=self_attention_cache[i],
820
+ cross_attn_cache=cross_attention_cache[i],
821
+ prefill=True,
822
+ )
823
+
824
+ # Final Norm
825
+ x = self.norm(x)
826
+ logits_BxTxCxV = self.logits_dense(x)
827
+
828
+ return logits_BxTxCxV.to(torch.float32)
829
+
830
+
831
+ class DiaModel(nn.Module):
832
+ """PyTorch Dia Model using DenseGeneral."""
833
+
834
+ def __init__(self, config: DiaConfig):
835
+ super().__init__()
836
+ self.config = config
837
+ self.encoder = Encoder(config)
838
+ self.decoder = Decoder(config)
839
+ #self._init_weights()
840
+
841
+
842
+ def _init_weights(self):
843
+ for module in self.modules():
844
+ if isinstance(module, (torch.nn.Linear, torch.nn.Conv1d)):
845
+ torch.nn.init.xavier_uniform_(module.weight)
846
+ if module.bias is not None:
847
+ torch.nn.init.zeros_(module.bias)
848
+ elif isinstance(module, torch.nn.Embedding):
849
+ torch.nn.init.xavier_uniform_(module.weight)
850
+ elif isinstance(module, torch.nn.LayerNorm) or isinstance(module, torch.nn.modules.normalization.RMSNorm):
851
+ if hasattr(module, 'weight') and module.weight is not None:
852
+ torch.nn.init.ones_(module.weight)
853
+ if hasattr(module, 'bias') and module.bias is not None:
854
+ torch.nn.init.zeros_(module.bias)
855
+
856
+ def forward(
857
+ self,
858
+ src_BxS: torch.Tensor,
859
+ tgt_BxTxC: torch.Tensor,
860
+ src_positions: torch.Tensor | None = None,
861
+ tgt_positions: torch.Tensor | None = None,
862
+ enc_self_attn_mask: torch.Tensor | None = None,
863
+ dec_self_attn_mask: torch.Tensor | None = None,
864
+ dec_cross_attn_mask: torch.Tensor | None = None,
865
+ enable_dropout: bool = True,
866
+ ):
867
+ deterministic = not enable_dropout
868
+
869
+ # --- Encoder Pass ---
870
+ encoder_out = self.encoder(
871
+ x_ids=src_BxS,
872
+ src_positions=src_positions,
873
+ deterministic=deterministic,
874
+ attn_mask=enc_self_attn_mask,
875
+ )
876
+
877
+ B, T, C = tgt_BxTxC.shape # Batch size, target sequence length, channels
878
+ device = tgt_BxTxC.device
879
+
880
+ self_attention_cache = [
881
+ KVCache(
882
+ num_heads=self.decoder.layers[i].self_attention.num_query_heads, # ✅ FIXED: use query heads!
883
+ max_len=T,
884
+ head_dim=self.decoder.layers[i].self_attention.head_dim,
885
+ device=device,
886
+ )
887
+ for i in range(self.decoder.num_layers)
888
+ ]
889
+
890
+ cross_attention_cache = self.decoder.precompute_cross_attention_kv(
891
+ max_len=encoder_out.shape[1],
892
+ encoder_out=encoder_out,
893
+ src_positions=src_positions,
894
+ )
895
+
896
+ # --- Decoder Pass ---
897
+ logits = self.decoder(
898
+ tgt_ids_BxTxC=tgt_BxTxC,
899
+ encoder_out=encoder_out,
900
+ tgt_positions=tgt_positions,
901
+ src_positions=src_positions,
902
+ deterministic=deterministic,
903
+ self_attn_mask=dec_self_attn_mask,
904
+ cross_attn_mask=dec_cross_attn_mask,
905
+ self_attention_cache=self_attention_cache,
906
+ cross_attention_cache=cross_attention_cache
907
+ )
908
+
909
+ return logits
dia/model.py ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dac
2
+ import numpy as np
3
+ import torch
4
+ import torchaudio
5
+ from huggingface_hub import hf_hub_download
6
+
7
+ from .audio import audio_to_codebook, codebook_to_audio
8
+ from .config import DiaConfig
9
+ from .layers import DiaModel, KVCache
10
+
11
+
12
+ def get_default_device():
13
+ if torch.cuda.is_available():
14
+ return torch.device("cuda")
15
+ elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
16
+ return torch.device("mps")
17
+ return torch.device("cpu")
18
+
19
+
20
+ def _sample_next_token(
21
+ logits_BCxV: torch.Tensor,
22
+ temperature: float,
23
+ top_p: float,
24
+ use_cfg_filter: bool,
25
+ cfg_filter_top_k: int | None = None,
26
+ ) -> torch.Tensor:
27
+ if temperature == 0.0:
28
+ return torch.argmax(logits_BCxV, dim=-1)
29
+
30
+ logits_BCxV = logits_BCxV / temperature
31
+ if use_cfg_filter and cfg_filter_top_k is not None:
32
+ _, top_k_indices_BCxV = torch.topk(logits_BCxV, k=cfg_filter_top_k, dim=-1)
33
+ mask = torch.ones_like(logits_BCxV, dtype=torch.bool)
34
+ mask.scatter_(dim=-1, index=top_k_indices_BCxV, value=False)
35
+ logits_BCxV = logits_BCxV.masked_fill(mask, -torch.inf)
36
+
37
+ if top_p < 1.0:
38
+ probs_BCxV = torch.softmax(logits_BCxV, dim=-1)
39
+ sorted_probs_BCxV, sorted_indices_BCxV = torch.sort(probs_BCxV, dim=-1, descending=True)
40
+ cumulative_probs_BCxV = torch.cumsum(sorted_probs_BCxV, dim=-1)
41
+
42
+ # Calculate indices to remove based on top_p
43
+ sorted_indices_to_remove_BCxV = cumulative_probs_BCxV > top_p
44
+ # Shift the mask to the right to keep the first token above the threshold
45
+ sorted_indices_to_remove_BCxV[..., 1:] = sorted_indices_to_remove_BCxV[..., :-1].clone()
46
+ sorted_indices_to_remove_BCxV[..., 0] = 0 # Always keep the most probable token
47
+
48
+ indices_to_remove_BCxV = torch.zeros_like(sorted_indices_to_remove_BCxV)
49
+ indices_to_remove_BCxV.scatter_(dim=-1, index=sorted_indices_BCxV, src=sorted_indices_to_remove_BCxV)
50
+ logits_BCxV = logits_BCxV.masked_fill(indices_to_remove_BCxV, -torch.inf)
51
+
52
+ final_probs_BCxV = torch.softmax(logits_BCxV, dim=-1)
53
+
54
+ sampled_indices_BC = torch.multinomial(final_probs_BCxV, num_samples=1)
55
+ sampled_indices_C = sampled_indices_BC.squeeze(-1)
56
+ return sampled_indices_C
57
+
58
+
59
+ class Dia:
60
+ def __init__(self, config: DiaConfig, device: torch.device | None = None):
61
+ """Initializes the Dia model.
62
+
63
+ Args:
64
+ config: The configuration object for the model.
65
+ device: The device to load the model onto. If None, will automatically select the best available device.
66
+
67
+ Raises:
68
+ RuntimeError: If there is an error loading the DAC model.
69
+ """
70
+ super().__init__()
71
+ self.config = config
72
+ self.device = device if device is not None else get_default_device()
73
+ self.model = DiaModel(config)
74
+ self.dac_model = None
75
+
76
+ @classmethod
77
+ def from_local(cls, config_path: str, checkpoint_path: str, device: torch.device | None = None) -> "Dia":
78
+ """Loads the Dia model from local configuration and checkpoint files.
79
+
80
+ Args:
81
+ config_path: Path to the configuration JSON file.
82
+ checkpoint_path: Path to the model checkpoint (.pth) file.
83
+ device: The device to load the model onto. If None, will automatically select the best available device.
84
+
85
+ Returns:
86
+ An instance of the Dia model loaded with weights and set to eval mode.
87
+
88
+ Raises:
89
+ FileNotFoundError: If the config or checkpoint file is not found.
90
+ RuntimeError: If there is an error loading the checkpoint.
91
+ """
92
+ config = DiaConfig.load(config_path)
93
+ if config is None:
94
+ raise FileNotFoundError(f"Config file not found at {config_path}")
95
+
96
+ dia = cls(config, device)
97
+
98
+ try:
99
+ state_dict = torch.load(checkpoint_path, map_location=dia.device)
100
+ dia.model.load_state_dict(state_dict)
101
+ except FileNotFoundError:
102
+ raise FileNotFoundError(f"Checkpoint file not found at {checkpoint_path}")
103
+ except Exception as e:
104
+ raise RuntimeError(f"Error loading checkpoint from {checkpoint_path}") from e
105
+
106
+ dia.model.to(dia.device)
107
+ dia.model.eval()
108
+ dia._load_dac_model()
109
+ return dia
110
+
111
+ @classmethod
112
+ def from_pretrained(
113
+ cls, model_name: str = "nari-labs/Dia-1.6B", device: torch.device | None = None
114
+ ) -> "Dia":
115
+ """Loads the Dia model from a Hugging Face Hub repository.
116
+
117
+ Downloads the configuration and checkpoint files from the specified
118
+ repository ID and then loads the model.
119
+
120
+ Args:
121
+ model_name: The Hugging Face Hub repository ID (e.g., "NariLabs/Dia-1.6B").
122
+ device: The device to load the model onto. If None, will automatically select the best available device.
123
+
124
+ Returns:
125
+ An instance of the Dia model loaded with weights and set to eval mode.
126
+
127
+ Raises:
128
+ FileNotFoundError: If config or checkpoint download/loading fails.
129
+ RuntimeError: If there is an error loading the checkpoint.
130
+ """
131
+ config_path = hf_hub_download(repo_id=model_name, filename="config.json")
132
+ checkpoint_path = hf_hub_download(repo_id=model_name, filename="dia-v0_1.pth")
133
+ return cls.from_local(config_path, checkpoint_path, device)
134
+
135
+ def _load_dac_model(self):
136
+ try:
137
+ dac_model_path = dac.utils.download()
138
+ dac_model = dac.DAC.load(dac_model_path).to(self.device)
139
+ except Exception as e:
140
+ raise RuntimeError("Failed to load DAC model") from e
141
+ self.dac_model = dac_model
142
+
143
+ def _create_attn_mask(
144
+ self,
145
+ q_padding_mask_1d: torch.Tensor,
146
+ k_padding_mask_1d: torch.Tensor,
147
+ is_causal: bool = False,
148
+ ) -> torch.Tensor:
149
+ """
150
+ Creates the attention mask (self or cross) mimicking JAX segment ID logic.
151
+ """
152
+ B1, Tq = q_padding_mask_1d.shape
153
+ B2, Tk = k_padding_mask_1d.shape
154
+ assert B1 == B2, "Query and key batch dimensions must match"
155
+
156
+ p_mask_q = q_padding_mask_1d.unsqueeze(2) # Shape [B, Tq, 1]
157
+ p_mask_k = k_padding_mask_1d.unsqueeze(1) # Shape [B, 1, Tk]
158
+
159
+ # Condition A: Non-padding query attends to non-padding key
160
+ non_pad_attends_non_pad = p_mask_q & p_mask_k # Shape [B, Tq, Tk]
161
+
162
+ # Condition B: Padding query attends to padding key
163
+ pad_attends_pad = (~p_mask_q) & (~p_mask_k) # Shape [B, Tq, Tk]
164
+
165
+ # Combine: True if padding status is compatible (both non-pad OR both pad)
166
+ # This implementation follows Jax TPU splash attention kernel
167
+ mask = non_pad_attends_non_pad | pad_attends_pad # Shape [B, Tq, Tk]
168
+
169
+ if is_causal:
170
+ # Ensure causality for self-attention (Tq == Tk)
171
+ assert Tq == Tk, "Causal mask requires query and key sequence lengths to be equal"
172
+ # Standard lower-triangular causal mask (True means allow)
173
+ causal_mask_2d = torch.tril(torch.ones((Tq, Tk), dtype=torch.bool, device=self.device)) # Shape [Tq, Tk]
174
+ causal_mask = mask & causal_mask_2d # Shape [B, Tq, Tk]
175
+ return causal_mask.unsqueeze(1) # Shape [B, 1, Tq, Tk] for broadcasting across heads
176
+ else:
177
+ # For cross-attention or non-causal self-attention
178
+ return mask.unsqueeze(1) # Shape [B, 1, Tq, Tk] for broadcasting across heads
179
+
180
+ def _prepare_text_input(self, text: str) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
181
+ """Encodes text prompt, pads, and creates attention mask and positions."""
182
+ text_pad_value = self.config.data.text_pad_value
183
+ max_len = self.config.data.text_length
184
+
185
+ byte_text = text.encode("utf-8")
186
+
187
+
188
+ replaced_bytes = byte_text
189
+
190
+ LANG2BYTE = {
191
+ "en": 3,
192
+ "de": 4,
193
+ "fr": 5,
194
+ "es": 6,
195
+ "it": 7,
196
+ "nl": 14,
197
+ "pl": 15,
198
+ "pt": 16,
199
+ "tr": 17,
200
+ "hu": 18,
201
+ }
202
+
203
+ for lang, byte_val in LANG2BYTE.items():
204
+ tag = f"[{lang}]".encode("ascii") # e.g. b"[de]"
205
+ code = bytes([byte_val]) # e.g. b"\x04"
206
+ replaced_bytes = replaced_bytes.replace(tag, code)
207
+ text_tokens = list(replaced_bytes)
208
+
209
+ current_len = len(text_tokens)
210
+ padding_needed = max_len - current_len
211
+ if padding_needed <= 0:
212
+ text_tokens = text_tokens[:max_len]
213
+ padded_text_np = np.array(text_tokens, dtype=np.uint8)
214
+ else:
215
+ padded_text_np = np.pad(
216
+ text_tokens,
217
+ (0, padding_needed),
218
+ mode="constant",
219
+ constant_values=text_pad_value,
220
+ ).astype(np.uint8)
221
+
222
+ src_tokens = torch.from_numpy(padded_text_np).to(torch.long).to(self.device).unsqueeze(0) # [1, S]
223
+ src_positions = torch.arange(max_len, device=self.device).to(torch.long).unsqueeze(0) # [1, S]
224
+
225
+ src_padding_mask = (src_tokens != text_pad_value).to(self.device) # [1, S]
226
+
227
+ enc_self_attn_mask = self._create_attn_mask(src_padding_mask, src_padding_mask, is_causal=False) # [1, S, S]
228
+
229
+ return src_tokens, src_positions, src_padding_mask, enc_self_attn_mask
230
+
231
+ @torch.inference_mode()
232
+ def generate(
233
+ self,
234
+ text: str,
235
+ max_tokens: int | None = None,
236
+ cfg_scale: float = 3.0,
237
+ temperature: float = 1.3,
238
+ top_p: float = 0.95,
239
+ use_cfg_filter: bool = True,
240
+ use_torch_compile: bool = False,
241
+ cfg_filter_top_k: int = 35,
242
+ audio_prompt_path: str | None = None,
243
+ ) -> np.ndarray:
244
+ """
245
+ Generates audio from a text prompt (and optional audio prompt) using the Nari model.
246
+
247
+ Returns:
248
+ A tensor of generated audio codes (shape: [max_tokens, num_channels]).
249
+ """
250
+ num_channels = self.config.data.channels
251
+ audio_bos_value = self.config.data.audio_bos_value
252
+ audio_eos_value = self.config.data.audio_eos_value
253
+ audio_pad_value = self.config.data.audio_pad_value
254
+ delay_pattern = self.config.data.delay_pattern
255
+ max_tokens = self.config.data.audio_length if max_tokens is None else max_tokens
256
+ delay_tensor = torch.tensor(delay_pattern, dtype=torch.long, device=self.device)
257
+ max_delay_pattern = max(delay_pattern)
258
+ self.model.eval()
259
+
260
+ (
261
+ cond_src_BxS,
262
+ cond_src_positions_BxS,
263
+ cond_src_padding_mask_BxS,
264
+ cond_enc_self_attn_mask_Bx1xSxS,
265
+ ) = self._prepare_text_input(text)
266
+
267
+ unc_src_BxS = torch.zeros_like(cond_src_BxS)
268
+ src_BxS = torch.cat([unc_src_BxS, cond_src_BxS], dim=0)
269
+ src_positions_BxS = cond_src_positions_BxS.expand(2, -1)
270
+ src_padding_mask_BxS = cond_src_padding_mask_BxS.expand(2, -1)
271
+ enc_self_attn_mask_Bx1xSxS = cond_enc_self_attn_mask_Bx1xSxS.expand(2, -1, -1, -1)
272
+
273
+ # 2. Encoder Pass
274
+ # with torch.autocast(device_type="cuda", dtype=forward_dtype):
275
+ encoder_out = self.model.encoder(
276
+ x_ids=src_BxS,
277
+ src_positions=src_positions_BxS,
278
+ deterministic=True,
279
+ attn_mask=enc_self_attn_mask_Bx1xSxS,
280
+ ) # Shape: (B, S, E)
281
+
282
+ # 3. Prepare Decoder Inputs
283
+ # 3-1. Allocate KV Cache (Static)
284
+ decoder_cross_attention_cache: list[KVCache] = self.model.decoder.precompute_cross_attention_kv(
285
+ max_tokens, encoder_out, src_positions_BxS
286
+ )
287
+
288
+ decoder_self_attention_cache: list[KVCache] = []
289
+ for _ in range(self.model.decoder.num_layers):
290
+ decoder_self_attention_cache.append(
291
+ KVCache(
292
+ self.config.model.decoder.gqa_query_heads,
293
+ max_tokens,
294
+ self.config.model.decoder.gqa_head_dim,
295
+ self.device,
296
+ )
297
+ )
298
+
299
+ # 3-2. Initialize Decoder Inputs
300
+ generated_BxTxC = torch.full(
301
+ (2, 1, num_channels),
302
+ fill_value=audio_bos_value,
303
+ dtype=torch.long,
304
+ device=self.device,
305
+ )
306
+
307
+ current_step = 0
308
+ prompt_len_inc_bos = 1 # Start with BOS length
309
+
310
+ # 3-3. Load Audio Prompt (if provided)
311
+ if audio_prompt_path is not None:
312
+ audio_prompt, sr = torchaudio.load(audio_prompt_path, channels_first=True) # C, T
313
+ if sr != 44100: # Resample to 44.1kHz
314
+ audio_prompt = torchaudio.functional.resample(audio_prompt, sr, 44100)
315
+ audio_prompt = audio_prompt.to(self.device).unsqueeze(0) # 1, C, T
316
+ audio_prompt = audio_to_codebook(self.dac_model, audio_prompt, data_config=self.config.data)
317
+ generated_BxTxC = torch.cat([generated_BxTxC, audio_prompt.expand(2, -1, -1)], dim=1)
318
+
319
+ prefill_len = generated_BxTxC.shape[1]
320
+ prompt_len_inc_bos = prefill_len
321
+ prefill_tgt_pos = torch.arange(prefill_len, device=self.device).unsqueeze(0).expand(2, -1)
322
+ prefill_tgt_padding_mask = (generated_BxTxC != audio_pad_value).any(dim=2)
323
+
324
+ prefill_self_attn_mask = self._create_attn_mask(
325
+ prefill_tgt_padding_mask,
326
+ prefill_tgt_padding_mask,
327
+ is_causal=True,
328
+ )
329
+ prefill_cross_attn_mask = self._create_attn_mask(
330
+ prefill_tgt_padding_mask,
331
+ src_padding_mask_BxS,
332
+ is_causal=False,
333
+ )
334
+
335
+ _ = self.model.decoder.forward(
336
+ tgt_ids_BxTxC=generated_BxTxC,
337
+ encoder_out=encoder_out,
338
+ tgt_positions=prefill_tgt_pos,
339
+ src_positions=src_positions_BxS,
340
+ deterministic=True,
341
+ self_attn_mask=prefill_self_attn_mask,
342
+ cross_attn_mask=prefill_cross_attn_mask,
343
+ self_attention_cache=decoder_self_attention_cache,
344
+ cross_attention_cache=decoder_cross_attention_cache,
345
+ )
346
+
347
+ current_step = prefill_len - 1
348
+
349
+ # 4. Autoregressive Generation Loop
350
+ eos_detected_channel_0 = False
351
+ eos_countdown = -1
352
+ extra_steps_after_eos = 30
353
+ # Make generated_BxTxC a fixed size tensor
354
+ # Length is either 1 + max tokens or 1 + prompt len + max tokens
355
+ generated_BxTxC = torch.cat(
356
+ [
357
+ generated_BxTxC,
358
+ torch.full(
359
+ (2, max_tokens, num_channels),
360
+ fill_value=-1,
361
+ dtype=torch.long,
362
+ device=self.device,
363
+ ),
364
+ ],
365
+ dim=1,
366
+ )
367
+
368
+ decode_step = self.model.decoder.decode_step
369
+ if use_torch_compile:
370
+ decode_step = torch.compile(
371
+ self.model.decoder.decode_step,
372
+ mode="default",
373
+ )
374
+
375
+ tgt_padding_mask = (
376
+ (generated_BxTxC[:, -1, :].unsqueeze(1) != audio_pad_value).any(dim=2).to(self.device)
377
+ ) # [B, 1]
378
+ # Generated tokens are never PAD, so we use fixed mask
379
+ decoder_cross_attn_mask = self._create_attn_mask(
380
+ tgt_padding_mask, # Query mask [B, 1]
381
+ src_padding_mask_BxS, # Key mask [B, S]
382
+ is_causal=False,
383
+ ) # [B, 1, 1, S]
384
+
385
+ for step in range(current_step, current_step + max_tokens):
386
+ tgt_ids_Bx1xC = generated_BxTxC[:, step, :].unsqueeze(1)
387
+ tgt_pos_Bx1 = torch.full(
388
+ (2, 1),
389
+ fill_value=step,
390
+ dtype=torch.long,
391
+ device=self.device,
392
+ )
393
+
394
+ logits_Bx1xCxV, new_cache = decode_step(
395
+ tgt_ids_Bx1xC=tgt_ids_Bx1xC,
396
+ tgt_pos_Bx1=tgt_pos_Bx1,
397
+ encoder_out=encoder_out,
398
+ self_attn_mask=None,
399
+ cross_attn_mask=decoder_cross_attn_mask,
400
+ self_attention_cache=decoder_self_attention_cache,
401
+ cross_attention_cache=decoder_cross_attention_cache,
402
+ )
403
+
404
+ for i, layer_cache in enumerate(decoder_self_attention_cache):
405
+ layer_cache.update_cache(new_cache[i][0], new_cache[i][1])
406
+
407
+ V = self.config.model.tgt_vocab_size
408
+ logits_last_BxCxV = logits_Bx1xCxV[:, -1, :, :] # B, C, V
409
+ uncond_logits_CxV = logits_last_BxCxV[0, :, :]
410
+ cond_logits_CxV = logits_last_BxCxV[1, :, :]
411
+
412
+ cfg_logits_CxV = cond_logits_CxV + cfg_scale * (cond_logits_CxV - uncond_logits_CxV)
413
+
414
+ logits_CxV = cfg_logits_CxV.reshape((-1, V)) # C, V
415
+ logits_CxV[:, 1025:] = -torch.inf
416
+
417
+ # Sample next token
418
+ pred_C = _sample_next_token(
419
+ logits_CxV.float(),
420
+ temperature=temperature,
421
+ top_p=top_p,
422
+ use_cfg_filter=use_cfg_filter,
423
+ cfg_filter_top_k=cfg_filter_top_k,
424
+ )
425
+
426
+ generation_step_index = step - current_step
427
+ if audio_prompt_path is None:
428
+ pred_C = torch.where(
429
+ generation_step_index >= delay_tensor,
430
+ pred_C,
431
+ audio_bos_value,
432
+ )
433
+
434
+ generated_BxTxC[:, step + 1, :] = pred_C.unsqueeze(0).expand(2, -1)
435
+
436
+ if not eos_detected_channel_0 and pred_C[0] == audio_eos_value:
437
+ eos_detected_channel_0 = True
438
+ eos_countdown = extra_steps_after_eos
439
+
440
+ if eos_countdown > 0:
441
+ step_after_eos = max_delay_pattern - eos_countdown
442
+ for i, d in enumerate(delay_pattern):
443
+ if step_after_eos == d:
444
+ generated_BxTxC[:, step + 1, i] = audio_eos_value
445
+ elif step_after_eos > d:
446
+ generated_BxTxC[:, step + 1, i] = audio_pad_value
447
+ eos_countdown -= 1
448
+ if eos_countdown == 0:
449
+ break
450
+
451
+ generation_step_index = step - current_step + 1
452
+
453
+ output_codes = generated_BxTxC[:, prompt_len_inc_bos : step + 1, :]
454
+
455
+ generated_codes = output_codes[0]
456
+
457
+ audio = codebook_to_audio(
458
+ generated_codes.transpose(1, 0), self.dac_model, delay_pattern, B=1, T=max_tokens, C=num_channels
459
+ )
460
+ return audio.squeeze().cpu().numpy()
dia_1_6B_dv.py ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tempfile
2
+ import time
3
+ from pathlib import Path
4
+ from typing import Optional, Tuple
5
+ import spaces
6
+
7
+ import gradio as gr
8
+ import numpy as np
9
+ import soundfile as sf
10
+ import torch
11
+
12
+ from dia.model import Dia
13
+
14
+ # Model selection
15
+ DIA_MODELS = {
16
+ "Dhivehi Dia-1.6B": "alakxender/Dia-1.6B-dhivehi-ep1",
17
+ #"Dhivehi 18k": "alakxender/Dia-1.6B-dhivehi-18k"
18
+ }
19
+
20
+ dia_models = {}
21
+
22
+ def load_dia_model(model_id):
23
+ if model_id not in dia_models:
24
+ print(f"Loading model {model_id}")
25
+ dia_models[model_id] = Dia.from_pretrained(model_id)
26
+ print(f"Loaded model {model_id}")
27
+ return dia_models[model_id]
28
+
29
+ @spaces.GPU
30
+ def run_inference(
31
+ text_input: str,
32
+ audio_prompt_input: Optional[Tuple[int, np.ndarray]],
33
+ transcription_input: Optional[str],
34
+ max_new_tokens: int,
35
+ cfg_scale: float,
36
+ temperature: float,
37
+ top_p: float,
38
+ cfg_filter_top_k: int,
39
+ speed_factor: float,
40
+ model_name: str,
41
+ ):
42
+ model_id = DIA_MODELS[model_name]
43
+ model = load_dia_model(model_id)
44
+ if not text_input or text_input.isspace():
45
+ raise gr.Error("Text input cannot be empty.")
46
+ temp_txt_file_path = None
47
+ temp_audio_prompt_path = None
48
+ output_audio = (44100, np.zeros(1, dtype=np.float32))
49
+ try:
50
+ prompt_path_for_generate = None
51
+ if audio_prompt_input is not None:
52
+ sr, audio_data = audio_prompt_input
53
+ duration_sec = len(audio_data) / float(sr) if sr else 0
54
+ if duration_sec > 10.0:
55
+ raise gr.Error("Audio prompt must be 10 seconds or shorter.")
56
+ if (
57
+ audio_data is None or audio_data.size == 0 or audio_data.max() == 0
58
+ ):
59
+ gr.Warning("Audio prompt seems empty or silent, ignoring prompt.")
60
+ else:
61
+ with tempfile.NamedTemporaryFile(
62
+ mode="wb", suffix=".wav", delete=False
63
+ ) as f_audio:
64
+ temp_audio_prompt_path = f_audio.name
65
+ if np.issubdtype(audio_data.dtype, np.integer):
66
+ max_val = np.iinfo(audio_data.dtype).max
67
+ audio_data = audio_data.astype(np.float32) / max_val
68
+ elif not np.issubdtype(audio_data.dtype, np.floating):
69
+ gr.Warning(
70
+ f"Unsupported audio prompt dtype {audio_data.dtype}, attempting conversion."
71
+ )
72
+ try:
73
+ audio_data = audio_data.astype(np.float32)
74
+ except Exception as conv_e:
75
+ raise gr.Error(
76
+ f"Failed to convert audio prompt to float32: {conv_e}"
77
+ )
78
+ if audio_data.ndim > 1:
79
+ if audio_data.shape[0] == 2:
80
+ audio_data = np.mean(audio_data, axis=0)
81
+ elif audio_data.shape[1] == 2:
82
+ audio_data = np.mean(audio_data, axis=1)
83
+ else:
84
+ gr.Warning(
85
+ f"Audio prompt has unexpected shape {audio_data.shape}, taking first channel/axis."
86
+ )
87
+ audio_data = (
88
+ audio_data[0]
89
+ if audio_data.shape[0] < audio_data.shape[1]
90
+ else audio_data[:, 0]
91
+ )
92
+ audio_data = np.ascontiguousarray(audio_data)
93
+ try:
94
+ sf.write(
95
+ temp_audio_prompt_path, audio_data, sr, subtype="FLOAT"
96
+ )
97
+ prompt_path_for_generate = temp_audio_prompt_path
98
+ print(
99
+ f"Created temporary audio prompt file: {temp_audio_prompt_path} (orig sr: {sr})"
100
+ )
101
+ except Exception as write_e:
102
+ print(f"Error writing temporary audio file: {write_e}")
103
+ raise gr.Error(f"Failed to save audio prompt: {write_e}")
104
+ start_time = time.time()
105
+ with torch.inference_mode():
106
+ combined_text = (
107
+ text_input.strip() + "\n" + transcription_input.strip()
108
+ if transcription_input and not transcription_input.isspace()
109
+ else text_input
110
+ )
111
+ output_audio_np = model.generate(
112
+ combined_text,
113
+ max_tokens=max_new_tokens,
114
+ cfg_scale=cfg_scale,
115
+ temperature=temperature,
116
+ top_p=top_p,
117
+ cfg_filter_top_k=cfg_filter_top_k,
118
+ use_torch_compile=False,
119
+ audio_prompt_path=prompt_path_for_generate,
120
+ )
121
+ end_time = time.time()
122
+ print(f"Generation finished in {end_time - start_time:.2f} seconds.")
123
+ if output_audio_np is not None:
124
+ output_sr = 44100
125
+ original_len = len(output_audio_np)
126
+ speed_factor = max(0.1, min(speed_factor, 5.0))
127
+ target_len = int(original_len / speed_factor)
128
+ if target_len != original_len and target_len > 0:
129
+ x_original = np.arange(original_len)
130
+ x_resampled = np.linspace(0, original_len - 1, target_len)
131
+ resampled_audio_np = np.interp(x_resampled, x_original, output_audio_np)
132
+ output_audio = (
133
+ output_sr,
134
+ resampled_audio_np.astype(np.float32),
135
+ )
136
+ print(
137
+ f"Resampled audio from {original_len} to {target_len} samples for {speed_factor:.2f}x speed."
138
+ )
139
+ else:
140
+ output_audio = (
141
+ output_sr,
142
+ output_audio_np,
143
+ )
144
+ print(f"Skipping audio speed adjustment (factor: {speed_factor:.2f}).")
145
+ print(
146
+ f"Audio conversion successful. Final shape: {output_audio[1].shape}, Sample Rate: {output_sr}"
147
+ )
148
+ if (
149
+ output_audio[1].dtype == np.float32
150
+ or output_audio[1].dtype == np.float64
151
+ ):
152
+ audio_for_gradio = np.clip(output_audio[1], -1.0, 1.0)
153
+ audio_for_gradio = (audio_for_gradio * 32767).astype(np.int16)
154
+ output_audio = (output_sr, audio_for_gradio)
155
+ print("Converted audio to int16 for Gradio output.")
156
+ else:
157
+ print("\nGeneration finished, but no valid tokens were produced.")
158
+ gr.Warning("Generation produced no output.")
159
+ except Exception as e:
160
+ print(f"Error during inference: {e}")
161
+ import traceback
162
+ traceback.print_exc()
163
+ raise gr.Error(f"Inference failed: {e}")
164
+ finally:
165
+ if temp_txt_file_path and Path(temp_txt_file_path).exists():
166
+ try:
167
+ Path(temp_txt_file_path).unlink()
168
+ print(f"Deleted temporary text file: {temp_txt_file_path}")
169
+ except OSError as e:
170
+ print(
171
+ f"Warning: Error deleting temporary text file {temp_txt_file_path}: {e}"
172
+ )
173
+ if temp_audio_prompt_path and Path(temp_audio_prompt_path).exists():
174
+ try:
175
+ Path(temp_audio_prompt_path).unlink()
176
+ print(f"Deleted temporary audio prompt file: {temp_audio_prompt_path}")
177
+ except OSError as e:
178
+ print(
179
+ f"Warning: Error deleting temporary audio prompt file {temp_audio_prompt_path}: {e}"
180
+ )
181
+ return output_audio
182
+
183
+ def get_dia_1_6B_tab():
184
+ css = """
185
+ #col-container {max-width: 90%; margin-left: auto; margin-right: auto;}
186
+ .dhivehi-text-nofont textarea {
187
+ font-size: 18px !important;
188
+ line-height: 1.8 !important;
189
+ direction: rtl !important;
190
+ text-align: right !important;
191
+ }
192
+ .dhivehi-text-nofont input {
193
+ font-size: 18px !important;
194
+ direction: rtl !important;
195
+ text-align: right !important;
196
+ }
197
+ """
198
+ default_text = ""
199
+ example_txt_path = Path("./example.txt")
200
+ if example_txt_path.exists():
201
+ try:
202
+ default_text = example_txt_path.read_text(encoding="utf-8").strip()
203
+ if not default_text:
204
+ default_text = "Example text file was empty."
205
+ except Exception as e:
206
+ print(f"Warning: Could not read example.txt: {e}")
207
+ with gr.Tab("🎙️ Dia-1.6B"):
208
+ gr.Markdown("# Dia Text-to-Speech Synthesis (Dia-1.6B)")
209
+ with gr.Row(equal_height=False):
210
+ with gr.Column(scale=1):
211
+ model_dropdown = gr.Dropdown(
212
+ choices=list(DIA_MODELS.keys()),
213
+ value=list(DIA_MODELS.keys())[0],
214
+ label="Select Dia Model"
215
+ )
216
+ text_input = gr.Textbox(
217
+ label="Input Text",
218
+ placeholder="ލިޔެލަން",
219
+ value=default_text,
220
+ lines=5,
221
+ elem_classes=["dhivehi-text-nofont"]
222
+ )
223
+ audio_prompt_input = gr.Audio(
224
+ label="Audio Prompt (≤ 10 s, Optional)",
225
+ show_label=True,
226
+ sources=["upload", "microphone"],
227
+ type="numpy",
228
+ )
229
+ transcription_input = gr.Textbox(
230
+ label="Audio Prompt Transcription (Optional)",
231
+ placeholder="ޓްރާންސްކްރިޕްޓް ލިޔެލަން",
232
+ lines=3,
233
+ elem_classes=["dhivehi-text-nofont"]
234
+ )
235
+ with gr.Accordion("Generation Parameters", open=False):
236
+ default_model = load_dia_model(DIA_MODELS[list(DIA_MODELS.keys())[0]])
237
+ max_new_tokens = gr.Slider(
238
+ label="Max New Tokens (Audio Length)",
239
+ minimum=860,
240
+ maximum=3072,
241
+ value=getattr(getattr(default_model.config, 'data', None), 'audio_length', 1536),
242
+ step=50,
243
+ info="Controls the maximum length of the generated audio (more tokens = longer audio).",
244
+ )
245
+ cfg_scale = gr.Slider(
246
+ label="CFG Scale (Guidance Strength)",
247
+ minimum=1.0,
248
+ maximum=5.0,
249
+ value=3.0,
250
+ step=0.1,
251
+ info="Higher values increase adherence to the text prompt.",
252
+ )
253
+ temperature = gr.Slider(
254
+ label="Temperature (Randomness)",
255
+ minimum=1.0,
256
+ maximum=2.5,
257
+ value=1.8,
258
+ step=0.05,
259
+ info="Lower values make the output more deterministic, higher values increase randomness.",
260
+ )
261
+ top_p = gr.Slider(
262
+ label="Top P (Nucleus Sampling)",
263
+ minimum=0.70,
264
+ maximum=1.0,
265
+ value=0.95,
266
+ step=0.01,
267
+ info="Filters vocabulary to the most likely tokens cumulatively reaching probability P.",
268
+ )
269
+ cfg_filter_top_k = gr.Slider(
270
+ label="CFG Filter Top K",
271
+ minimum=15,
272
+ maximum=100,
273
+ value=45,
274
+ step=1,
275
+ info="Top k filter for CFG guidance.",
276
+ )
277
+ speed_factor_slider = gr.Slider(
278
+ label="Speed Factor",
279
+ minimum=0.8,
280
+ maximum=1.0,
281
+ value=1.0,
282
+ step=0.02,
283
+ info="Adjusts the speed of the generated audio (1.0 = original speed).",
284
+ )
285
+ generate_btn = gr.Button("Generate Audio", variant="primary")
286
+ with gr.Column(scale=1):
287
+ audio_output = gr.Audio(
288
+ label="Generated Audio",
289
+ type="numpy",
290
+ autoplay=False,
291
+ )
292
+ generate_btn.click(
293
+ run_inference,
294
+ inputs=[
295
+ text_input,
296
+ audio_prompt_input,
297
+ transcription_input,
298
+ max_new_tokens,
299
+ cfg_scale,
300
+ temperature,
301
+ top_p,
302
+ cfg_filter_top_k,
303
+ speed_factor_slider,
304
+ model_dropdown,
305
+ ],
306
+ outputs=[audio_output],
307
+ )
308
+ # Examples (optional, can be extended)
309
+ examples_list = [
310
+ [
311
+ """[S1] އައްސަލާމު އަލައިކުމް. (clears throat) Good morning!
312
+ [S2] How are you today?
313
+ [S1] I'm fine, thanks. ކިހިނެއް ހާލު؟
314
+ [S2] (coughs) ކުޑަކޮށް ބަލިކޮށް މިއުޅެނީ
315
+ [S1] Oh okay. Get well soon...
316
+ [S2] Thanks! See you later... ފަހުން ދިމާވެލާނީ
317
+ [S1]""",
318
+ None,
319
+ "",
320
+ 1536,
321
+ 5.0,
322
+ 2.5,
323
+ 0.95,
324
+ 45,
325
+ 1.0,
326
+ list(DIA_MODELS.keys())[0],
327
+ ],
328
+ ["""[FEMALE-01] [S1] ގައުމަށް އައި މިނިވަން ނޫރާނީ... [S2] ދައުރުން މި ހަނދާންތައް އާކުރަނީ... [S1] އައުދާނަ އިތުރު އަބުޠާލުންނަށް... [S2] ޒިކުރާގެ މަލުން މި ވެދުން ކުރަނީ.""",
329
+ None,
330
+ "",
331
+ 1536,
332
+ 3.0,
333
+ 1.8,
334
+ 0.95,
335
+ 45,
336
+ 0.96,
337
+ list(DIA_MODELS.keys())[0]
338
+ ],
339
+ ["""[MALE-01] [S1] މާޒީގެ އުޖާލާ މަންޒަރުތައް... [S2] މާރީތި އުފާވެރި ކުރެހުންތައް... [S2] ދާތީ އަދު ހާމަ ވަމުން ކުލަތައް... [S2] ތާރީޚު އަލުން މި އިޢާދަ ވަނީ.""",
340
+ None,
341
+ "",
342
+ 1536,
343
+ 3.0,
344
+ 1.8,
345
+ 0.95,
346
+ 45,
347
+ 0.96,
348
+ list(DIA_MODELS.keys())[0]
349
+ ],
350
+ ]
351
+ if examples_list:
352
+ gr.Examples(
353
+ examples=examples_list,
354
+ inputs=[
355
+ text_input,
356
+ audio_prompt_input,
357
+ transcription_input,
358
+ max_new_tokens,
359
+ cfg_scale,
360
+ temperature,
361
+ top_p,
362
+ cfg_filter_top_k,
363
+ speed_factor_slider,
364
+ model_dropdown,
365
+ ],
366
+ outputs=[audio_output],
367
+ fn=run_inference,
368
+ cache_examples=False,
369
+ label="Examples (Click to Run)",
370
+ )
371
+ else:
372
+ gr.Markdown("_(No examples configured or example prompt file missing)_")
373
+ gr.Markdown(
374
+ "---\n"
375
+ "**General Guidelines:**\n"
376
+ "- Keep input text length moderate\n"
377
+ " - Short input (corresponding to under 5s of audio) will sound unnatural\n"
378
+ " - Very long input (corresponding to over 20s of audio) will make the speech unnaturally fast\n\n"
379
+ "- Use non-verbal tags sparingly, from the list in the README. Overusing or using unlisted non-verbals may cause weird artifacts\n\n"
380
+ "- Always begin input text with [S1], and always alternate between [S1] and [S2] (i.e. [S1]... [S1]... is not good)\n\n"
381
+ "**When using audio prompts (voice cloning):**\n"
382
+ "- Provide the transcript of the to-be cloned audio before the generation text\n"
383
+ "- Transcript must use [S1], [S2] speaker tags correctly:\n"
384
+ " - Single speaker: [S1]...\n"
385
+ " - Two speakers: [S1]... [S2]...\n"
386
+ "- Duration of the to-be cloned audio should be 5~10 seconds for the best results\n"
387
+ " - (Keep in mind: 1 second ≈ 86 tokens)\n"
388
+ "- Put [S1] or [S2] (the second-to-last speaker's tag) at the end of the audio to improve audio quality at the end"
389
+ )
390
+ # No explicit return needed for context manager pattern
example.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ [S1] [dv] Hmm... (coughs) sorry.... ކެއްސާވަރުން!...
2
+ [S2] Good Morning!...
3
+ [S1] ބާއްޖަވެރި ހެދުނެއް ދުރާގާތުން އަޑުއަހާ އެންމެންނަށް (laughs)
4
+ [S2]