alefiury commited on
Commit
49d9287
·
verified ·
1 Parent(s): c008a8b

Upload G-DFlowTTS model

Browse files
README.md ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-nc-4.0
3
+ library_name: transformers
4
+ language:
5
+ - en
6
+ pipeline_tag: text-to-speech
7
+ datasets:
8
+ - neuphonic/emilia-yodas-english-neucodec
9
+ tags:
10
+ - audio
11
+ - speech
12
+ - text-to-speech
13
+ - tts
14
+ - zero-shot
15
+ - voice-cloning
16
+ - discrete-flow-matching
17
+ - neucodec
18
+ ---
19
+
20
+ # Mask, Sample, Revise: A Revisable CTMC Inference Stack for Guided Discrete Flow Matching Text-to-Speech
21
+
22
+ [![arXiv](https://img.shields.io/badge/arXiv-2606.13989-b31b1b.svg?logo=arxiv)](https://arxiv.org/abs/2606.13989) [![GitHub](https://img.shields.io/badge/GitHub-G--DFlowTTS-181717.svg?logo=github)](https://github.com/alefiury/G-DFlowTTS) [![SLT 2026](https://img.shields.io/badge/IEEE%20SLT-2026-00629B.svg)](https://attend.ieee.org/slt-2026/)
23
+
24
+ Accepted at the [IEEE Spoken Language Technology Workshop (SLT 2026)](https://attend.ieee.org/slt-2026/).
25
+
26
+ **G-DFlowTTS (Guided Discrete Flow Matching TTS)** is a zero-shot,
27
+ alignment-free text-to-speech model based on **Discrete Flow Matching (DFM)**.
28
+ A DiT with adaptive layer norm predicts
29
+ [NeuCodec](https://huggingface.co/neuphonic/neucodec) audio codes conditioned
30
+ on GPT-2 text tokens, and a Continuous-Time Markov Chain (CTMC) sampler infills
31
+ them in parallel after an acoustic prompt. This checkpoint was trained on
32
+ **NeuCodec Emilia-YODAS (English)**.
33
+
34
+ In the paper we propose **Mask, Sample, Revise**, an inference-time CTMC stack
35
+ for DFM-TTS that requires no post-hoc fine-tuning:
36
+
37
+ - **SC-ReMask (Schedule-Constrained CTMC Remasking)**: a new remasking strategy
38
+ for Discrete Flow Matching. It adapts the remasking formulation of
39
+ [ReMDM](https://openreview.net/forum?id=IJryQAOy0p), originally designed for
40
+ masked discrete diffusion, to the CTMC formulation of DFM. The two
41
+ formulations are not directly compatible, so remasking is re-derived as an
42
+ explicit token-to-mask CTMC transition whose rate is constrained by the
43
+ probability-path schedule and added to the tau-leaping hazard. Generated
44
+ tokens can return to the mask state and be revised, which yields good
45
+ results with **fewer inference steps**.
46
+ - **Discrete guidance**: predictor-free guidance (PFG) mixes conditional and
47
+ unconditional CTMC rates to strengthen text conditioning.
48
+ - **Prompt-matched conditional coupling**: training paths that match the
49
+ prompted infilling task.
50
+
51
+ On LibriSpeech test-clean at 32 sampling steps, the full stack reduces WER
52
+ from 75.44% (unguided baseline) to 8.39%, outperforming unguided and
53
+ guidance-only samplers that use substantially more steps.
54
+
55
+ ## Model
56
+
57
+ | | |
58
+ |---|---|
59
+ | **Architecture** | DiT (adaLN), 12 blocks, hidden size 768, 12 heads |
60
+ | **Parameters** | 233M |
61
+ | **Audio tokens** | [`neuphonic/neucodec`](https://huggingface.co/neuphonic/neucodec) (50 Hz, 65,536 codes) |
62
+ | **Text tokenizer** | [`openai-community/gpt2`](https://huggingface.co/openai-community/gpt2) |
63
+ | **Source distribution** | Masked |
64
+ | **Scheduler** | Polynomial (n = 1) |
65
+ | **Loss** | Cross-entropy |
66
+ | **Text dropout** | 0.1 |
67
+ | **Input sampling rate** | 16,000 Hz (reference audio) |
68
+ | **Output sampling rate** | 24,000 Hz |
69
+
70
+ ## Training Dataset
71
+
72
+ | **Dataset** | **Hours** | **Utterances** | **Language** | **License** |
73
+ |---|---|---|---|---|
74
+ | [NeuCodec Emilia-YODAS (English)](https://huggingface.co/datasets/neuphonic/emilia-yodas-english-neucodec) | > 78k | > 30M | English | CC BY 4.0 |
75
+
76
+ The dataset contains the English subset of
77
+ [Emilia-YODAS](https://huggingface.co/datasets/amphion/Emilia-YODAS) pre-encoded with NeuCodec.
78
+
79
+ ## Usage
80
+
81
+ Load the model directly from the Hugging Face Hub with Transformers. No clone
82
+ or manual snapshot download is required.
83
+
84
+ ```python
85
+ import soundfile as sf
86
+ from transformers import AutoModel
87
+
88
+ model = AutoModel.from_pretrained(
89
+ "alefiury/G-DFlowTTS-NeuCodec-Emilia-YODAS",
90
+ trust_remote_code=True,
91
+ ).to("cuda")
92
+
93
+ ref_audio, sr = sf.read("reference.wav")
94
+
95
+ wav = model.synthesize(
96
+ text="I just received wonderful news about the promotion I have been waiting for!",
97
+ ref_audio=ref_audio,
98
+ ref_sampling_rate=sr,
99
+ ref_text="Kids are talking by the door.",
100
+ steps=32,
101
+ # Predictor-free guidance
102
+ use_pfg=True,
103
+ gamma=1.5,
104
+ # SC-ReMask: schedule-constrained CTMC remasking
105
+ use_sc_remask=True,
106
+ sc_remask_eta_rescale=0.5,
107
+ sc_remask_eta_cap=0.5,
108
+ sc_remask_tswitch=0.0,
109
+ seed=0,
110
+ )
111
+
112
+ sf.write("output.wav", wav.numpy(), model.config.sampling_rate)
113
+ ```
114
+
115
+ The settings above are the best configuration reported in the paper
116
+ (Mask, Sample, Revise): 32 sampling steps, PFG with γ = 1.5, and always-on
117
+ SC-ReMask (t_switch = 0, η_rescale = η_cap = 0.5).
118
+
119
+ `ref_text` must be the exact transcription of the reference audio. Reference
120
+ audio is converted to mono and resampled to **16,000 Hz** (resampling requires
121
+ `torchaudio`). The output length is estimated from the reference speaking rate;
122
+ use `speed` to adjust it or `duration` to set it explicitly in 50 Hz frames.
123
+
124
+ `trust_remote_code=True` is required because the G-DFlowTTS architecture and
125
+ sampler are shipped with this model repository. NeuCodec is loaded
126
+ automatically from its original Hugging Face repository.
127
+
128
+ ### Sampling options
129
+
130
+ Extra keyword arguments of `synthesize` are forwarded to `model.sample_codes`:
131
+
132
+ | Argument | Default | Description |
133
+ |---|---|---|
134
+ | `steps` | 128 | Number of CTMC sampling steps |
135
+ | `speed` | 1.0 | Speaking rate used by the length heuristic |
136
+ | `duration` | `None` | Number of 50 Hz frames to generate (overrides `speed`) |
137
+ | `x1_temp`, `temp_schedule` | 1.0, `"dfm36"` | Sampling temperature and its schedule (`"dfm36"` or `"constant"`) |
138
+ | `use_pfg`, `gamma` | `False`, 1.5 | Prediction-free guidance |
139
+ | `use_tsr`, `tsr_k`, `tsr_sigma` | `False`, 1.0, 0.1 | Temporal score rescaling |
140
+ | `use_sc_remask` | `False` | SC-ReMask: schedule-constrained CTMC remasking (token-to-mask transitions that make generated tokens revisable) |
141
+ | `sc_remask_eta_rescale`, `sc_remask_eta_cap`, `sc_remask_tswitch` | 0.5, 0.5, 0.0 | SC-ReMask schedule: σ = η_rescale · min(η_cap, σ_max), disabled for t < t_switch |
142
+ | `sc_remask_use_conf`, `sc_remask_conf_threshold`, `sc_remask_beta`, `sc_remask_strength` | `False`, 0.35, 2.0, 1.0 | Optional confidence weighting: remask low-confidence tokens more |
143
+
144
+ The model was trained with text dropout, so prediction-free guidance (`use_pfg=True`) is supported.
145
+
146
+ The raw forward pass returns audio-code logits:
147
+ `model(x_t, text_ids, time, drop_text=False, audio_att_mask=None)`.
148
+
149
+ ## License
150
+
151
+ Released under the `cc-by-nc-4.0` license.
152
+ The training data ([`neuphonic/emilia-yodas-english-neucodec`](https://huggingface.co/datasets/neuphonic/emilia-yodas-english-neucodec)) is CC BY 4.0, and [NeuCodec](https://huggingface.co/neuphonic/neucodec) remains subject to its own license.
config.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "GDFlowTTSModel"
4
+ ],
5
+ "audio_add_token": 2,
6
+ "audio_eos_token": 65536,
7
+ "audio_mask_token": 65537,
8
+ "audio_vocab_size": 65536,
9
+ "auto_map": {
10
+ "AutoConfig": "configuration_gdflowtts.GDFlowTTSConfig",
11
+ "AutoModel": "modeling_gdflowtts.GDFlowTTSModel"
12
+ },
13
+ "codec_input_sampling_rate": 16000,
14
+ "codec_name": "neuphonic/neucodec",
15
+ "cond_dim": 128,
16
+ "cond_drop_prob": 0.1,
17
+ "dropout": 0.1,
18
+ "dtype": "float32",
19
+ "frequency_embedding_size": 256,
20
+ "hidden_size": 768,
21
+ "mlp_ratio": 4,
22
+ "model_type": "gdflowtts",
23
+ "n_blocks": 12,
24
+ "n_heads": 12,
25
+ "rotary_base": 10000,
26
+ "sampling_rate": 24000,
27
+ "scheduler_exponent": 1.0,
28
+ "text_add_token": 0,
29
+ "text_filler_token": 50256,
30
+ "text_vocab_size": 50257,
31
+ "transformers_version": "5.19.0"
32
+ }
configuration_gdflowtts.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+
4
+ class GDFlowTTSConfig(PretrainedConfig):
5
+ """Configuration for the G-DFlowTTS discrete flow matching TTS model.
6
+
7
+ The model predicts NeuCodec audio codes (50 Hz) conditioned on GPT-2 text
8
+ tokens, using a masked source distribution and a polynomial convex
9
+ scheduler (alpha_t = t ** scheduler_exponent).
10
+ """
11
+
12
+ model_type = "gdflowtts"
13
+
14
+ def __init__(
15
+ self,
16
+ audio_vocab_size: int = 65536,
17
+ text_vocab_size: int = 50257,
18
+ hidden_size: int = 768,
19
+ cond_dim: int = 128,
20
+ n_heads: int = 12,
21
+ n_blocks: int = 12,
22
+ mlp_ratio: int = 4,
23
+ dropout: float = 0.1,
24
+ audio_add_token: int = 2,
25
+ text_add_token: int = 0,
26
+ text_filler_token: int = 50256,
27
+ audio_eos_token: int = 65536,
28
+ audio_mask_token: int = 65537,
29
+ rotary_base: int = 10_000,
30
+ frequency_embedding_size: int = 256,
31
+ scheduler_exponent: float = 1.0,
32
+ cond_drop_prob: float = 0.0,
33
+ codec_name: str = "neuphonic/neucodec",
34
+ codec_input_sampling_rate: int = 16_000,
35
+ sampling_rate: int = 24_000,
36
+ **kwargs,
37
+ ):
38
+ self.audio_vocab_size = audio_vocab_size
39
+ self.text_vocab_size = text_vocab_size
40
+ self.hidden_size = hidden_size
41
+ self.cond_dim = cond_dim
42
+ self.n_heads = n_heads
43
+ self.n_blocks = n_blocks
44
+ self.mlp_ratio = mlp_ratio
45
+ self.dropout = dropout
46
+ self.audio_add_token = audio_add_token
47
+ self.text_add_token = text_add_token
48
+ self.text_filler_token = text_filler_token
49
+ self.audio_eos_token = audio_eos_token
50
+ self.audio_mask_token = audio_mask_token
51
+ self.rotary_base = rotary_base
52
+ self.frequency_embedding_size = frequency_embedding_size
53
+ self.scheduler_exponent = scheduler_exponent
54
+ self.cond_drop_prob = cond_drop_prob
55
+ self.codec_name = codec_name
56
+ self.codec_input_sampling_rate = codec_input_sampling_rate
57
+ self.sampling_rate = sampling_rate
58
+ super().__init__(**kwargs)
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:46a7b6e6ad0ce1004c4b88289674ca80ec31994fdd7d4dac1bf1aec137ad0f19
3
+ size 931576432
modeling_gdflowtts.py ADDED
@@ -0,0 +1,513 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """G-DFlowTTS for 🤗 Transformers (``trust_remote_code=True``).
2
+
3
+ Self-contained port of ``modules/gdflowtts/model.py`` plus the CTMC sampler of
4
+ ``infer_simplified.py``. Only ``torch`` and ``transformers`` are required;
5
+ ``torchaudio`` is used only to resample reference audio that is not 16 kHz.
6
+ """
7
+
8
+ import math
9
+ from typing import Optional, Union
10
+
11
+ import numpy as np
12
+ import torch
13
+ import torch.nn.functional as F
14
+ from torch import nn, Tensor
15
+ from transformers import AutoFeatureExtractor, AutoModel, AutoTokenizer, PreTrainedModel
16
+
17
+ from .configuration_gdflowtts import GDFlowTTSConfig
18
+
19
+
20
+ def modulate(x: Tensor, shift: Tensor, scale: Tensor) -> Tensor:
21
+ return x * (1 + scale) + shift
22
+
23
+
24
+ def rotate_half(x: Tensor) -> Tensor:
25
+ x1, x2 = x[..., : x.shape[-1] // 2], x[..., x.shape[-1] // 2 :]
26
+ return torch.cat((-x2, x1), dim=-1)
27
+
28
+
29
+ class LayerNorm(nn.Module):
30
+ """Bias-free LayerNorm computed in float32."""
31
+
32
+ def __init__(self, dim: int):
33
+ super().__init__()
34
+ self.weight = nn.Parameter(torch.ones([dim]))
35
+ self.dim = dim
36
+
37
+ def forward(self, x: Tensor) -> Tensor:
38
+ out = F.layer_norm(x.float(), [self.dim]) * self.weight.float()[None, None, :]
39
+ return out.to(x.dtype)
40
+
41
+
42
+ class TimestepEmbedder(nn.Module):
43
+ def __init__(self, hidden_size: int, frequency_embedding_size: int = 256):
44
+ super().__init__()
45
+ self.mlp = nn.Sequential(
46
+ nn.Linear(frequency_embedding_size, hidden_size, bias=True),
47
+ nn.SiLU(),
48
+ nn.Linear(hidden_size, hidden_size, bias=True),
49
+ )
50
+ self.frequency_embedding_size = frequency_embedding_size
51
+
52
+ @staticmethod
53
+ def timestep_embedding(time: Tensor, dim: int, max_period: int = 10000) -> Tensor:
54
+ half = dim // 2
55
+ freqs = torch.exp(
56
+ -math.log(max_period) * torch.arange(0, half, dtype=torch.float32, device=time.device) / half
57
+ )
58
+ args = time[:, None].float() * freqs[None]
59
+ embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
60
+ if dim % 2:
61
+ embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
62
+ return embedding
63
+
64
+ def forward(self, time: Tensor) -> Tensor:
65
+ t_freq = self.timestep_embedding(time, self.frequency_embedding_size)
66
+ return self.mlp(t_freq.to(self.mlp[0].weight.dtype))
67
+
68
+
69
+ class DDiTBlock(nn.Module):
70
+ def __init__(self, dim: int, n_heads: int, cond_dim: int, mlp_ratio: int = 4, dropout: float = 0.1):
71
+ super().__init__()
72
+ assert dim % n_heads == 0, "dim must be divisible by n_heads"
73
+ self.n_heads = n_heads
74
+ self.head_dim = dim // n_heads
75
+ self.dropout = dropout
76
+
77
+ self.norm1 = LayerNorm(dim)
78
+ self.qw = nn.Linear(dim, dim, bias=False)
79
+ self.kw = nn.Linear(dim, dim, bias=False)
80
+ self.vw = nn.Linear(dim, dim, bias=False)
81
+ self.attn_out = nn.Linear(dim, dim, bias=False)
82
+ self.dropout1 = nn.Dropout(dropout)
83
+
84
+ self.norm2 = LayerNorm(dim)
85
+ self.mlp = nn.Sequential(
86
+ nn.Linear(dim, mlp_ratio * dim, bias=True),
87
+ nn.GELU(approximate="tanh"),
88
+ nn.Linear(mlp_ratio * dim, dim, bias=True),
89
+ )
90
+ self.adaLN_modulation = nn.Linear(cond_dim, 6 * dim, bias=True)
91
+
92
+ def forward(self, x: Tensor, cos: Tensor, sin: Tensor, c: Tensor, att_mask: Tensor) -> Tensor:
93
+ batch_size, seq_len = x.shape[0], x.shape[1]
94
+ att_mask = att_mask[:, None, None, :].to(device=x.device, dtype=torch.bool)
95
+
96
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
97
+ self.adaLN_modulation(c)[:, None].chunk(6, dim=2)
98
+ )
99
+
100
+ x_skip = x
101
+ h = modulate(self.norm1(x), shift_msa, scale_msa)
102
+ q, k, v = (
103
+ proj(h).view(batch_size, seq_len, self.n_heads, self.head_dim)
104
+ for proj in (self.qw, self.kw, self.vw)
105
+ )
106
+
107
+ # Rotary embedding on q/k, computed in float32
108
+ q = (q.float() * cos + rotate_half(q.float()) * sin).to(v.dtype)
109
+ k = (k.float() * cos + rotate_half(k.float()) * sin).to(v.dtype)
110
+
111
+ q, k, v = (t.transpose(1, 2) for t in (q, k, v))
112
+ h = F.scaled_dot_product_attention(q, k, v, attn_mask=att_mask)
113
+ h = h.transpose(1, 2).reshape(batch_size, seq_len, -1)
114
+
115
+ x = x_skip + gate_msa * F.dropout(self.attn_out(h), p=self.dropout, training=self.training)
116
+ h = self.mlp(modulate(self.norm2(x), shift_mlp, scale_mlp))
117
+ x = x + gate_mlp * F.dropout(h, p=self.dropout, training=self.training)
118
+ return x
119
+
120
+
121
+ class DDitFinalLayer(nn.Module):
122
+ def __init__(self, hidden_size: int, out_channels: int, cond_dim: int):
123
+ super().__init__()
124
+ self.norm_final = LayerNorm(hidden_size)
125
+ self.linear = nn.Linear(hidden_size, out_channels)
126
+ self.adaLN_modulation = nn.Linear(cond_dim, 2 * hidden_size, bias=True)
127
+
128
+ def forward(self, x: Tensor, c: Tensor) -> Tensor:
129
+ shift, scale = self.adaLN_modulation(c)[:, None].chunk(2, dim=2)
130
+ return self.linear(modulate(self.norm_final(x), shift, scale))
131
+
132
+
133
+ class GDFlowTTSPreTrainedModel(PreTrainedModel):
134
+ config_class = GDFlowTTSConfig
135
+ base_model_prefix = "gdflowtts"
136
+ main_input_name = "x_t"
137
+ _no_split_modules = ["DDiTBlock"]
138
+ _supports_sdpa = True
139
+
140
+ def _init_weights(self, module):
141
+ if isinstance(module, nn.Linear):
142
+ nn.init.normal_(module.weight, std=0.02)
143
+ if module.bias is not None:
144
+ nn.init.zeros_(module.bias)
145
+ elif isinstance(module, nn.Embedding):
146
+ nn.init.normal_(module.weight, std=0.02)
147
+ elif isinstance(module, LayerNorm):
148
+ nn.init.ones_(module.weight)
149
+
150
+
151
+ class GDFlowTTSModel(GDFlowTTSPreTrainedModel):
152
+ def __init__(self, config: GDFlowTTSConfig):
153
+ super().__init__(config)
154
+ self.total_audio_vocab_size = config.audio_vocab_size + config.audio_add_token
155
+ total_text_vocab_size = config.text_vocab_size + config.text_add_token
156
+
157
+ self.audio_embed = nn.Embedding(self.total_audio_vocab_size, config.hidden_size)
158
+ self.text_embed = nn.Embedding(total_text_vocab_size, config.hidden_size)
159
+ self.time_embedding = TimestepEmbedder(config.cond_dim, config.frequency_embedding_size)
160
+ # Audio and text embeddings are concatenated, then projected back
161
+ self.input_proj = nn.Linear(config.hidden_size * 2, config.hidden_size)
162
+
163
+ self.blocks = nn.ModuleList(
164
+ [
165
+ DDiTBlock(config.hidden_size, config.n_heads, config.cond_dim, config.mlp_ratio, config.dropout)
166
+ for _ in range(config.n_blocks)
167
+ ]
168
+ )
169
+ self.output_layer = DDitFinalLayer(config.hidden_size, self.total_audio_vocab_size, config.cond_dim)
170
+
171
+ # Lazily loaded helpers; kept out of the module tree so they are never saved
172
+ self.__dict__["_codec"] = None
173
+ self.__dict__["_feature_extractor"] = None
174
+ self.__dict__["_tokenizer"] = None
175
+
176
+ self.post_init()
177
+
178
+ def _rotary_cos_sin(self, seq_len: int, device: torch.device):
179
+ head_dim = self.config.hidden_size // self.config.n_heads
180
+ inv_freq = 1.0 / (
181
+ self.config.rotary_base
182
+ ** (torch.arange(0, head_dim, 2, device=device, dtype=torch.float32) / head_dim)
183
+ )
184
+ t = torch.arange(seq_len, device=device, dtype=torch.float32)
185
+ freqs = torch.outer(t, inv_freq)
186
+ emb = torch.cat((freqs, freqs), dim=-1)
187
+ # [1, seq_len, 1, head_dim] to broadcast over (batch, seq, heads, head_dim)
188
+ return emb.cos()[None, :, None, :], emb.sin()[None, :, None, :]
189
+
190
+ def forward(
191
+ self,
192
+ x_t: Tensor,
193
+ text_ids: Tensor,
194
+ time: Tensor,
195
+ drop_text: bool = False,
196
+ text_att_mask: Optional[Tensor] = None,
197
+ audio_att_mask: Optional[Tensor] = None,
198
+ ) -> Tensor:
199
+ """Returns logits over the audio vocabulary, shape [B, T, audio_vocab_size + audio_add_token]."""
200
+ seq_len = x_t.shape[1]
201
+ audio_emb = self.audio_embed(x_t)
202
+
203
+ if audio_att_mask is None:
204
+ audio_att_mask = torch.ones(x_t.shape, device=x_t.device, dtype=torch.bool)
205
+
206
+ # Text tokens are aligned position-wise with audio: truncate/pad to audio length
207
+ text = text_ids[:, :seq_len]
208
+ text = F.pad(text, (0, seq_len - text.shape[1]), value=self.config.text_filler_token)
209
+ if drop_text:
210
+ text = torch.full_like(text, self.config.text_filler_token)
211
+ text_emb = self.text_embed(text)
212
+
213
+ x = self.input_proj(torch.cat([audio_emb, text_emb], dim=-1))
214
+ c = F.silu(self.time_embedding(time))
215
+ cos, sin = self._rotary_cos_sin(seq_len, x.device)
216
+
217
+ for block in self.blocks:
218
+ x = block(x, cos, sin, c, audio_att_mask)
219
+
220
+ return self.output_layer(x, c)
221
+
222
+ # ------------------------------------------------------------------
223
+ # Codec / tokenizer helpers
224
+ # ------------------------------------------------------------------
225
+
226
+ def load_codec(self, codec_name: Optional[str] = None):
227
+ """Loads NeuCodec (native in Transformers) on the model's device."""
228
+ if self._codec is None:
229
+ name = codec_name or self.config.codec_name
230
+ self.__dict__["_codec"] = AutoModel.from_pretrained(name).to(self.device).eval()
231
+ self.__dict__["_feature_extractor"] = AutoFeatureExtractor.from_pretrained(name)
232
+ return self._codec
233
+
234
+ def load_tokenizer(self, tokenizer_name: Optional[str] = None):
235
+ if self._tokenizer is None:
236
+ # The tokenizer lives in this (already trusted) repo, whose config.json has an
237
+ # auto_map; without trust_remote_code AutoTokenizer would prompt interactively.
238
+ tok = AutoTokenizer.from_pretrained(tokenizer_name or self.config._name_or_path, trust_remote_code=True)
239
+ if tok.pad_token is None:
240
+ tok.pad_token = tok.eos_token
241
+ self.__dict__["_tokenizer"] = tok
242
+ return self._tokenizer
243
+
244
+ @torch.no_grad()
245
+ def encode_audio(self, audio: Union[np.ndarray, Tensor], sampling_rate: int) -> Tensor:
246
+ """Encodes a mono waveform into 1-D NeuCodec codes."""
247
+ codec = self.load_codec()
248
+ audio = torch.as_tensor(audio, dtype=torch.float32).cpu()
249
+ if audio.ndim > 1:
250
+ audio = audio.reshape(-1, audio.shape[-1]).mean(dim=0)
251
+ target_sr = self.config.codec_input_sampling_rate
252
+ if sampling_rate != target_sr:
253
+ try:
254
+ import torchaudio
255
+ except ImportError as e:
256
+ raise ValueError(
257
+ f"Reference audio must be {target_sr} Hz (got {sampling_rate}); "
258
+ "install torchaudio for automatic resampling."
259
+ ) from e
260
+ audio = torchaudio.functional.resample(audio, sampling_rate, target_sr)
261
+
262
+ inputs = self._feature_extractor(
263
+ audio=[audio.numpy()], sampling_rate=target_sr, return_tensors="pt"
264
+ ).to(self.device, codec.dtype)
265
+ return codec.encode(**inputs).audio_codes.long().reshape(-1)
266
+
267
+ @torch.no_grad()
268
+ def decode_audio(self, codes: Tensor) -> Tensor:
269
+ """Decodes 1-D NeuCodec codes into a 24 kHz waveform."""
270
+ codec = self.load_codec()
271
+ return codec.decode(codes.to(self.device).long().view(1, 1, -1)).audio_values.reshape(-1).float().cpu()
272
+
273
+ # ------------------------------------------------------------------
274
+ # Sampling
275
+ # ------------------------------------------------------------------
276
+
277
+ def _scheduler(self, t: float):
278
+ """Polynomial convex scheduler: returns (alpha_t, d alpha_t / dt)."""
279
+ n = float(self.config.scheduler_exponent)
280
+ if t == 0.0 and n < 1.0:
281
+ return 0.0, float("inf")
282
+ return t**n, n * t ** (n - 1.0)
283
+
284
+ @torch.no_grad()
285
+ def sample_codes(
286
+ self,
287
+ text_ids: Tensor,
288
+ ref_codes: Tensor,
289
+ suffix_len: int,
290
+ steps: int = 128,
291
+ text_att_mask: Optional[Tensor] = None,
292
+ x1_temp: float = 1.0,
293
+ temp_schedule: str = "dfm36",
294
+ remask_noise: float = 0.0,
295
+ use_pfg: bool = False,
296
+ gamma: float = 1.5,
297
+ use_tsr: bool = False,
298
+ tsr_k: float = 1.0,
299
+ tsr_sigma: float = 0.1,
300
+ use_sc_remask: bool = False,
301
+ sc_remask_eta_rescale: float = 0.5,
302
+ sc_remask_eta_cap: float = 0.5,
303
+ sc_remask_tswitch: float = 0.0,
304
+ sc_remask_use_conf: bool = False,
305
+ sc_remask_conf_threshold: float = 0.35,
306
+ sc_remask_beta: float = 2.0,
307
+ sc_remask_strength: float = 1.0,
308
+ generator: Optional[torch.Generator] = None,
309
+ ) -> Tensor:
310
+ """Masked-source CTMC sampler (tau-leaping) with optional PFG, TSR and SC-ReMask.
311
+
312
+ Returns the full sequence [1, len(ref_codes) + suffix_len], whose last
313
+ position is pinned to EOS.
314
+ """
315
+ device = self.device
316
+ S = self.total_audio_vocab_size
317
+ mask_id = int(self.config.audio_mask_token)
318
+ eos_id = int(self.config.audio_eos_token)
319
+ eps = 1e-12
320
+
321
+ if use_pfg and float(self.config.cond_drop_prob) <= 0.0:
322
+ raise RuntimeError("use_pfg=True but this checkpoint was trained without text dropout.")
323
+
324
+ ref_codes = ref_codes.to(device).long().reshape(-1)
325
+ text_ids = text_ids.to(device)
326
+ if text_att_mask is not None:
327
+ text_att_mask = text_att_mask.to(device)
328
+ prefix_len = int(ref_codes.numel())
329
+ total_len = prefix_len + int(suffix_len)
330
+
331
+ xt = torch.full((1, total_len), mask_id, device=device, dtype=torch.long)
332
+ xt[:, :prefix_len] = ref_codes
333
+ xt[0, -1] = eos_id
334
+ audio_att_mask = torch.ones_like(xt, dtype=torch.bool)
335
+
336
+ dt = 1.0 / max(1, int(steps))
337
+
338
+ def logits_at(t: Tensor, drop_text: bool) -> Tensor:
339
+ return self(
340
+ x_t=xt,
341
+ text_ids=text_ids,
342
+ time=t,
343
+ drop_text=drop_text,
344
+ text_att_mask=text_att_mask,
345
+ audio_att_mask=audio_att_mask,
346
+ ).float()
347
+
348
+ for k in range(int(steps)):
349
+ t_lin = k * dt
350
+ t = torch.full((1,), t_lin, device=device, dtype=torch.float32)
351
+ alpha_t, dalpha_t = self._scheduler(t_lin)
352
+ alpha_s, _ = self._scheduler(min(1.0, (k + 1) * dt))
353
+ dalpha_t = max(dalpha_t, 1e-6)
354
+ lam = dalpha_t / max(1.0 - alpha_t, 1e-6)
355
+
356
+ if temp_schedule == "dfm36":
357
+ temp = max(1e-3, float(x1_temp) * (1.0 - t_lin) ** 2)
358
+ else:
359
+ temp = max(1e-3, float(x1_temp))
360
+
361
+ # Temporal Score Rescaling
362
+ tsr_ratio = 1.0
363
+ if use_tsr and float(tsr_k) != 1.0:
364
+ a = min(max(alpha_t, 0.0), 1.0)
365
+ num = (1.0 - a) + a * tsr_sigma**2
366
+ den = (1.0 - a) + a * tsr_sigma**2 / float(tsr_k)
367
+ tsr_ratio = max(num / max(den, eps), eps)
368
+
369
+ def probs_from(logits: Tensor) -> Tensor:
370
+ p = torch.softmax(logits * tsr_ratio / temp, dim=-1)
371
+ p[..., mask_id] = 0.0
372
+ return p / p.sum(dim=-1, keepdim=True).clamp_min(eps)
373
+
374
+ probs_c = probs_from(logits_at(t, drop_text=False))
375
+ probs_u = probs_from(logits_at(t, drop_text=True)) if use_pfg else None
376
+
377
+ xt_is_mask = (xt == mask_id).unsqueeze(-1).float()
378
+ base_r = (1.0 + float(remask_noise) * t_lin) * lam
379
+ R_c = xt_is_mask * probs_c * base_r
380
+ R_u = xt_is_mask * probs_u * base_r if use_pfg else None
381
+
382
+ if remask_noise > 0.0:
383
+ to_mask = (1.0 - xt_is_mask) * F.one_hot(torch.tensor(mask_id, device=device), S).float()
384
+ R_c = R_c + to_mask * float(remask_noise)
385
+ if R_u is not None:
386
+ R_u = R_u + to_mask * float(remask_noise)
387
+
388
+ # Prediction-free guidance in log-rate space
389
+ if use_pfg:
390
+ R_mix = torch.exp(gamma * torch.log(R_c + 1e-9) + (1.0 - gamma) * torch.log(R_u + 1e-9))
391
+ else:
392
+ R_mix = R_c
393
+
394
+ # SC-ReMask: schedule-constrained remasking (token -> MASK)
395
+ if use_sc_remask:
396
+ sigma_max = min(1.0, max(1.0 - alpha_s, 0.0) / max(alpha_t, eps))
397
+ sigma = sc_remask_eta_rescale * min(sc_remask_eta_cap, sigma_max)
398
+ sigma = min(max(sigma, 0.0), 1.0 - 1e-6)
399
+ if t_lin < sc_remask_tswitch:
400
+ sigma = 0.0
401
+ if sigma > 0.0:
402
+ r_base = -math.log(max(1e-9, 1.0 - sigma)) / max(1e-12, dt)
403
+ eligible = torch.ones_like(xt, dtype=torch.bool)
404
+ eligible[:, :prefix_len] = False
405
+ eligible[:, -1] = False
406
+ eligible = eligible & (xt != mask_id) & (xt != eos_id)
407
+
408
+ if sc_remask_use_conf:
409
+ if use_pfg:
410
+ logp = gamma * torch.log(probs_c + eps) + (1.0 - gamma) * torch.log(probs_u + eps)
411
+ probs_mix = torch.exp(logp)
412
+ probs_mix = probs_mix / probs_mix.sum(dim=-1, keepdim=True).clamp_min(eps)
413
+ else:
414
+ probs_mix = probs_c
415
+ p_cur = probs_mix.gather(-1, xt[..., None].clamp(0, S - 1)).squeeze(-1)
416
+ thr = float(sc_remask_conf_threshold)
417
+ w = (p_cur < thr).float() * ((thr - p_cur) / max(1e-9, thr)).clamp(0.0, 1.0) ** sc_remask_beta
418
+ w = (w * float(sc_remask_strength)).clamp(0.0, 1.0)
419
+ else:
420
+ w = torch.ones_like(xt, dtype=torch.float32)
421
+
422
+ add = torch.zeros_like(R_mix)
423
+ add[..., mask_id] = (r_base * w).to(R_mix.dtype)
424
+ R_mix = R_mix + add * eligible.unsqueeze(-1).to(R_mix.dtype)
425
+
426
+ # CTMC tau-leap step
427
+ R_off = R_mix.clone()
428
+ R_off.scatter_(-1, xt[..., None], 0.0)
429
+ hazard = R_off.sum(dim=-1)
430
+ p_jump = 1.0 - torch.exp(-dt * hazard)
431
+ do_jump = torch.rand(p_jump.shape, device=device, generator=generator) < p_jump
432
+ do_jump[:, :prefix_len] = False
433
+ do_jump[:, -1] = False
434
+
435
+ if do_jump.any():
436
+ q = R_off / hazard.clamp_min(1e-9).unsqueeze(-1)
437
+ jump_idx = do_jump.view(-1).nonzero(as_tuple=False).squeeze(-1)
438
+ q_jump = q.view(-1, S).index_select(0, jump_idx)
439
+ q_jump = torch.nan_to_num(q_jump, nan=0.0, posinf=0.0, neginf=0.0).clamp_min(0.0)
440
+ q_jump = q_jump / q_jump.sum(dim=-1, keepdim=True).clamp_min(1e-12)
441
+ sampled = torch.multinomial(q_jump, 1, generator=generator).squeeze(-1)
442
+ xt.view(-1)[jump_idx] = sampled
443
+
444
+ xt[0, -1] = eos_id
445
+ xt[:, :prefix_len] = ref_codes
446
+
447
+ # Early stop only when nothing can be remasked anymore
448
+ if remask_noise <= 0.0 and not use_sc_remask and not (xt[:, prefix_len:] == mask_id).any():
449
+ break
450
+
451
+ return xt
452
+
453
+ @torch.no_grad()
454
+ def synthesize(
455
+ self,
456
+ text: str,
457
+ ref_audio: Union[np.ndarray, Tensor],
458
+ ref_text: str,
459
+ ref_sampling_rate: int = 16_000,
460
+ steps: int = 128,
461
+ speed: float = 1.0,
462
+ duration: Optional[int] = None,
463
+ seed: Optional[int] = None,
464
+ **sampling_kwargs,
465
+ ) -> Tensor:
466
+ """Zero-shot TTS: clones the voice of ``ref_audio`` (transcribed as ``ref_text``).
467
+
468
+ Args:
469
+ text: text to synthesize.
470
+ ref_audio: mono reference waveform.
471
+ ref_text: transcription of the reference audio.
472
+ ref_sampling_rate: sampling rate of ``ref_audio``.
473
+ steps: number of CTMC sampling steps.
474
+ speed: speaking rate for the length heuristic (>1 is faster).
475
+ duration: number of codec frames (50 Hz) to generate; overrides ``speed``.
476
+ seed: optional random seed.
477
+ **sampling_kwargs: forwarded to :meth:`sample_codes` (x1_temp, use_pfg, gamma, use_tsr, ...).
478
+
479
+ Returns:
480
+ 1-D float tensor with the waveform at ``config.sampling_rate`` (24 kHz).
481
+ """
482
+ tokenizer = self.load_tokenizer()
483
+ enc = tokenizer(ref_text + ". " + text, return_tensors="pt")
484
+ ref_codes = self.encode_audio(ref_audio, ref_sampling_rate)
485
+
486
+ if duration is None:
487
+ duration = math.floor((ref_codes.numel() / max(1, len(ref_text))) * (len(text) / speed))
488
+ # +1 for the EOS token pinned at the last position
489
+ suffix_len = int(duration) + 1
490
+
491
+ generator = None
492
+ if seed is not None:
493
+ generator = torch.Generator(device=self.device).manual_seed(seed)
494
+
495
+ xt = self.sample_codes(
496
+ text_ids=enc["input_ids"],
497
+ text_att_mask=enc["attention_mask"].bool(),
498
+ ref_codes=ref_codes,
499
+ suffix_len=suffix_len,
500
+ steps=steps,
501
+ generator=generator,
502
+ **sampling_kwargs,
503
+ )
504
+
505
+ gen = xt[0, ref_codes.numel():]
506
+ eos_pos = (gen == self.config.audio_eos_token).nonzero(as_tuple=False)
507
+ if eos_pos.numel() > 0:
508
+ gen = gen[: int(eos_pos[0])]
509
+ gen = gen[gen != self.config.audio_mask_token]
510
+ return self.decode_audio(gen)
511
+
512
+
513
+ __all__ = ["GDFlowTTSPreTrainedModel", "GDFlowTTSModel"]
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "backend": "tokenizers",
4
+ "bos_token": "<|endoftext|>",
5
+ "eos_token": "<|endoftext|>",
6
+ "errors": "replace",
7
+ "is_local": false,
8
+ "local_files_only": false,
9
+ "model_max_length": 1024,
10
+ "pad_token": null,
11
+ "tokenizer_class": "GPT2Tokenizer",
12
+ "unk_token": "<|endoftext|>"
13
+ }