Add Miso TTS 8B ZeroGPU demo
Browse files- README.md +11 -7
- __pycache__/app.cpython-312.pyc +0 -0
- __pycache__/generator.cpython-312.pyc +0 -0
- __pycache__/models.cpython-312.pyc +0 -0
- __pycache__/moshi_compat.cpython-312.pyc +0 -0
- __pycache__/watermarking.cpython-312.pyc +0 -0
- app.py +99 -0
- generator.py +238 -0
- models.py +290 -0
- moshi_compat.py +42 -0
- requirements.txt +12 -0
- watermarking.py +87 -0
README.md
CHANGED
|
@@ -1,13 +1,17 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
| 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()
|