multimodalart HF Staff commited on
Commit
cecbc0f
·
verified ·
1 Parent(s): f3d74c5

Add Miso TTS 8B ZeroGPU demo

Browse files
README.md CHANGED
@@ -1,13 +1,17 @@
1
  ---
2
- title: MisoTTS
3
- emoji: 👀
4
- colorFrom: pink
5
- colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.16.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: Miso TTS 8B
3
+ emoji: 🍲
4
+ colorFrom: yellow
5
+ colorTo: red
6
  sdk: gradio
7
+ sdk_version: 5.20.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: other
11
+ short_description: 8B CSM-style text-to-speech with voice cloning
12
+ python_version: "3.10"
13
+ startup_duration_timeout: 1h
14
  ---
15
 
16
+ Demo for [MisoLabs/MisoTTS](https://huggingface.co/MisoLabs/MisoTTS), an 8B Sesame CSM-style
17
+ text-to-speech model. Inference code from [MisoLabsAI/MisoTTS](https://github.com/MisoLabsAI/MisoTTS).
__pycache__/app.cpython-312.pyc ADDED
Binary file (5.78 kB). View file
 
__pycache__/generator.cpython-312.pyc ADDED
Binary file (14.6 kB). View file
 
__pycache__/models.cpython-312.pyc ADDED
Binary file (16.3 kB). View file
 
__pycache__/moshi_compat.cpython-312.pyc ADDED
Binary file (2.33 kB). View file
 
__pycache__/watermarking.cpython-312.pyc ADDED
Binary file (4.19 kB). View file
 
app.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ os.environ.setdefault("NO_TORCH_COMPILE", "1")
4
+
5
+ import spaces
6
+ import numpy as np
7
+ import torch
8
+ import torchaudio
9
+ import gradio as gr
10
+
11
+ from generator import Segment, load_miso_8b
12
+
13
+ device = "cuda" if torch.cuda.is_available() else "cpu"
14
+ generator = load_miso_8b(device=device, model_path_or_repo_id="MisoLabs/MisoTTS")
15
+ SAMPLE_RATE = generator.sample_rate
16
+
17
+ MAX_INPUT_CHARS = 1000
18
+
19
+ DESCRIPTION = """
20
+ # Miso TTS 8B
21
+
22
+ Text-to-speech with the [MisoLabs/MisoTTS](https://huggingface.co/MisoLabs/MisoTTS) model — an
23
+ 8B [Sesame CSM](https://github.com/SesameAILabs/csm)-style model that generates Mimi audio codes
24
+ from text, with optional voice continuation from a reference clip.
25
+
26
+ Provide a reference audio + its transcript to clone a voice, or leave them empty for a default voice.
27
+ Outputs carry an imperceptible watermark identifying the audio as AI-generated.
28
+ """
29
+
30
+
31
+ def _resample_to_model(audio: torch.Tensor, sr: int) -> torch.Tensor:
32
+ audio = audio.mean(dim=0) if audio.ndim > 1 else audio
33
+ if sr != SAMPLE_RATE:
34
+ audio = torchaudio.functional.resample(audio, orig_freq=sr, new_freq=SAMPLE_RATE)
35
+ return audio
36
+
37
+
38
+ @spaces.GPU(duration=120)
39
+ def synthesize(text, ref_audio_path, ref_text, speaker_id, max_length_ms, temperature, topk):
40
+ text = (text or "").strip()
41
+ if not text:
42
+ raise gr.Error("Please enter some text to synthesize.")
43
+ if len(text) > MAX_INPUT_CHARS:
44
+ raise gr.Error(f"Text too long (>{MAX_INPUT_CHARS} characters).")
45
+
46
+ context = []
47
+ if ref_audio_path:
48
+ if not (ref_text or "").strip():
49
+ raise gr.Error("Please provide the transcript of the reference audio.")
50
+ wav, sr = torchaudio.load(ref_audio_path)
51
+ wav = _resample_to_model(wav, sr).to(device)
52
+ context = [Segment(speaker=int(speaker_id), text=ref_text.strip(), audio=wav)]
53
+
54
+ audio = generator.generate(
55
+ text=text,
56
+ speaker=int(speaker_id),
57
+ context=context,
58
+ max_audio_length_ms=float(max_length_ms),
59
+ temperature=float(temperature),
60
+ topk=int(topk),
61
+ )
62
+
63
+ audio_np = (audio * 32768).clamp(-32768, 32767).to(torch.int16).cpu().numpy()
64
+ return SAMPLE_RATE, audio_np
65
+
66
+
67
+ with gr.Blocks(title="Miso TTS 8B") as demo:
68
+ gr.Markdown(DESCRIPTION)
69
+ with gr.Row():
70
+ with gr.Column():
71
+ text = gr.Textbox(
72
+ label="Text to synthesize",
73
+ placeholder="Hello from Miso.",
74
+ lines=3,
75
+ value="Hello from Miso. This is an eight billion parameter text to speech model.",
76
+ )
77
+ with gr.Accordion("Voice cloning (optional)", open=False):
78
+ ref_audio = gr.Audio(label="Reference audio", type="filepath")
79
+ ref_text = gr.Textbox(
80
+ label="Reference transcript",
81
+ placeholder="The exact words spoken in the reference audio.",
82
+ lines=2,
83
+ )
84
+ with gr.Accordion("Advanced", open=False):
85
+ speaker_id = gr.Slider(0, 1, value=0, step=1, label="Speaker ID")
86
+ max_length = gr.Slider(2000, 30000, value=10000, step=1000, label="Max audio length (ms)")
87
+ temperature = gr.Slider(0.1, 1.5, value=0.9, step=0.05, label="Temperature")
88
+ topk = gr.Slider(1, 100, value=50, step=1, label="Top-k")
89
+ run = gr.Button("Generate", variant="primary")
90
+ with gr.Column():
91
+ out = gr.Audio(label="Generated speech")
92
+
93
+ run.click(
94
+ synthesize,
95
+ inputs=[text, ref_audio, ref_text, speaker_id, max_length, temperature, topk],
96
+ outputs=[out],
97
+ )
98
+
99
+ demo.queue().launch()
generator.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ import os
3
+ from typing import List, Optional, Tuple
4
+
5
+ os.environ.setdefault("HF_HUB_ETAG_TIMEOUT", "60")
6
+ os.environ.setdefault("HF_HUB_DOWNLOAD_TIMEOUT", "60")
7
+
8
+ import torch
9
+ import torchaudio
10
+ from huggingface_hub import hf_hub_download
11
+ from models import MISO_TTS_8B_CONFIG, Model, ModelArgs
12
+ from moshi_compat import patch_bitsandbytes_import_for_unquantized_layers
13
+ from moshi.models import loaders
14
+ from tokenizers.processors import TemplateProcessing
15
+ from transformers import AutoTokenizer
16
+ from watermarking import MISO_TTS_WATERMARK, load_watermarker, watermark
17
+
18
+ DEFAULT_MISO_TTS_REPO_ID = "MisoLabs/MisoTTS"
19
+ patch_bitsandbytes_import_for_unquantized_layers()
20
+
21
+
22
+ @dataclass
23
+ class Segment:
24
+ speaker: int
25
+ text: str
26
+ # (num_samples,), sample_rate = 24_000
27
+ audio: torch.Tensor
28
+
29
+
30
+ def load_llama3_tokenizer():
31
+ """
32
+ https://github.com/huggingface/transformers/issues/22794#issuecomment-2092623992
33
+ """
34
+ tokenizer_name = "NousResearch/Llama-3.2-1B"
35
+ tokenizer = AutoTokenizer.from_pretrained(tokenizer_name)
36
+ bos = tokenizer.bos_token
37
+ eos = tokenizer.eos_token
38
+ tokenizer._tokenizer.post_processor = TemplateProcessing(
39
+ single=f"{bos}:0 $A:0 {eos}:0",
40
+ pair=f"{bos}:0 $A:0 {eos}:0 {bos}:1 $B:1 {eos}:1",
41
+ special_tokens=[(f"{bos}", tokenizer.bos_token_id), (f"{eos}", tokenizer.eos_token_id)],
42
+ )
43
+
44
+ return tokenizer
45
+
46
+
47
+ class Generator:
48
+ def __init__(
49
+ self,
50
+ model: Model,
51
+ ):
52
+ self._model = model
53
+ self._model.setup_caches(1)
54
+
55
+ self._text_tokenizer = load_llama3_tokenizer()
56
+ self._frame_size = self._model.config.audio_num_codebooks + 1
57
+
58
+ device = next(model.parameters()).device
59
+ mimi_weight = hf_hub_download(loaders.DEFAULT_REPO, loaders.MIMI_NAME)
60
+ mimi = loaders.get_mimi(mimi_weight, device=device)
61
+ mimi.set_num_codebooks(self._model.config.audio_num_codebooks)
62
+ self._audio_tokenizer = mimi
63
+
64
+ self._watermarker = load_watermarker(device=device)
65
+
66
+ self.sample_rate = mimi.sample_rate
67
+ self.device = device
68
+
69
+ def _tokenize_text_segment(self, text: str, speaker: int) -> Tuple[torch.Tensor, torch.Tensor]:
70
+ frame_tokens = []
71
+ frame_masks = []
72
+
73
+ text_tokens = self._text_tokenizer.encode(f"[{speaker}] {text.lstrip()}")
74
+ text_frame = torch.zeros(len(text_tokens), self._frame_size).long()
75
+ text_frame_mask = torch.zeros(len(text_tokens), self._frame_size).bool()
76
+ text_frame[:, -1] = torch.tensor(text_tokens)
77
+ text_frame_mask[:, -1] = True
78
+
79
+ frame_tokens.append(text_frame.to(self.device))
80
+ frame_masks.append(text_frame_mask.to(self.device))
81
+
82
+ return torch.cat(frame_tokens, dim=0), torch.cat(frame_masks, dim=0)
83
+
84
+ def _tokenize_audio(self, audio: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
85
+ assert audio.ndim == 1, "Audio must be single channel"
86
+
87
+ frame_tokens = []
88
+ frame_masks = []
89
+
90
+ # (K, T)
91
+ audio = audio.to(self.device)
92
+ audio_tokens = self._audio_tokenizer.encode(audio.unsqueeze(0).unsqueeze(0))[0]
93
+ # add EOS frame
94
+ eos_frame = torch.zeros(audio_tokens.size(0), 1).to(self.device)
95
+ audio_tokens = torch.cat([audio_tokens, eos_frame], dim=1)
96
+
97
+ audio_frame = torch.zeros(audio_tokens.size(1), self._frame_size).long().to(self.device)
98
+ audio_frame_mask = torch.zeros(audio_tokens.size(1), self._frame_size).bool().to(self.device)
99
+ audio_frame[:, :-1] = audio_tokens.transpose(0, 1)
100
+ audio_frame_mask[:, :-1] = True
101
+
102
+ frame_tokens.append(audio_frame)
103
+ frame_masks.append(audio_frame_mask)
104
+
105
+ return torch.cat(frame_tokens, dim=0), torch.cat(frame_masks, dim=0)
106
+
107
+ def _tokenize_segment(self, segment: Segment) -> Tuple[torch.Tensor, torch.Tensor]:
108
+ """
109
+ Returns:
110
+ (seq_len, audio_num_codebooks + 1), (seq_len, audio_num_codebooks + 1)
111
+ """
112
+ text_tokens, text_masks = self._tokenize_text_segment(segment.text, segment.speaker)
113
+ audio_tokens, audio_masks = self._tokenize_audio(segment.audio)
114
+
115
+ return torch.cat([text_tokens, audio_tokens], dim=0), torch.cat([text_masks, audio_masks], dim=0)
116
+
117
+ @torch.inference_mode()
118
+ def generate(
119
+ self,
120
+ text: str,
121
+ speaker: int,
122
+ context: List[Segment],
123
+ max_audio_length_ms: float = 90_000,
124
+ temperature: float = 0.9,
125
+ topk: int = 50,
126
+ ) -> torch.Tensor:
127
+ self._model.reset_caches()
128
+
129
+ max_generation_len = int(max_audio_length_ms / 80)
130
+ tokens, tokens_mask = [], []
131
+ for segment in context:
132
+ segment_tokens, segment_tokens_mask = self._tokenize_segment(segment)
133
+ tokens.append(segment_tokens)
134
+ tokens_mask.append(segment_tokens_mask)
135
+
136
+ gen_segment_tokens, gen_segment_tokens_mask = self._tokenize_text_segment(text, speaker)
137
+ tokens.append(gen_segment_tokens)
138
+ tokens_mask.append(gen_segment_tokens_mask)
139
+
140
+ prompt_tokens = torch.cat(tokens, dim=0).long().to(self.device)
141
+ prompt_tokens_mask = torch.cat(tokens_mask, dim=0).bool().to(self.device)
142
+
143
+ samples = []
144
+ curr_tokens = prompt_tokens.unsqueeze(0)
145
+ curr_tokens_mask = prompt_tokens_mask.unsqueeze(0)
146
+ curr_pos = torch.arange(0, prompt_tokens.size(0)).unsqueeze(0).long().to(self.device)
147
+
148
+ max_seq_len = 2048
149
+ max_context_len = max_seq_len - max_generation_len
150
+ if curr_tokens.size(1) >= max_context_len:
151
+ raise ValueError(
152
+ f"Inputs too long, must be below max_seq_len - max_generation_len: {max_context_len}"
153
+ )
154
+
155
+ for _ in range(max_generation_len):
156
+ sample = self._model.generate_frame(curr_tokens, curr_tokens_mask, curr_pos, temperature, topk)
157
+ if torch.all(sample == 0):
158
+ break # eos
159
+
160
+ samples.append(sample)
161
+
162
+ curr_tokens = torch.cat([sample, torch.zeros(1, 1).long().to(self.device)], dim=1).unsqueeze(1)
163
+ curr_tokens_mask = torch.cat(
164
+ [torch.ones_like(sample).bool(), torch.zeros(1, 1).bool().to(self.device)], dim=1
165
+ ).unsqueeze(1)
166
+ curr_pos = curr_pos[:, -1:] + 1
167
+
168
+ audio = self._audio_tokenizer.decode(torch.stack(samples).permute(1, 2, 0)).squeeze(0).squeeze(0)
169
+
170
+ # This applies an imperceptible watermark to identify audio as AI-generated.
171
+ # If using Miso TTS in another application, use your own private key and keep it secret.
172
+ audio, wm_sample_rate = watermark(self._watermarker, audio, self.sample_rate, MISO_TTS_WATERMARK)
173
+ audio = torchaudio.functional.resample(audio, orig_freq=wm_sample_rate, new_freq=self.sample_rate)
174
+
175
+ return audio
176
+
177
+
178
+ def _state_dict_from_checkpoint(checkpoint: object) -> dict[str, torch.Tensor]:
179
+ if not isinstance(checkpoint, dict):
180
+ raise TypeError(f"Expected checkpoint dict, got {type(checkpoint).__name__}")
181
+
182
+ for key in ("state_dict", "model_state_dict", "model"):
183
+ value = checkpoint.get(key)
184
+ if isinstance(value, dict):
185
+ checkpoint = value
186
+ break
187
+
188
+ state_dict = {}
189
+ for key, value in checkpoint.items():
190
+ if torch.is_tensor(value):
191
+ state_dict[key.removeprefix("module.")] = value
192
+ if not state_dict:
193
+ raise ValueError("Checkpoint did not contain any tensor state_dict entries")
194
+ return state_dict
195
+
196
+
197
+ def _load_model(
198
+ model_path_or_repo_id: str,
199
+ config: ModelArgs,
200
+ device: str,
201
+ dtype: torch.dtype,
202
+ ) -> Model:
203
+ if os.path.isfile(model_path_or_repo_id):
204
+ model_file = model_path_or_repo_id
205
+ elif os.path.isdir(model_path_or_repo_id):
206
+ model_file = os.path.join(model_path_or_repo_id, "model.safetensors")
207
+ else:
208
+ model_file = hf_hub_download(repo_id=model_path_or_repo_id, filename="model.safetensors")
209
+
210
+ if os.path.isfile(model_file):
211
+ model = Model(config)
212
+ if model_file.endswith(".safetensors"):
213
+ try:
214
+ from safetensors.torch import load_file
215
+ except ImportError as exc:
216
+ raise ImportError("Install safetensors to load .safetensors checkpoint files") from exc
217
+
218
+ state_dict = load_file(model_file, device="cpu")
219
+ else:
220
+ checkpoint = torch.load(model_file, map_location="cpu")
221
+ state_dict = _state_dict_from_checkpoint(checkpoint)
222
+ model.load_state_dict(state_dict)
223
+ else:
224
+ raise FileNotFoundError(f"Could not resolve model checkpoint: {model_path_or_repo_id}")
225
+
226
+ model.to(device=device, dtype=dtype)
227
+ model.eval()
228
+ return model
229
+
230
+
231
+ def load_miso_8b(
232
+ device: str = "cuda",
233
+ model_path_or_repo_id: Optional[str] = None,
234
+ dtype: torch.dtype = torch.bfloat16,
235
+ ) -> Generator:
236
+ source = model_path_or_repo_id or os.environ.get("MISO_TTS_8B_MODEL", DEFAULT_MISO_TTS_REPO_ID)
237
+ model = _load_model(source, MISO_TTS_8B_CONFIG, device=device, dtype=dtype)
238
+ return Generator(model)
models.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ import contextlib
3
+ import io
4
+ from typing import Tuple
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+ import torch.nn.functional as F
9
+ from huggingface_hub import PyTorchModelHubMixin
10
+
11
+ with contextlib.redirect_stdout(io.StringIO()) as _torchtune_stdout:
12
+ from torchtune.models import llama3_2
13
+
14
+ _torchtune_import_output = _torchtune_stdout.getvalue()
15
+ if _torchtune_import_output.strip() != "import error: No module named 'triton'":
16
+ print(_torchtune_import_output, end="")
17
+
18
+
19
+ def llama3_2_8B():
20
+ return llama3_2.llama3_2(
21
+ vocab_size=128_256,
22
+ num_layers=32,
23
+ num_heads=32,
24
+ num_kv_heads=8,
25
+ embed_dim=4096,
26
+ max_seq_len=2048,
27
+ intermediate_dim=14_336,
28
+ attn_dropout=0.1,
29
+ norm_eps=1e-5,
30
+ rope_base=500_000,
31
+ scale_factor=32,
32
+ )
33
+
34
+
35
+ def llama3_2_300M():
36
+ return llama3_2.llama3_2(
37
+ vocab_size=128_256,
38
+ num_layers=8,
39
+ num_heads=24,
40
+ num_kv_heads=6,
41
+ embed_dim=1536,
42
+ max_seq_len=2048,
43
+ intermediate_dim=6912,
44
+ attn_dropout=0.1,
45
+ norm_eps=1e-5,
46
+ rope_base=500_000,
47
+ scale_factor=32,
48
+ )
49
+
50
+
51
+ FLAVORS = {
52
+ "llama-8B": llama3_2_8B,
53
+ "llama-300M": llama3_2_300M,
54
+ }
55
+
56
+
57
+ def _prepare_transformer(model):
58
+ embed_dim = model.tok_embeddings.embedding_dim
59
+ model.tok_embeddings = nn.Identity()
60
+ model.output = nn.Identity()
61
+ return model, embed_dim
62
+
63
+
64
+ def _create_causal_mask(seq_len: int, device: torch.device):
65
+ return torch.tril(torch.ones(seq_len, seq_len, dtype=torch.bool, device=device))
66
+
67
+
68
+ def _index_causal_mask(mask: torch.Tensor, input_pos: torch.Tensor):
69
+ """
70
+ Args:
71
+ mask: (max_seq_len, max_seq_len)
72
+ input_pos: (batch_size, seq_len)
73
+
74
+ Returns:
75
+ (batch_size, seq_len, max_seq_len)
76
+ """
77
+ r = mask[input_pos, :]
78
+ return r
79
+
80
+
81
+ def _multinomial_sample_one_no_sync(probs): # Does multinomial sampling without a cuda synchronization
82
+ q = torch.empty_like(probs).exponential_(1)
83
+ return torch.argmax(probs / q, dim=-1, keepdim=True).to(dtype=torch.int)
84
+
85
+
86
+ def sample_topk(logits: torch.Tensor, topk: int, temperature: float):
87
+ logits = logits / temperature
88
+
89
+ filter_value: float = -float("Inf")
90
+ indices_to_remove = logits < torch.topk(logits, topk)[0][..., -1, None]
91
+ scores_processed = logits.masked_fill(indices_to_remove, filter_value)
92
+ scores_processed = torch.nn.functional.log_softmax(scores_processed, dim=-1)
93
+ probs = torch.nn.functional.softmax(scores_processed, dim=-1)
94
+
95
+ sample_token = _multinomial_sample_one_no_sync(probs)
96
+ return sample_token
97
+
98
+
99
+ def _masked_cross_entropy(logits, targets, mask, vocab_size):
100
+ losses = F.cross_entropy(logits.reshape(-1, vocab_size), targets.reshape(-1), reduction="none")
101
+ weights = mask.reshape(-1).to(losses.dtype)
102
+ total = weights.sum().clamp_min(1.0)
103
+ return (losses * weights).sum() / total, total
104
+
105
+
106
+ @dataclass
107
+ class ModelArgs:
108
+ backbone_flavor: str
109
+ decoder_flavor: str
110
+ text_vocab_size: int
111
+ audio_vocab_size: int
112
+ audio_num_codebooks: int
113
+
114
+
115
+ MISO_TTS_8B_CONFIG = ModelArgs(
116
+ backbone_flavor="llama-8B",
117
+ decoder_flavor="llama-300M",
118
+ text_vocab_size=128_256,
119
+ audio_vocab_size=2051,
120
+ audio_num_codebooks=32,
121
+ )
122
+
123
+
124
+ class Model(
125
+ nn.Module,
126
+ PyTorchModelHubMixin,
127
+ pipeline_tag="text-to-speech",
128
+ license="other",
129
+ ):
130
+ def __init__(self, config: ModelArgs):
131
+ super().__init__()
132
+ self.config = config
133
+
134
+ self.backbone, backbone_dim = _prepare_transformer(FLAVORS[config.backbone_flavor]())
135
+ self.decoder, decoder_dim = _prepare_transformer(FLAVORS[config.decoder_flavor]())
136
+
137
+ self.text_embeddings = nn.Embedding(config.text_vocab_size, backbone_dim)
138
+ self.audio_embeddings = nn.Embedding(config.audio_vocab_size * config.audio_num_codebooks, backbone_dim)
139
+
140
+ self.projection = nn.Linear(backbone_dim, decoder_dim, bias=False)
141
+ self.codebook0_head = nn.Linear(backbone_dim, config.audio_vocab_size, bias=False)
142
+ self.audio_head = nn.Parameter(torch.empty(config.audio_num_codebooks - 1, decoder_dim, config.audio_vocab_size))
143
+
144
+ def setup_caches(self, max_batch_size: int) -> None:
145
+ """Setup KV caches and return a causal mask."""
146
+ dtype = next(self.parameters()).dtype
147
+ device = next(self.parameters()).device
148
+
149
+ self.backbone.setup_caches(max_batch_size, dtype)
150
+ self.decoder.setup_caches(max_batch_size, dtype, decoder_max_seq_len=self.config.audio_num_codebooks)
151
+
152
+ self.register_buffer("backbone_causal_mask", _create_causal_mask(self.backbone.max_seq_len, device))
153
+ self.register_buffer("decoder_causal_mask", _create_causal_mask(self.config.audio_num_codebooks, device))
154
+
155
+ def generate_frame(
156
+ self,
157
+ tokens: torch.Tensor,
158
+ tokens_mask: torch.Tensor,
159
+ input_pos: torch.Tensor,
160
+ temperature: float,
161
+ topk: int,
162
+ ) -> torch.Tensor:
163
+ """
164
+ Args:
165
+ tokens: (batch_size, seq_len, audio_num_codebooks+1)
166
+ tokens_mask: (batch_size, seq_len, audio_num_codebooks+1)
167
+ input_pos: (batch_size, seq_len) positions for each token
168
+ mask: (batch_size, seq_len, max_seq_len
169
+
170
+ Returns:
171
+ (batch_size, audio_num_codebooks) sampled tokens
172
+ """
173
+ dtype = next(self.parameters()).dtype
174
+ b, s, _ = tokens.size()
175
+
176
+ assert self.backbone.caches_are_enabled(), "backbone caches are not enabled"
177
+ curr_backbone_mask = _index_causal_mask(self.backbone_causal_mask, input_pos)
178
+ embeds = self._embed_tokens(tokens)
179
+ masked_embeds = embeds * tokens_mask.unsqueeze(-1)
180
+ h = masked_embeds.sum(dim=2)
181
+ h = self.backbone(h, input_pos=input_pos, mask=curr_backbone_mask).to(dtype=dtype)
182
+
183
+ last_h = h[:, -1, :]
184
+ c0_logits = self.codebook0_head(last_h)
185
+ c0_sample = sample_topk(c0_logits, topk, temperature)
186
+ c0_embed = self._embed_audio(0, c0_sample)
187
+
188
+ curr_h = torch.cat([last_h.unsqueeze(1), c0_embed], dim=1)
189
+ curr_sample = c0_sample.clone()
190
+ curr_pos = torch.arange(0, curr_h.size(1), device=curr_h.device).unsqueeze(0).repeat(curr_h.size(0), 1)
191
+
192
+ # Decoder caches must be reset every frame.
193
+ self.decoder.reset_caches()
194
+ for i in range(1, self.config.audio_num_codebooks):
195
+ curr_decoder_mask = _index_causal_mask(self.decoder_causal_mask, curr_pos)
196
+ decoder_h = self.decoder(self.projection(curr_h), input_pos=curr_pos, mask=curr_decoder_mask).to(
197
+ dtype=dtype
198
+ )
199
+ ci_logits = torch.mm(decoder_h[:, -1, :], self.audio_head[i - 1])
200
+ ci_sample = sample_topk(ci_logits, topk, temperature)
201
+ ci_embed = self._embed_audio(i, ci_sample)
202
+
203
+ curr_h = ci_embed
204
+ curr_sample = torch.cat([curr_sample, ci_sample], dim=1)
205
+ curr_pos = curr_pos[:, -1:] + 1
206
+
207
+ return curr_sample
208
+
209
+ def forward(
210
+ self,
211
+ tokens: torch.Tensor,
212
+ tokens_mask: torch.Tensor,
213
+ targets: torch.Tensor,
214
+ targets_mask: torch.Tensor,
215
+ decoder_idx: torch.Tensor,
216
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
217
+ dtype = next(self.parameters()).dtype
218
+ b, s, nc_plus_1 = tokens.size()
219
+ num_codebooks = nc_plus_1 - 1
220
+ _, s_amortized = decoder_idx.size()
221
+
222
+ targets_c0 = targets[:, :, 0]
223
+ am_idx = decoder_idx.view(b, s_amortized, 1).expand(b, s_amortized, num_codebooks - 1)
224
+ targets_c1_plus = torch.gather(targets[:, :, 1:], dim=1, index=am_idx)
225
+ valid_c0 = targets_mask[:, :, 0].bool()
226
+ valid_c1_plus = torch.gather(targets_mask[:, :, 1:].bool(), dim=1, index=am_idx)
227
+
228
+ embeds = self._embed_tokens(tokens)
229
+ masked_embeds = embeds * tokens_mask.unsqueeze(-1)
230
+ h = masked_embeds.sum(dim=2)
231
+ h = self.backbone(h).to(dtype=dtype)
232
+
233
+ c0_logits = self.codebook0_head(h)
234
+ h = h.unsqueeze(2)
235
+
236
+ target_frame = torch.cat([targets, torch.zeros(b, s, 1, device=h.device, dtype=targets.dtype)], dim=2)
237
+ target_embeds = self._embed_tokens(target_frame)
238
+ decoder_input = torch.cat([h, target_embeds[:, :, :-2, :]], dim=2)
239
+
240
+ idx = decoder_idx.view(b, s_amortized, 1, 1).expand(
241
+ b,
242
+ s_amortized,
243
+ num_codebooks,
244
+ decoder_input.size(-1),
245
+ )
246
+ decoder_input_amortized = torch.gather(decoder_input, dim=1, index=idx)
247
+ decoder_h = self.decoder(
248
+ self.projection(decoder_input_amortized).view(b * s_amortized, num_codebooks, -1).to(dtype=dtype)
249
+ )
250
+ decoder_h = decoder_h.view(b, s_amortized, num_codebooks, -1)
251
+
252
+ logits_c1_plus = torch.einsum(
253
+ "bsid,idv->bsiv",
254
+ decoder_h[:, :, 1:, :],
255
+ self.audio_head,
256
+ )
257
+
258
+ c0_loss, c0_weight = _masked_cross_entropy(
259
+ c0_logits,
260
+ targets_c0,
261
+ valid_c0,
262
+ self.config.audio_vocab_size,
263
+ )
264
+ c1_plus_loss, c1_weight = _masked_cross_entropy(
265
+ logits_c1_plus,
266
+ targets_c1_plus,
267
+ valid_c1_plus,
268
+ self.config.audio_vocab_size,
269
+ )
270
+ loss = (c0_loss * c0_weight + c1_plus_loss * c1_weight) / (c0_weight + c1_weight).clamp_min(1.0)
271
+ return c0_logits, logits_c1_plus, c0_loss, c1_plus_loss, loss
272
+
273
+ def reset_caches(self):
274
+ self.backbone.reset_caches()
275
+ self.decoder.reset_caches()
276
+
277
+ def _embed_audio(self, codebook: int, tokens: torch.Tensor) -> torch.Tensor:
278
+ return self.audio_embeddings(tokens + codebook * self.config.audio_vocab_size)
279
+
280
+ def _embed_tokens(self, tokens: torch.Tensor) -> torch.Tensor:
281
+ text_embeds = self.text_embeddings(tokens[:, :, -1]).unsqueeze(-2)
282
+
283
+ audio_tokens = tokens[:, :, :-1] + (
284
+ self.config.audio_vocab_size * torch.arange(self.config.audio_num_codebooks, device=tokens.device)
285
+ )
286
+ audio_embeds = self.audio_embeddings(audio_tokens.view(-1)).reshape(
287
+ tokens.size(0), tokens.size(1), self.config.audio_num_codebooks, -1
288
+ )
289
+
290
+ return torch.cat([audio_embeds, text_embeds], dim=-2)
moshi_compat.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+ from torch import nn
5
+
6
+ from moshi.utils import quantize
7
+
8
+
9
+ def patch_bitsandbytes_import_for_unquantized_layers() -> None:
10
+ original_linear = quantize.linear
11
+ original_multi_linear = quantize.multi_linear
12
+
13
+ def linear(module: nn.Module, x: torch.Tensor, name: str = "weight") -> torch.Tensor:
14
+ if quantize.is_quantized(module, name):
15
+ return original_linear(module, x, name)
16
+ return nn.functional.linear(x, getattr(module, name))
17
+
18
+ def multi_linear(
19
+ num_steps: int,
20
+ schedule: list[int] | None,
21
+ module: nn.Module,
22
+ x: torch.Tensor,
23
+ offset: int,
24
+ name: str = "weight",
25
+ ) -> torch.Tensor:
26
+ if quantize.is_quantized(module, name):
27
+ return original_multi_linear(num_steps, schedule, module, x, offset, name)
28
+
29
+ weight = getattr(module, name)
30
+ num_linear = num_steps if schedule is None else max(schedule) + 1
31
+ weight = weight.view(num_linear, -1, weight.shape[-1])
32
+
33
+ outputs = []
34
+ for t in range(x.shape[1]):
35
+ linear_index = t + offset
36
+ if schedule is not None:
37
+ linear_index = schedule[linear_index]
38
+ outputs.append(nn.functional.linear(x[:, t], weight[linear_index]))
39
+ return torch.stack(outputs, 1)
40
+
41
+ quantize.linear = linear
42
+ quantize.multi_linear = multi_linear
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch==2.8.0
2
+ torchaudio==2.8.0
3
+ tokenizers==0.21.0
4
+ transformers==4.49.0
5
+ huggingface_hub==0.28.1
6
+ moshi==0.2.12
7
+ torchtune==0.4.0
8
+ torchao==0.9.0
9
+ silentcipher @ git+https://github.com/SesameAILabs/silentcipher@master
10
+ pydantic==2.10.6
11
+ safetensors
12
+ bitsandbytes; sys_platform == "linux"
watermarking.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+
3
+ import silentcipher
4
+ import torch
5
+ import torchaudio
6
+
7
+ # Warning: When using MisoTTS in another application, you must set this key
8
+ # and keep the watermark key secret.
9
+ MISO_TTS_WATERMARK = [0, 0, 0, 0, 0]
10
+
11
+
12
+ def cli_check_audio() -> None:
13
+ parser = argparse.ArgumentParser()
14
+ parser.add_argument("--audio_path", type=str, required=True)
15
+ args = parser.parse_args()
16
+
17
+ check_audio_from_file(args.audio_path)
18
+
19
+
20
+ def load_watermarker(device: str = "cuda") -> silentcipher.server.Model:
21
+ try:
22
+ model = silentcipher.get_model(
23
+ model_type="44.1k",
24
+ device=device,
25
+ )
26
+ except Exception as exc:
27
+ raise RuntimeError(
28
+ "Failed to load the SilentCipher watermarking model. Miso TTS weights may "
29
+ "already be downloaded successfully; this is a separate Hugging Face Hub "
30
+ "download from sony/silentcipher. Retry the command to resume the cache, or "
31
+ "pre-download it with `uv run huggingface-cli download sony/silentcipher`."
32
+ ) from exc
33
+ return model
34
+
35
+
36
+ @torch.inference_mode()
37
+ def watermark(
38
+ watermarker: silentcipher.server.Model,
39
+ audio_array: torch.Tensor,
40
+ sample_rate: int,
41
+ watermark_key: list[int],
42
+ ) -> tuple[torch.Tensor, int]:
43
+ audio_array_44khz = torchaudio.functional.resample(audio_array, orig_freq=sample_rate, new_freq=44100)
44
+ encoded, _ = watermarker.encode_wav(audio_array_44khz, 44100, watermark_key, calc_sdr=False, message_sdr=36)
45
+
46
+ output_sample_rate = min(44100, sample_rate)
47
+ encoded = torchaudio.functional.resample(encoded, orig_freq=44100, new_freq=output_sample_rate)
48
+ return encoded, output_sample_rate
49
+
50
+
51
+ @torch.inference_mode()
52
+ def verify(
53
+ watermarker: silentcipher.server.Model,
54
+ watermarked_audio: torch.Tensor,
55
+ sample_rate: int,
56
+ watermark_key: list[int],
57
+ ) -> bool:
58
+ watermarked_audio_44khz = torchaudio.functional.resample(watermarked_audio, orig_freq=sample_rate, new_freq=44100)
59
+ result = watermarker.decode_wav(watermarked_audio_44khz, 44100, phase_shift_decoding=True)
60
+
61
+ is_watermarked = result["status"]
62
+ if is_watermarked:
63
+ is_miso_tts_watermarked = result["messages"][0] == watermark_key
64
+ else:
65
+ is_miso_tts_watermarked = False
66
+
67
+ return is_watermarked and is_miso_tts_watermarked
68
+
69
+
70
+ def check_audio_from_file(audio_path: str) -> None:
71
+ watermarker = load_watermarker(device="cuda")
72
+
73
+ audio_array, sample_rate = load_audio(audio_path)
74
+ is_watermarked = verify(watermarker, audio_array, sample_rate, MISO_TTS_WATERMARK)
75
+
76
+ outcome = "Watermarked" if is_watermarked else "Not watermarked"
77
+ print(f"{outcome}: {audio_path}")
78
+
79
+
80
+ def load_audio(audio_path: str) -> tuple[torch.Tensor, int]:
81
+ audio_array, sample_rate = torchaudio.load(audio_path)
82
+ audio_array = audio_array.mean(dim=0)
83
+ return audio_array, int(sample_rate)
84
+
85
+
86
+ if __name__ == "__main__":
87
+ cli_check_audio()