shangeth commited on
Commit
02dfada
·
verified ·
1 Parent(s): 4d6b9a9

v1 delay-pattern (k=8): replace flat k=3 with MusicGen-style delay

Browse files
README.md CHANGED
@@ -14,52 +14,62 @@ tags:
14
  - neural-codec
15
  pipeline_tag: text-to-speech
16
  datasets:
17
- - openslr/librispeech_asr
 
18
  ---
19
 
20
  # Wren-TTS-360M (v1)
21
 
22
  **Wren** is a series of small (<3B) multimodal speech LLMs covering TTS, ASR, and
23
- speech-language modelling. This is the first public checkpoint: **Wren-TTS-360M-v1**,
24
- a text-to-speech model that generates [Kyutai Mimi](https://huggingface.co/kyutai/mimi)
25
- neural-codec tokens from text and decodes them to 24 kHz waveform with the Mimi decoder.
26
- The autoregressive backbone is [HuggingFaceTB/SmolLM2-360M](https://huggingface.co/HuggingFaceTB/SmolLM2-360M).
27
 
28
- An early research checkpoint — useful for experimentation, not production.
29
 
30
  ## Architecture
31
 
32
  ```
33
- text ──► SmolLM2-360M ──► k audio-code heads ──► Mimi decoder ──► 24 kHz waveform
34
  ```
35
 
36
- - **Backbone:** SmolLM2-360M (causal LM, embeddings shared with text input)
37
  - **Audio tokenizer:** Mimi (`kyutai/mimi`), 12.5 fps, 2048-entry codebooks
38
- - **Codebooks used:** 3 (of Mimi's 8 extractable)
39
- - **Interleaved layout:** `[ text | <audio_sep> | cb0_f0 cb1_f0 cb2_f0 | cb0_f1 ... ]`
40
- - **Per-codebook heads:** `Linear(hidden, 2048)`. `cb0` has one extra output (index 2048)
41
- used as the `AUDIO_EOS` stop token.
42
- - **Optional speaker conditioning:** prepend `<audio_start> ref_codes <audio_end>` before the
43
- text prompt for zero-shot voice cloning from a short reference clip.
 
 
 
 
 
 
44
 
45
  ## Training data
46
 
47
- - [LibriSpeech](https://www.openslr.org/12) `train-clean-100` + `train-clean-360` — ~460 h, multi-speaker, English audiobooks
48
- - Trained for 5 epochs with effective batch size 32
 
49
 
50
- Text is lowercased before tokenization. This v1 checkpoint was trained on LibriSpeech
51
- only; LJSpeech was not included in this run.
52
 
53
  ## Usage
54
 
55
  ```bash
56
- pip install torch torchaudio transformers
57
  ```
58
 
59
- ### Text-to-speech
 
60
 
61
  ```python
62
  import torch
 
 
63
  from transformers import AutoModel, AutoProcessor
64
 
65
  model_id = "shangeth/Wren-TTS-360M-v1"
@@ -68,74 +78,75 @@ device = "cuda" if torch.cuda.is_available() else "cpu"
68
  processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
69
  model = AutoModel.from_pretrained(model_id, trust_remote_code=True).to(device).eval()
70
 
 
 
 
 
 
 
 
 
71
  inputs = processor("Hello world, how are you today?")
72
  inputs = {k: v.to(device) for k, v in inputs.items()}
73
 
74
  waveform = model.generate(
75
  **inputs,
 
76
  max_audio_frames=200,
77
  min_audio_frames=2,
78
- temperature=0.8,
79
- top_k=50,
80
- top_p=0.9,
81
  output_audio=True,
82
  )
83
  processor.save_audio(waveform, "out.wav")
84
  ```
85
 
86
- ### Zero-shot voice cloning
87
-
88
- ```python
89
- import torchaudio
90
 
91
- ref_wav, sr = torchaudio.load("reference.wav")
92
- ref_codes = model.encode_audio(ref_wav, sr)[:, :150] # cap at ~12 s of reference
93
 
94
- waveform = model.generate(
95
- **inputs,
96
- ref_codes=ref_codes,
97
- output_audio=True,
98
- max_audio_frames=200,
99
- min_audio_frames=2,
100
- temperature=0.8, top_k=50, top_p=0.9,
101
- )
102
- processor.save_audio(waveform, "cloned.wav")
103
- ```
104
 
105
- ## Sampling tips
106
 
107
- Defaults are `temperature=0.8`, `top_k=50`, `top_p=0.9`, `max_audio_frames=200` (~16 s).
108
- If you hear the model generating extra words past the prompt (hallucination), try:
 
109
 
110
- - Lower `temperature` (e.g. 0.6) and `top_p` (e.g. 0.8)
111
- - Raise `eos_bias` (e.g. 2.0–6.0) to make the model more eager to stop
112
- - Lower `max_audio_frames` to roughly `12 * len(text)`
113
- - Set `min_audio_frames=1` for very short prompts
 
 
114
 
115
  ## Limitations & known issues
116
 
117
- - **Hallucinated continuations:** the model sometimes generates plausible speech beyond
118
- the input text. This is a class-imbalance artifact of the `AUDIO_EOS` supervision
119
- (one EOS target per ~100–300 audio frames) and is being addressed in training.
120
  - **English only.**
121
- - **Limited expressiveness:** read-style prosody inherited from LibriSpeech audiobook data.
122
- - **Small backbone (360M)** + modest training data — quality is below production TTS systems.
123
- - **Val loss began to overfit** around epoch 5; v2 will add early stopping, LJSpeech,
124
- and EOS-class re-weighting.
 
125
 
126
  ## The Wren series
127
 
128
- Wren is a family of compact (<3B parameter) multimodal speech LLMs — small enough to run
129
- on a single consumer GPU, designed for open research on unified speech understanding and
130
- synthesis. Planned siblings:
131
 
132
  - **Wren-TTS** — text → speech (this release)
133
  - **Wren-ASR** — speech → text
134
  - **Wren-LM** — speech-language modelling / dialog
135
  - **Wren-Omni** — unified ASR + TTS + LM in one checkpoint
136
 
137
- All Wren models share the same design principles: small backbone LLM + neural audio codec,
138
- open weights, simple PyTorch checkpoints, reproducible training recipes.
139
 
140
  ## Repository contents
141
 
@@ -143,7 +154,7 @@ open weights, simple PyTorch checkpoints, reproducible training recipes.
143
  |---|---|
144
  | `model.safetensors` | Model weights |
145
  | `config.json` | `WrenConfig` (with `auto_map` for `trust_remote_code`) |
146
- | `tokenizer.json` + friends | SmolLM2 tokenizer with Wren's 4 special tokens added |
147
  | `processor_config.json` | `WrenProcessor` auto_map |
148
  | `configuration_wren.py` | `WrenConfig(PretrainedConfig)` |
149
  | `modeling_wren.py` | `WrenForTTS(PreTrainedModel)` — loads Mimi codec lazily on first generate |
@@ -160,11 +171,13 @@ open weights, simple PyTorch checkpoints, reproducible training recipes.
160
  url = {https://github.com/shangeth/wren}
161
  }
162
 
163
- @inproceedings{panayotov2015librispeech,
164
- title = {Librispeech: an ASR corpus based on public domain audio books},
165
- author = {Panayotov, Vassil and Chen, Guoguo and Povey, Daniel and Khudanpur, Sanjeev},
166
- booktitle = {ICASSP},
167
- year = {2015}
 
 
168
  }
169
  ```
170
 
 
14
  - neural-codec
15
  pipeline_tag: text-to-speech
16
  datasets:
17
+ - mythicinfinity/libritts_r
18
+ - keithito/lj_speech
19
  ---
20
 
21
  # Wren-TTS-360M (v1)
22
 
23
  **Wren** is a series of small (<3B) multimodal speech LLMs covering TTS, ASR, and
24
+ speech-language modelling. **Wren-TTS-360M-v1** generates
25
+ [Kyutai Mimi](https://huggingface.co/kyutai/mimi) neural-codec tokens from text
26
+ using a [HuggingFaceTB/SmolLM2-360M](https://huggingface.co/HuggingFaceTB/SmolLM2-360M)
27
+ backbone, then decodes to 24 kHz waveform with the Mimi decoder.
28
 
29
+ An open research checkpoint — useful for experimentation, not production.
30
 
31
  ## Architecture
32
 
33
  ```
34
+ text ──► SmolLM2-360M ──► k=8 parallel Mimi heads ──► Mimi decoder ──► 24 kHz
35
  ```
36
 
37
+ - **Backbone:** SmolLM2-360M (causal LM; text + audio share the same backbone)
38
  - **Audio tokenizer:** Mimi (`kyutai/mimi`), 12.5 fps, 2048-entry codebooks
39
+ - **Codebooks used:** all 8 Mimi codebooks
40
+ - **Layout:** **MusicGen-style delay pattern** at each step, k summed codebook
41
+ embeddings go in, k parallel heads predict k tokens out. Codebook q at frame f
42
+ lives at step `s = f + q`, so same-frame RVQ conditioning is preserved via the delay.
43
+ - **Per-codebook input tables:** `Embedding(2049, hidden)` extra row = `AUDIO_PAD` at
44
+ sequence edges.
45
+ - **Per-codebook output heads:** `Linear(hidden, 2048)` for cb1..cb7.
46
+ cb0 gets `Linear(hidden, 2049)` with the extra class = `AUDIO_EOS` (stop token).
47
+ - **Speaker conditioning (required):** prepend `<|reference_start|> ref_codes <|reference_end|>`
48
+ to the prompt; `ref_codes` is the Mimi encoding of a short reference clip. The model was
49
+ trained multispeaker-only and expects a reference at inference — without one, output quality
50
+ is poor.
51
 
52
  ## Training data
53
 
54
+ - [LibriTTS-R](https://huggingface.co/datasets/mythicinfinity/libritts_r)
55
+ `train-clean-{100,360}` + `train-other-500` ~960 h multi-speaker English
56
+ - [LJSpeech](https://keithito.com/LJ-Speech-Dataset/) — ~24 h single speaker
57
 
58
+ Text casing and punctuation are preserved. Pass text naturally do not pre-lowercase.
 
59
 
60
  ## Usage
61
 
62
  ```bash
63
+ pip install torch torchaudio transformers datasets
64
  ```
65
 
66
+ > **A reference audio clip is required.** The model was trained multispeaker-only; without
67
+ > `ref_codes` it produces poor output. Any 3–12 s English speech clip works as the voice reference.
68
 
69
  ```python
70
  import torch
71
+ import numpy as np
72
+ from datasets import load_dataset
73
  from transformers import AutoModel, AutoProcessor
74
 
75
  model_id = "shangeth/Wren-TTS-360M-v1"
 
78
  processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
79
  model = AutoModel.from_pretrained(model_id, trust_remote_code=True).to(device).eval()
80
 
81
+ # Grab one LibriSpeech test-clean clip (~3.5s, not in training) as the reference voice.
82
+ # Swap this block for `torchaudio.load("your_reference.wav")` to use your own clip.
83
+ sample = next(iter(load_dataset("openslr/librispeech_asr", "clean", split="test", streaming=True)))
84
+ ref_wav = torch.from_numpy(np.asarray(sample["audio"]["array"], dtype=np.float32)).unsqueeze(0)
85
+ ref_sr = sample["audio"]["sampling_rate"]
86
+ ref_codes = model.encode_audio(ref_wav, ref_sr)[:, :150] # cap at ~12s; encode_audio resamples to 24 kHz
87
+
88
+ # Tokenize the target text and generate speech in the reference voice
89
  inputs = processor("Hello world, how are you today?")
90
  inputs = {k: v.to(device) for k, v in inputs.items()}
91
 
92
  waveform = model.generate(
93
  **inputs,
94
+ ref_codes=ref_codes,
95
  max_audio_frames=200,
96
  min_audio_frames=2,
97
+ temperature=0.8, top_k=50, top_p=0.9,
 
 
98
  output_audio=True,
99
  )
100
  processor.save_audio(waveform, "out.wav")
101
  ```
102
 
103
+ ## Sampling tips
 
 
 
104
 
105
+ Defaults: `temperature=0.8`, `top_k=50`, `top_p=0.9`, `max_audio_frames=200` (~16 s).
106
+ If you hear the model generating extra speech past the intended text (hallucination):
107
 
108
+ - Raise `eos_bias` — e.g. 2.0–6.0 — to make the model more eager to stop
109
+ - Lower `temperature` (0.6) and `top_p` (0.8)
110
+ - Set `max_audio_frames` ≈ `12 * len(text_in_chars)`
111
+ - Set `min_audio_frames=1` for very short prompts
 
 
 
 
 
 
112
 
113
+ ## Why delay pattern
114
 
115
+ Mimi uses **residual vector quantization (RVQ)**: cb0 is semantic, cb1..cb7 encode
116
+ successive residuals. cb_q is only meaningful given cb0..cb_{q-1}, so same-frame
117
+ conditioning matters.
118
 
119
+ A flat interleaved layout (`cb0_f0, cb1_f0, ..., cb0_f1, ...`) preserves that
120
+ conditioning best but balloons sequence length by `k×` and forces `k` autoregressive
121
+ LLM calls per frame. The delay pattern keeps RVQ conditioning (cb_q at frame f is
122
+ predicted from a hidden state that has already attended over cb0..cb_{q-1} of the
123
+ same frame) while cutting sequence length to `T + k - 1` and LLM calls to **one per
124
+ step** — enabling all 8 Mimi codebooks without blowing up context.
125
 
126
  ## Limitations & known issues
127
 
128
+ - **Hallucinated continuations**: occasionally generates plausible speech past the
129
+ input text. Mitigate with `eos_bias` at inference.
 
130
  - **English only.**
131
+ - **Audiobook-style prosody** inherited from LibriTTS-R; not as expressive as modern
132
+ conversational TTS.
133
+ - **Small backbone (360M)** quality is below frontier TTS systems.
134
+ - cb0 begins to overfit earlier than cb3–cb7; the released checkpoint is the
135
+ best-epoch point (by overall val loss) from the full training run.
136
 
137
  ## The Wren series
138
 
139
+ Wren is a family of compact (<3B parameter) multimodal speech LLMs — small enough
140
+ to run on a single consumer GPU, designed for open research on unified speech
141
+ understanding and synthesis. Planned siblings:
142
 
143
  - **Wren-TTS** — text → speech (this release)
144
  - **Wren-ASR** — speech → text
145
  - **Wren-LM** — speech-language modelling / dialog
146
  - **Wren-Omni** — unified ASR + TTS + LM in one checkpoint
147
 
148
+ All Wren models share the same design principles: small backbone LLM + neural
149
+ audio codec, open weights, simple PyTorch checkpoints, reproducible training recipes.
150
 
151
  ## Repository contents
152
 
 
154
  |---|---|
155
  | `model.safetensors` | Model weights |
156
  | `config.json` | `WrenConfig` (with `auto_map` for `trust_remote_code`) |
157
+ | `tokenizer.json` + friends | SmolLM2 tokenizer with Wren's 3 special tokens added |
158
  | `processor_config.json` | `WrenProcessor` auto_map |
159
  | `configuration_wren.py` | `WrenConfig(PretrainedConfig)` |
160
  | `modeling_wren.py` | `WrenForTTS(PreTrainedModel)` — loads Mimi codec lazily on first generate |
 
171
  url = {https://github.com/shangeth/wren}
172
  }
173
 
174
+ @inproceedings{koizumi2023libritts,
175
+ title = {LibriTTS-R: A Restored Multi-Speaker Text-to-Speech Corpus},
176
+ author = {Koizumi, Yuma and Zen, Heiga and Karita, Shigeki and Ding, Yifan
177
+ and Yatabe, Kohei and Morioka, Nobuyuki and Bacchiani, Michiel and
178
+ Zhang, Yu and Han, Wei and Bapna, Ankur},
179
+ booktitle = {Interspeech},
180
+ year = {2023}
181
  }
182
  ```
183
 
added_tokens.json CHANGED
@@ -1,6 +1,5 @@
1
  {
2
- "<|audio_end|>": 49155,
3
- "<|audio_eos|>": 49153,
4
- "<|audio_sep|>": 49152,
5
- "<|audio_start|>": 49154
6
  }
 
1
  {
2
+ "<|audio_start|>": 49152,
3
+ "<|reference_end|>": 49154,
4
+ "<|reference_start|>": 49153
 
5
  }
config.json CHANGED
@@ -2,20 +2,20 @@
2
  "architectures": [
3
  "WrenForTTS"
4
  ],
5
- "audio_end_id": 49155,
6
- "audio_eos_token_id": 49153,
7
- "audio_sep_id": 49152,
8
- "audio_start_id": 49154,
9
  "auto_map": {
10
  "AutoConfig": "configuration_wren.WrenConfig",
11
  "AutoModel": "modeling_wren.WrenForTTS"
12
  },
13
  "codebook_size": 2048,
14
  "dtype": "bfloat16",
15
- "k_codebooks": 3,
16
  "llm_name": "HuggingFaceTB/SmolLM2-360M",
17
  "mimi_model_name": "kyutai/mimi",
18
  "model_type": "wren",
 
 
 
19
  "sampling_rate": 24000,
20
  "transformers_version": "4.57.6",
21
  "vocab_size": 49160
 
2
  "architectures": [
3
  "WrenForTTS"
4
  ],
5
+ "audio_start_id": 49152,
 
 
 
6
  "auto_map": {
7
  "AutoConfig": "configuration_wren.WrenConfig",
8
  "AutoModel": "modeling_wren.WrenForTTS"
9
  },
10
  "codebook_size": 2048,
11
  "dtype": "bfloat16",
12
+ "k_codebooks": 8,
13
  "llm_name": "HuggingFaceTB/SmolLM2-360M",
14
  "mimi_model_name": "kyutai/mimi",
15
  "model_type": "wren",
16
+ "pattern": "delay",
17
+ "reference_end_id": 49154,
18
+ "reference_start_id": 49153,
19
  "sampling_rate": 24000,
20
  "transformers_version": "4.57.6",
21
  "vocab_size": 49160
configuration_wren.py CHANGED
@@ -9,13 +9,13 @@ class WrenConfig(PretrainedConfig):
9
  self,
10
  llm_name: str = "HuggingFaceTB/SmolLM2-360M",
11
  mimi_model_name: str = "kyutai/mimi",
12
- k_codebooks: int = 3,
13
  codebook_size: int = 2048,
14
  vocab_size: int = 49160,
15
- audio_sep_id: int = None,
16
- audio_eos_token_id: int = None,
17
- audio_start_id: int = None,
18
- audio_end_id: int = None,
19
  sampling_rate: int = 24000,
20
  **kwargs,
21
  ):
@@ -24,9 +24,8 @@ class WrenConfig(PretrainedConfig):
24
  self.k_codebooks = k_codebooks
25
  self.codebook_size = codebook_size
26
  self.vocab_size = vocab_size
27
- self.audio_sep_id = audio_sep_id
28
- self.audio_eos_token_id = audio_eos_token_id
29
  self.audio_start_id = audio_start_id
30
- self.audio_end_id = audio_end_id
 
31
  self.sampling_rate = sampling_rate
32
  super().__init__(**kwargs)
 
9
  self,
10
  llm_name: str = "HuggingFaceTB/SmolLM2-360M",
11
  mimi_model_name: str = "kyutai/mimi",
12
+ k_codebooks: int = 8,
13
  codebook_size: int = 2048,
14
  vocab_size: int = 49160,
15
+ # Special-token IDs (tokenized positions in the text vocab)
16
+ audio_start_id: int = None, # <|audio_start|> — text→target-audio boundary
17
+ reference_start_id: int = None, # <|reference_start|> — opens speaker-reference block
18
+ reference_end_id: int = None, # <|reference_end|> — closes speaker-reference block
19
  sampling_rate: int = 24000,
20
  **kwargs,
21
  ):
 
24
  self.k_codebooks = k_codebooks
25
  self.codebook_size = codebook_size
26
  self.vocab_size = vocab_size
 
 
27
  self.audio_start_id = audio_start_id
28
+ self.reference_start_id = reference_start_id
29
+ self.reference_end_id = reference_end_id
30
  self.sampling_rate = sampling_rate
31
  super().__init__(**kwargs)
model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:f8b90adfd5eb6aafeb1bf509833f4560020b7d201f2cb1043af318250f256d6e
3
- size 770881760
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:df687c893d29e93e454908d8a158cb16991e487dbdec86ecf94cccbe4ef37913
3
+ size 849556624
modeling_wren.py CHANGED
@@ -3,8 +3,19 @@ Wren-TTS model — a transformers-compatible wrapper over SmolLM2 + Mimi codeboo
3
 
4
  Designed for use with `AutoModel.from_pretrained(..., trust_remote_code=True)`.
5
  Self-contained: no imports from a `src/` folder.
 
 
 
 
 
 
 
 
 
 
6
  """
7
 
 
8
  from typing import Optional
9
 
10
  import torch
@@ -18,15 +29,6 @@ except ImportError:
18
 
19
 
20
  class WrenForTTS(PreTrainedModel):
21
- """
22
- SmolLM2 backbone + k separate audio-code embedding tables + k audio heads.
23
-
24
- Sequence layout fed to the LLM (via inputs_embeds):
25
- [ text tokens... | <audio_sep> | cb0_f0 | cb1_f0 | ... | cbk_f0 | cb0_f1 | ... ]
26
-
27
- cb0's head has an extra output class `AUDIO_EOS = codebook_size` used as stop token.
28
- """
29
-
30
  config_class = WrenConfig
31
  base_model_prefix = "wren"
32
 
@@ -34,18 +36,21 @@ class WrenForTTS(PreTrainedModel):
34
  super().__init__(config)
35
  self.k = config.k_codebooks
36
  self.AUDIO_EOS = config.codebook_size
 
37
 
38
  # Build backbone from its config only. Pretrained weights for the backbone
39
  # are included in our own state_dict, so no need to re-download here.
40
- # Set vocab_size directly on the sub-config to avoid a subsequent
41
- # resize_token_embeddings call (which breaks under meta-tensor init).
42
  llm_cfg = AutoConfig.from_pretrained(config.llm_name)
43
  llm_cfg.vocab_size = config.vocab_size
44
  self.llm = AutoModelForCausalLM.from_config(llm_cfg)
45
 
46
  hidden = self.llm.config.hidden_size
 
 
 
 
47
  self.audio_embeds = nn.ModuleList([
48
- nn.Embedding(config.codebook_size, hidden)
49
  for _ in range(self.k)
50
  ])
51
  self.audio_heads = nn.ModuleList([
@@ -53,6 +58,7 @@ class WrenForTTS(PreTrainedModel):
53
  for i in range(self.k)
54
  ])
55
 
 
56
  self._mimi = None # lazy-loaded on first use
57
 
58
  # --- Mimi codec (lazy-loaded, decoder + encoder used for audio I/O) ---
@@ -94,6 +100,13 @@ class WrenForTTS(PreTrainedModel):
94
 
95
  # --- Generation ---
96
 
 
 
 
 
 
 
 
97
  @torch.no_grad()
98
  def generate(
99
  self,
@@ -112,11 +125,11 @@ class WrenForTTS(PreTrainedModel):
112
  Generate Mimi codes (or waveform) from a tokenized prompt.
113
 
114
  Args:
115
- input_ids: [1, L] — text tokens ending with <|audio_sep|>, as produced by WrenProcessor.
116
  ref_codes: optional [k, T_ref] reference codes for voice cloning.
117
  max_audio_frames: hard cap on output length.
118
- min_audio_frames: suppress EOS for this many frames (prevents trivially-short outputs).
119
- eos_bias: additive bias on AUDIO_EOS logit; raise (e.g. 2–6) to reduce hallucinated continuations.
120
  output_audio: if True, return [1, T] waveform; else return [k, n_frames] codes.
121
  """
122
  device = next(self.parameters()).device
@@ -128,67 +141,91 @@ class WrenForTTS(PreTrainedModel):
128
 
129
  prompt_embeds_list = []
130
 
131
- # Optional reference-audio block (voice cloning)
132
  if ref_codes is not None:
133
- if self.config.audio_start_id is None or self.config.audio_end_id is None:
134
- raise ValueError("audio_start_id/audio_end_id missing from config; cannot use ref_codes")
135
  ref_codes = ref_codes.to(device)
136
- start_t = torch.tensor([[self.config.audio_start_id]], dtype=torch.long, device=device)
 
137
  prompt_embeds_list.append(embed_tokens(start_t.clamp(0, text_vocab - 1)))
138
 
139
- ref_tokens = ref_codes.T.reshape(-1) # interleaved [T_ref * k]
140
- for pos, code in enumerate(ref_tokens):
141
- cb_idx = pos % self.k
142
- idx = code.clamp(0, self.config.codebook_size - 1).unsqueeze(0)
143
- prompt_embeds_list.append(self.audio_embeds[cb_idx](idx).unsqueeze(0))
 
 
 
 
144
 
145
- end_t = torch.tensor([[self.config.audio_end_id]], dtype=torch.long, device=device)
146
  prompt_embeds_list.append(embed_tokens(end_t.clamp(0, text_vocab - 1)))
147
 
148
- # Text prompt — already terminated by <|audio_sep|> via the processor
149
  ids = input_ids.to(device)
150
  if ids.dim() == 1:
151
  ids = ids.unsqueeze(0)
152
  prompt_embeds_list.append(embed_tokens(ids.clamp(0, text_vocab - 1)))
153
 
154
  prompt_embeds = torch.cat(prompt_embeds_list, dim=1).to(llm_dtype)
155
- out = self.llm.model(inputs_embeds=prompt_embeds, use_cache=True)
156
- hidden = out.last_hidden_state
157
- past_kv = out.past_key_values
158
-
159
- all_codes, audio_pos, n_frames = [], 0, 0
160
- while n_frames < max_audio_frames:
161
- cb_idx = audio_pos % self.k
162
- h = hidden[:, -1:, :]
163
- logits = self.audio_heads[cb_idx](h.float()).squeeze(1)
164
-
165
- if cb_idx == 0:
166
- logits[:, self.AUDIO_EOS] = logits[:, self.AUDIO_EOS] + eos_bias
167
- if n_frames < min_audio_frames:
168
- logits[:, self.AUDIO_EOS] = float("-inf")
169
-
170
- next_code = _sample(logits, temperature, top_k, top_p)
171
-
172
- if cb_idx == 0 and next_code.item() == self.AUDIO_EOS:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  break
174
 
175
- all_codes.append(next_code.item())
176
- audio_pos += 1
177
- if audio_pos % self.k == 0:
178
- n_frames += 1
179
-
180
- embed_idx = next_code.clamp(0, self.config.codebook_size - 1)
181
- emb = self.audio_embeds[cb_idx](embed_idx.unsqueeze(0)).to(hidden.dtype)
182
- out = self.llm.model(inputs_embeds=emb, past_key_values=past_kv, use_cache=True)
183
  hidden = out.last_hidden_state
184
  past_kv = out.past_key_values
185
 
186
- complete = (len(all_codes) // self.k) * self.k
187
- if complete == 0:
 
 
 
188
  codes = torch.zeros(self.k, 0, dtype=torch.long)
189
  else:
190
- codes = torch.tensor(all_codes[:complete], dtype=torch.long)
191
- codes = codes.reshape(complete // self.k, self.k).T # [k, n_frames]
 
192
 
193
  if output_audio:
194
  return self.decode_audio(codes)
 
3
 
4
  Designed for use with `AutoModel.from_pretrained(..., trust_remote_code=True)`.
5
  Self-contained: no imports from a `src/` folder.
6
+
7
+ Sequence layout (MusicGen-style delay pattern):
8
+
9
+ [ text tokens... | <audio_start> | tgt_delay_steps ]
10
+ + optional [ <reference_start> | ref_delay_steps | <reference_end> ] prefix
11
+
12
+ At each audio step s the model sees the sum of k per-codebook input embeddings
13
+ (scaled by 1/sqrt(k)) and predicts k tokens via k parallel heads. Codebook q at
14
+ frame f lives at step s = f + q. AUDIO_EOS (=codebook_size) is cb0's stop class;
15
+ cb0 emits it at step T (one past the last real frame).
16
  """
17
 
18
+ import math
19
  from typing import Optional
20
 
21
  import torch
 
29
 
30
 
31
  class WrenForTTS(PreTrainedModel):
 
 
 
 
 
 
 
 
 
32
  config_class = WrenConfig
33
  base_model_prefix = "wren"
34
 
 
36
  super().__init__(config)
37
  self.k = config.k_codebooks
38
  self.AUDIO_EOS = config.codebook_size
39
+ self.AUDIO_PAD = config.codebook_size
40
 
41
  # Build backbone from its config only. Pretrained weights for the backbone
42
  # are included in our own state_dict, so no need to re-download here.
 
 
43
  llm_cfg = AutoConfig.from_pretrained(config.llm_name)
44
  llm_cfg.vocab_size = config.vocab_size
45
  self.llm = AutoModelForCausalLM.from_config(llm_cfg)
46
 
47
  hidden = self.llm.config.hidden_size
48
+
49
+ # Input tables: codebook_size + 1 (extra row at index codebook_size = AUDIO_PAD).
50
+ # Output heads: cb0 has codebook_size + 1 (extra class = AUDIO_EOS); cb1..cb_{k-1}
51
+ # have codebook_size (no PAD/EOS output — PAD positions have -100 labels in training).
52
  self.audio_embeds = nn.ModuleList([
53
+ nn.Embedding(config.codebook_size + 1, hidden)
54
  for _ in range(self.k)
55
  ])
56
  self.audio_heads = nn.ModuleList([
 
58
  for i in range(self.k)
59
  ])
60
 
61
+ self.embed_scale = 1.0 / math.sqrt(self.k)
62
  self._mimi = None # lazy-loaded on first use
63
 
64
  # --- Mimi codec (lazy-loaded, decoder + encoder used for audio I/O) ---
 
100
 
101
  # --- Generation ---
102
 
103
+ def _audio_embed_step(self, codes_step: torch.LongTensor) -> torch.Tensor:
104
+ """Summed per-codebook embedding for one audio step. codes_step: [1, k]. Returns [1, 1, H]."""
105
+ acc = self.audio_embeds[0](codes_step[:, 0:1])
106
+ for q in range(1, self.k):
107
+ acc = acc + self.audio_embeds[q](codes_step[:, q:q + 1])
108
+ return acc * self.embed_scale
109
+
110
  @torch.no_grad()
111
  def generate(
112
  self,
 
125
  Generate Mimi codes (or waveform) from a tokenized prompt.
126
 
127
  Args:
128
+ input_ids: [1, L] — text tokens ending with <|audio_start|>, as produced by WrenProcessor.
129
  ref_codes: optional [k, T_ref] reference codes for voice cloning.
130
  max_audio_frames: hard cap on output length.
131
+ min_audio_frames: suppress EOS for this many frames.
132
+ eos_bias: additive bias on cb0's AUDIO_EOS logit; raise (2–6) to reduce tail hallucination.
133
  output_audio: if True, return [1, T] waveform; else return [k, n_frames] codes.
134
  """
135
  device = next(self.parameters()).device
 
141
 
142
  prompt_embeds_list = []
143
 
144
+ # Optional reference block: <reference_start> ref_delayed <reference_end>
145
  if ref_codes is not None:
146
+ if self.config.reference_start_id is None or self.config.reference_end_id is None:
147
+ raise ValueError("reference_start_id/reference_end_id missing from config; cannot use ref_codes")
148
  ref_codes = ref_codes.to(device)
149
+
150
+ start_t = torch.tensor([[self.config.reference_start_id]], dtype=torch.long, device=device)
151
  prompt_embeds_list.append(embed_tokens(start_t.clamp(0, text_vocab - 1)))
152
 
153
+ T_ref = ref_codes.shape[1]
154
+ L_ref = T_ref + self.k - 1
155
+ ref_delayed = torch.full((self.k, L_ref), self.AUDIO_PAD, dtype=torch.long, device=device)
156
+ for q in range(self.k):
157
+ ref_delayed[q, q:q + T_ref] = ref_codes[q]
158
+
159
+ for s in range(L_ref):
160
+ codes_step = ref_delayed[:, s:s + 1].T.contiguous() # [1, k]
161
+ prompt_embeds_list.append(self._audio_embed_step(codes_step))
162
 
163
+ end_t = torch.tensor([[self.config.reference_end_id]], dtype=torch.long, device=device)
164
  prompt_embeds_list.append(embed_tokens(end_t.clamp(0, text_vocab - 1)))
165
 
166
+ # Text prompt — already terminated by <|audio_start|> via the processor
167
  ids = input_ids.to(device)
168
  if ids.dim() == 1:
169
  ids = ids.unsqueeze(0)
170
  prompt_embeds_list.append(embed_tokens(ids.clamp(0, text_vocab - 1)))
171
 
172
  prompt_embeds = torch.cat(prompt_embeds_list, dim=1).to(llm_dtype)
173
+ out = self.llm.model(inputs_embeds=prompt_embeds, use_cache=True)
174
+ hidden = out.last_hidden_state
175
+ past_kv = out.past_key_values
176
+
177
+ # Delay-pattern autoregressive loop
178
+ outputs: list = [[] for _ in range(self.k)]
179
+ eos_step: Optional[int] = None
180
+ max_steps = max_audio_frames + self.k - 1
181
+
182
+ for step in range(max_steps):
183
+ h = hidden[:, -1:, :]
184
+ logits_per_cb = [self.audio_heads[q](h.float()).squeeze(1) for q in range(self.k)]
185
+
186
+ logits_per_cb[0][:, self.AUDIO_EOS] = logits_per_cb[0][:, self.AUDIO_EOS] + eos_bias
187
+ if step < min_audio_frames:
188
+ logits_per_cb[0][:, self.AUDIO_EOS] = float("-inf")
189
+
190
+ next_codes = torch.empty(self.k, dtype=torch.long, device=device)
191
+ for q in range(self.k):
192
+ if step < q:
193
+ next_codes[q] = self.AUDIO_PAD
194
+ continue
195
+ if q == 0 and eos_step is not None:
196
+ next_codes[q] = self.AUDIO_PAD
197
+ continue
198
+ if q > 0 and eos_step is not None and (step - q) >= eos_step:
199
+ next_codes[q] = self.AUDIO_PAD
200
+ continue
201
+ sampled = _sample(logits_per_cb[q], temperature, top_k, top_p)
202
+ if q == 0 and sampled.item() == self.AUDIO_EOS:
203
+ eos_step = step
204
+ next_codes[q] = self.AUDIO_PAD
205
+ else:
206
+ next_codes[q] = sampled
207
+
208
+ for q in range(self.k):
209
+ outputs[q].append(next_codes[q].item())
210
+
211
+ if eos_step is not None and step >= eos_step + self.k - 1:
212
  break
213
 
214
+ next_embed = self._audio_embed_step(next_codes.unsqueeze(0)).to(hidden.dtype)
215
+ out = self.llm.model(inputs_embeds=next_embed, past_key_values=past_kv, use_cache=True)
 
 
 
 
 
 
216
  hidden = out.last_hidden_state
217
  past_kv = out.past_key_values
218
 
219
+ # Un-delay into [k, T]
220
+ T = eos_step if eos_step is not None else max_audio_frames
221
+ max_available = min(len(outputs[q]) - q for q in range(self.k))
222
+ T = min(T, max_available)
223
+ if T <= 0:
224
  codes = torch.zeros(self.k, 0, dtype=torch.long)
225
  else:
226
+ codes = torch.empty(self.k, T, dtype=torch.long)
227
+ for q in range(self.k):
228
+ codes[q] = torch.tensor(outputs[q][q:q + T], dtype=torch.long)
229
 
230
  if output_audio:
231
  return self.decode_audio(codes)
processing_wren.py CHANGED
@@ -1,8 +1,10 @@
1
  """
2
  Wren processor: text tokenization + audio saving.
3
 
4
- Text is lowercased to match training distribution. The `<|audio_sep|>` separator is
5
- always appended so `model.generate(**processor(text))` "just works".
 
 
6
  """
7
 
8
  from typing import List, Union
@@ -17,9 +19,9 @@ class WrenProcessor(ProcessorMixin):
17
 
18
  def __init__(self, tokenizer):
19
  super().__init__(tokenizer=tokenizer)
20
- self.audio_sep_id = tokenizer.convert_tokens_to_ids("<|audio_sep|>")
21
- self.audio_start_id = tokenizer.convert_tokens_to_ids("<|audio_start|>")
22
- self.audio_end_id = tokenizer.convert_tokens_to_ids("<|audio_end|>")
23
 
24
  def __call__(
25
  self,
@@ -27,9 +29,6 @@ class WrenProcessor(ProcessorMixin):
27
  return_tensors: str = "pt",
28
  **kwargs,
29
  ):
30
- # Training data was lowercased; match that distribution.
31
- text = text.lower() if isinstance(text, str) else [t.lower() for t in text]
32
-
33
  enc = self.tokenizer(
34
  text,
35
  add_special_tokens = False,
@@ -40,10 +39,10 @@ class WrenProcessor(ProcessorMixin):
40
  if ids.dim() == 1:
41
  ids = ids.unsqueeze(0)
42
 
43
- # Append <|audio_sep|> as the final prompt token
44
  sep = torch.full(
45
  (ids.shape[0], 1),
46
- self.audio_sep_id,
47
  dtype=ids.dtype,
48
  device=ids.device,
49
  )
 
1
  """
2
  Wren processor: text tokenization + audio saving.
3
 
4
+ Text casing is preserved as-is. Pass text naturally ("Hello, World!") the model
5
+ is trained on mixed-case data (LJSpeech mixed-case, LibriTTS with punctuation).
6
+ The `<|audio_start|>` separator is always appended so `model.generate(**processor(text))`
7
+ "just works".
8
  """
9
 
10
  from typing import List, Union
 
19
 
20
  def __init__(self, tokenizer):
21
  super().__init__(tokenizer=tokenizer)
22
+ self.audio_start_id = tokenizer.convert_tokens_to_ids("<|audio_start|>")
23
+ self.reference_start_id = tokenizer.convert_tokens_to_ids("<|reference_start|>")
24
+ self.reference_end_id = tokenizer.convert_tokens_to_ids("<|reference_end|>")
25
 
26
  def __call__(
27
  self,
 
29
  return_tensors: str = "pt",
30
  **kwargs,
31
  ):
 
 
 
32
  enc = self.tokenizer(
33
  text,
34
  add_special_tokens = False,
 
39
  if ids.dim() == 1:
40
  ids = ids.unsqueeze(0)
41
 
42
+ # Append <|audio_start|> as the final prompt token
43
  sep = torch.full(
44
  (ids.shape[0], 1),
45
+ self.audio_start_id,
46
  dtype=ids.dtype,
47
  device=ids.device,
48
  )
special_tokens_map.json CHANGED
@@ -1,28 +1,21 @@
1
  {
2
  "additional_special_tokens": [
3
  {
4
- "content": "<|audio_sep|>",
5
- "lstrip": false,
6
- "normalized": false,
7
- "rstrip": false,
8
- "single_word": false
9
- },
10
- {
11
- "content": "<|audio_eos|>",
12
  "lstrip": false,
13
  "normalized": false,
14
  "rstrip": false,
15
  "single_word": false
16
  },
17
  {
18
- "content": "<|audio_start|>",
19
  "lstrip": false,
20
  "normalized": false,
21
  "rstrip": false,
22
  "single_word": false
23
  },
24
  {
25
- "content": "<|audio_end|>",
26
  "lstrip": false,
27
  "normalized": false,
28
  "rstrip": false,
 
1
  {
2
  "additional_special_tokens": [
3
  {
4
+ "content": "<|audio_start|>",
 
 
 
 
 
 
 
5
  "lstrip": false,
6
  "normalized": false,
7
  "rstrip": false,
8
  "single_word": false
9
  },
10
  {
11
+ "content": "<|reference_start|>",
12
  "lstrip": false,
13
  "normalized": false,
14
  "rstrip": false,
15
  "single_word": false
16
  },
17
  {
18
+ "content": "<|reference_end|>",
19
  "lstrip": false,
20
  "normalized": false,
21
  "rstrip": false,
tokenizer.json CHANGED
@@ -158,7 +158,7 @@
158
  },
159
  {
160
  "id": 49152,
161
- "content": "<|audio_sep|>",
162
  "single_word": false,
163
  "lstrip": false,
164
  "rstrip": false,
@@ -167,7 +167,7 @@
167
  },
168
  {
169
  "id": 49153,
170
- "content": "<|audio_eos|>",
171
  "single_word": false,
172
  "lstrip": false,
173
  "rstrip": false,
@@ -176,16 +176,7 @@
176
  },
177
  {
178
  "id": 49154,
179
- "content": "<|audio_start|>",
180
- "single_word": false,
181
- "lstrip": false,
182
- "rstrip": false,
183
- "normalized": false,
184
- "special": true
185
- },
186
- {
187
- "id": 49155,
188
- "content": "<|audio_end|>",
189
  "single_word": false,
190
  "lstrip": false,
191
  "rstrip": false,
 
158
  },
159
  {
160
  "id": 49152,
161
+ "content": "<|audio_start|>",
162
  "single_word": false,
163
  "lstrip": false,
164
  "rstrip": false,
 
167
  },
168
  {
169
  "id": 49153,
170
+ "content": "<|reference_start|>",
171
  "single_word": false,
172
  "lstrip": false,
173
  "rstrip": false,
 
176
  },
177
  {
178
  "id": 49154,
179
+ "content": "<|reference_end|>",
 
 
 
 
 
 
 
 
 
180
  "single_word": false,
181
  "lstrip": false,
182
  "rstrip": false,
tokenizer_config.json CHANGED
@@ -138,7 +138,7 @@
138
  "special": true
139
  },
140
  "49152": {
141
- "content": "<|audio_sep|>",
142
  "lstrip": false,
143
  "normalized": false,
144
  "rstrip": false,
@@ -146,7 +146,7 @@
146
  "special": true
147
  },
148
  "49153": {
149
- "content": "<|audio_eos|>",
150
  "lstrip": false,
151
  "normalized": false,
152
  "rstrip": false,
@@ -154,15 +154,7 @@
154
  "special": true
155
  },
156
  "49154": {
157
- "content": "<|audio_start|>",
158
- "lstrip": false,
159
- "normalized": false,
160
- "rstrip": false,
161
- "single_word": false,
162
- "special": true
163
- },
164
- "49155": {
165
- "content": "<|audio_end|>",
166
  "lstrip": false,
167
  "normalized": false,
168
  "rstrip": false,
@@ -171,10 +163,9 @@
171
  }
172
  },
173
  "additional_special_tokens": [
174
- "<|audio_sep|>",
175
- "<|audio_eos|>",
176
  "<|audio_start|>",
177
- "<|audio_end|>"
 
178
  ],
179
  "bos_token": "<|endoftext|>",
180
  "clean_up_tokenization_spaces": false,
 
138
  "special": true
139
  },
140
  "49152": {
141
+ "content": "<|audio_start|>",
142
  "lstrip": false,
143
  "normalized": false,
144
  "rstrip": false,
 
146
  "special": true
147
  },
148
  "49153": {
149
+ "content": "<|reference_start|>",
150
  "lstrip": false,
151
  "normalized": false,
152
  "rstrip": false,
 
154
  "special": true
155
  },
156
  "49154": {
157
+ "content": "<|reference_end|>",
 
 
 
 
 
 
 
 
158
  "lstrip": false,
159
  "normalized": false,
160
  "rstrip": false,
 
163
  }
164
  },
165
  "additional_special_tokens": [
 
 
166
  "<|audio_start|>",
167
+ "<|reference_start|>",
168
+ "<|reference_end|>"
169
  ],
170
  "bos_token": "<|endoftext|>",
171
  "clean_up_tokenization_spaces": false,