muscriptor 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- muscriptor/__init__.py +7 -0
- muscriptor/__main__.py +3 -0
- muscriptor/events.py +208 -0
- muscriptor/main.py +327 -0
- muscriptor/models/__init__.py +0 -0
- muscriptor/models/lm.py +514 -0
- muscriptor/modules/__init__.py +0 -0
- muscriptor/modules/conditioners.py +321 -0
- muscriptor/modules/mel_spectrogram.py +106 -0
- muscriptor/modules/streaming.py +68 -0
- muscriptor/modules/transformer.py +202 -0
- muscriptor/server.py +267 -0
- muscriptor/tokenizer/__init__.py +3 -0
- muscriptor/tokenizer/mt3.py +231 -0
- muscriptor/tokenizer/notes.py +344 -0
- muscriptor/transcription_model.py +629 -0
- muscriptor/utils/__init__.py +0 -0
- muscriptor/utils/audio.py +112 -0
- muscriptor/utils/auralization.py +145 -0
- muscriptor/utils/download.py +68 -0
- muscriptor/utils/midi.py +25 -0
- muscriptor/utils/resample.py +191 -0
- muscriptor/utils/sampling.py +89 -0
- muscriptor-0.1.0.dist-info/METADATA +336 -0
- muscriptor-0.1.0.dist-info/RECORD +28 -0
- muscriptor-0.1.0.dist-info/WHEEL +4 -0
- muscriptor-0.1.0.dist-info/entry_points.txt +2 -0
- muscriptor-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Minimal conditioners for muscriptor inference.
|
|
2
|
+
|
|
3
|
+
Contains only the classes needed to run the transcription model:
|
|
4
|
+
- ConditioningAttributes, ConditionType, WavCondition
|
|
5
|
+
- MelSpectrogramConditioner (audio → mel → linear projection)
|
|
6
|
+
- ClassConditioner (class index → embedding)
|
|
7
|
+
- ConditioningProvider
|
|
8
|
+
- nullify_all_conditions (for CFG at inference)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from copy import deepcopy
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import NamedTuple, Any
|
|
16
|
+
import torch
|
|
17
|
+
from torch import nn
|
|
18
|
+
from torch.nn import functional as F
|
|
19
|
+
from einops import rearrange
|
|
20
|
+
|
|
21
|
+
from muscriptor.modules.mel_spectrogram import _MelSpectrogram
|
|
22
|
+
from muscriptor.utils.sampling import length_to_mask
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ConditionType = tuple[torch.Tensor, torch.Tensor] # (embedding [B, T, D], mask [B, T])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WavCondition(NamedTuple):
|
|
29
|
+
wav: torch.Tensor
|
|
30
|
+
length: torch.Tensor
|
|
31
|
+
sample_rate: list[int]
|
|
32
|
+
path: list[str | None] = []
|
|
33
|
+
seek_time: list[float | None] = []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ConditioningAttributes:
|
|
38
|
+
text: dict[str, str | None] = field(default_factory=dict)
|
|
39
|
+
wav: dict[str, WavCondition] = field(default_factory=dict)
|
|
40
|
+
joint_embed: dict[str, Any] = field(default_factory=dict)
|
|
41
|
+
symbolic: dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
def __getitem__(self, item):
|
|
44
|
+
return getattr(self, item)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def text_attributes(self):
|
|
48
|
+
return self.text.keys()
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def wav_attributes(self):
|
|
52
|
+
return self.wav.keys()
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def joint_embed_attributes(self):
|
|
56
|
+
return self.joint_embed.keys()
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def symbolic_attributes(self):
|
|
60
|
+
return self.symbolic.keys()
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def attributes(self):
|
|
64
|
+
return {
|
|
65
|
+
"text": self.text_attributes,
|
|
66
|
+
"wav": self.wav_attributes,
|
|
67
|
+
"joint_embed": self.joint_embed_attributes,
|
|
68
|
+
"symbolic": self.symbolic_attributes,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def condition_types(cls) -> list:
|
|
73
|
+
return ["text", "wav", "joint_embed", "symbolic"]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def nullify_wav(cond: WavCondition) -> WavCondition:
|
|
77
|
+
B = cond.wav.shape[0]
|
|
78
|
+
return WavCondition(
|
|
79
|
+
wav=torch.zeros(*cond.wav.shape[:-1], 1, device=cond.wav.device),
|
|
80
|
+
length=torch.zeros(B, dtype=cond.length.dtype, device=cond.wav.device),
|
|
81
|
+
sample_rate=cond.sample_rate,
|
|
82
|
+
path=[None] * B,
|
|
83
|
+
seek_time=[None] * B,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def nullify_all_conditions(
|
|
88
|
+
samples: list[ConditioningAttributes],
|
|
89
|
+
) -> list[ConditioningAttributes]:
|
|
90
|
+
"""Return a copy of ``samples`` with every wav/text condition nulled out.
|
|
91
|
+
|
|
92
|
+
Used to build the unconditional batch for classifier-free guidance.
|
|
93
|
+
"""
|
|
94
|
+
samples = deepcopy(samples)
|
|
95
|
+
for sample in samples:
|
|
96
|
+
for k in list(sample.wav):
|
|
97
|
+
sample.wav[k] = nullify_wav(sample.wav[k])
|
|
98
|
+
for k in list(sample.text):
|
|
99
|
+
sample.text[k] = None
|
|
100
|
+
return samples
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class MelSpectrogramConditioner(nn.Module):
|
|
104
|
+
"""Log mel spectrogram conditioner. Projects mel bins to transformer dim."""
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
output_dim: int,
|
|
109
|
+
device: torch.device | str,
|
|
110
|
+
sample_rate: int,
|
|
111
|
+
n_fft: int = 2048,
|
|
112
|
+
frame_rate: int = 100,
|
|
113
|
+
n_mel_bins: int = 512,
|
|
114
|
+
normalize_audio: bool = False,
|
|
115
|
+
log_scale: bool = True,
|
|
116
|
+
eps: float = 1e-6,
|
|
117
|
+
# unused arg kept for config compatibility
|
|
118
|
+
fine_frame_rate: int = None,
|
|
119
|
+
):
|
|
120
|
+
self.fine_frame_rate_ratio = 1
|
|
121
|
+
if fine_frame_rate is not None:
|
|
122
|
+
assert fine_frame_rate % frame_rate == 0
|
|
123
|
+
self.fine_frame_rate_ratio = fine_frame_rate // frame_rate
|
|
124
|
+
|
|
125
|
+
super().__init__()
|
|
126
|
+
self.dim = n_mel_bins * self.fine_frame_rate_ratio
|
|
127
|
+
self.output_dim = output_dim
|
|
128
|
+
self.output_proj = nn.Linear(self.dim, output_dim)
|
|
129
|
+
self.device = device
|
|
130
|
+
self.sample_rate = sample_rate
|
|
131
|
+
self.frame_rate = frame_rate
|
|
132
|
+
self.normalize_audio = normalize_audio
|
|
133
|
+
self.log_scale = log_scale
|
|
134
|
+
self.eps = eps
|
|
135
|
+
|
|
136
|
+
if self.fine_frame_rate_ratio == 1:
|
|
137
|
+
assert sample_rate % frame_rate == 0
|
|
138
|
+
self.hop_length = sample_rate // frame_rate
|
|
139
|
+
else:
|
|
140
|
+
assert sample_rate % fine_frame_rate == 0
|
|
141
|
+
self.hop_length = sample_rate // fine_frame_rate
|
|
142
|
+
|
|
143
|
+
self.mel_spec_transform = _MelSpectrogram(
|
|
144
|
+
sample_rate=sample_rate,
|
|
145
|
+
n_fft=n_fft,
|
|
146
|
+
hop_length=self.hop_length,
|
|
147
|
+
n_mels=n_mel_bins,
|
|
148
|
+
power=1.0,
|
|
149
|
+
center=True,
|
|
150
|
+
pad_mode="reflect",
|
|
151
|
+
).to(device)
|
|
152
|
+
|
|
153
|
+
def tokenize(self, x: WavCondition) -> WavCondition:
|
|
154
|
+
wav, length, sample_rate, path, seek_time = x
|
|
155
|
+
assert length is not None
|
|
156
|
+
return WavCondition(
|
|
157
|
+
wav.to(self.device), length.to(self.device), sample_rate, path, seek_time
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def _mel_embedding(self, x: WavCondition) -> torch.Tensor:
|
|
161
|
+
if x.wav.shape[-1] == 1:
|
|
162
|
+
return torch.zeros(x.wav.shape[0], 1, self.dim, device=self.device)
|
|
163
|
+
if torch.cuda.is_available():
|
|
164
|
+
torch.cuda.synchronize()
|
|
165
|
+
t0 = time.perf_counter()
|
|
166
|
+
with torch.no_grad():
|
|
167
|
+
wav = x.wav
|
|
168
|
+
if self.normalize_audio:
|
|
169
|
+
wav = wav / (wav.abs().max(dim=-1, keepdim=True).values + 1e-8)
|
|
170
|
+
mel = self.mel_spec_transform(wav)
|
|
171
|
+
mel = rearrange(mel, "b 1 d t -> b t d")
|
|
172
|
+
if self.fine_frame_rate_ratio > 1:
|
|
173
|
+
mel = rearrange(
|
|
174
|
+
mel[:, :-1], "b (t f) d -> b t (f d)", f=self.fine_frame_rate_ratio
|
|
175
|
+
)
|
|
176
|
+
if self.log_scale:
|
|
177
|
+
mel = torch.log(mel + self.eps)
|
|
178
|
+
if torch.cuda.is_available():
|
|
179
|
+
torch.cuda.synchronize()
|
|
180
|
+
print(
|
|
181
|
+
f"[muscriptor] mel-spec ({wav.shape[0]} × {wav.shape[-1]} samples): "
|
|
182
|
+
f"{time.perf_counter() - t0:.3f}s"
|
|
183
|
+
)
|
|
184
|
+
return mel
|
|
185
|
+
|
|
186
|
+
def forward(self, x: WavCondition) -> ConditionType:
|
|
187
|
+
_, lengths, *_ = x
|
|
188
|
+
with torch.no_grad():
|
|
189
|
+
embeds = self._mel_embedding(x)
|
|
190
|
+
embeds = embeds.to(self.output_proj.weight)
|
|
191
|
+
embeds = self.output_proj(embeds)
|
|
192
|
+
|
|
193
|
+
if lengths is not None:
|
|
194
|
+
lengths = lengths / (self.sample_rate // self.frame_rate)
|
|
195
|
+
mask = length_to_mask(lengths, max_len=embeds.shape[1]).int()
|
|
196
|
+
else:
|
|
197
|
+
mask = torch.ones_like(embeds[..., 0])
|
|
198
|
+
mask_f = mask.float().unsqueeze(-1).to(embeds.device)
|
|
199
|
+
embeds = embeds * mask_f
|
|
200
|
+
return embeds, mask
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class ClassConditioner(nn.Module):
|
|
204
|
+
"""Conditioner that embeds class indices (e.g., instrument group, dataset name)."""
|
|
205
|
+
|
|
206
|
+
def __init__(
|
|
207
|
+
self,
|
|
208
|
+
num_classes: int,
|
|
209
|
+
output_dim: int,
|
|
210
|
+
device: torch.device | str = "cpu",
|
|
211
|
+
):
|
|
212
|
+
super().__init__()
|
|
213
|
+
self.device = device
|
|
214
|
+
self.embed = nn.Embedding(num_classes + 1, output_dim).to(device)
|
|
215
|
+
self.pad_idx = 0
|
|
216
|
+
|
|
217
|
+
def tokenize(self, x: list[str | None]) -> torch.Tensor:
|
|
218
|
+
int_x = [list(map(int, s.split())) if s is not None else [-1] for s in x]
|
|
219
|
+
max_len = max(len(xi) for xi in int_x)
|
|
220
|
+
int_x = [xi + [-1] * (max_len - len(xi)) for xi in int_x]
|
|
221
|
+
int_x = 1 + torch.LongTensor(int_x).to(self.device)
|
|
222
|
+
return int_x
|
|
223
|
+
|
|
224
|
+
def forward(self, inputs: torch.Tensor) -> ConditionType:
|
|
225
|
+
embeds = self.embed(inputs + 1)
|
|
226
|
+
mask = torch.ones_like(embeds[..., 0])
|
|
227
|
+
return embeds, mask
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def collate_wavs(
|
|
231
|
+
samples: list[ConditioningAttributes], wav_conditions: list[str]
|
|
232
|
+
) -> dict[str, WavCondition]:
|
|
233
|
+
"""Collate wav conditions from a list of ConditioningAttributes."""
|
|
234
|
+
wavs = defaultdict(list)
|
|
235
|
+
lengths = defaultdict(list)
|
|
236
|
+
sample_rates: dict[str, list] = defaultdict(list)
|
|
237
|
+
paths: dict[str, list] = defaultdict(list)
|
|
238
|
+
seek_times: dict[str, list] = defaultdict(list)
|
|
239
|
+
out: dict[str, WavCondition] = {}
|
|
240
|
+
|
|
241
|
+
for sample in samples:
|
|
242
|
+
for attribute in wav_conditions:
|
|
243
|
+
wav, length, sample_rate, path, seek_time = sample.wav[attribute]
|
|
244
|
+
assert wav.dim() == 3
|
|
245
|
+
B, K, T = wav.shape
|
|
246
|
+
assert B == 1
|
|
247
|
+
if K == 2:
|
|
248
|
+
wav = wav.mean(1, keepdim=True)
|
|
249
|
+
wavs[attribute].append(wav)
|
|
250
|
+
lengths[attribute].append(length)
|
|
251
|
+
sample_rates[attribute].extend(sample_rate)
|
|
252
|
+
paths[attribute].extend(path)
|
|
253
|
+
seek_times[attribute].extend(seek_time)
|
|
254
|
+
|
|
255
|
+
for attribute in wav_conditions:
|
|
256
|
+
# Stack along batch dim
|
|
257
|
+
all_wavs = wavs[attribute]
|
|
258
|
+
max_len = max(w.shape[-1] for w in all_wavs)
|
|
259
|
+
padded = torch.cat(
|
|
260
|
+
[F.pad(w, (0, max_len - w.shape[-1])) for w in all_wavs], dim=0
|
|
261
|
+
)
|
|
262
|
+
out[attribute] = WavCondition(
|
|
263
|
+
padded,
|
|
264
|
+
torch.cat(lengths[attribute]),
|
|
265
|
+
sample_rates[attribute],
|
|
266
|
+
paths[attribute],
|
|
267
|
+
seek_times[attribute],
|
|
268
|
+
)
|
|
269
|
+
return out
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class ConditioningProvider(nn.Module):
|
|
273
|
+
"""Runs all conditioners and returns a dict of condition tensors."""
|
|
274
|
+
|
|
275
|
+
def __init__(
|
|
276
|
+
self,
|
|
277
|
+
conditioners: dict[str, nn.Module],
|
|
278
|
+
device: torch.device | str = "cpu",
|
|
279
|
+
):
|
|
280
|
+
super().__init__()
|
|
281
|
+
self.device = device
|
|
282
|
+
self.conditioners = nn.ModuleDict(conditioners)
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def text_conditions(self):
|
|
286
|
+
return [
|
|
287
|
+
k for k, v in self.conditioners.items() if isinstance(v, ClassConditioner)
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def wav_conditions(self):
|
|
292
|
+
return [
|
|
293
|
+
k
|
|
294
|
+
for k, v in self.conditioners.items()
|
|
295
|
+
if isinstance(v, MelSpectrogramConditioner)
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
def tokenize(self, inputs: list[ConditioningAttributes]) -> dict[str, Any]:
|
|
299
|
+
output = {}
|
|
300
|
+
# Collate text conditions
|
|
301
|
+
text_batch: dict[str, list[str | None]] = defaultdict(list)
|
|
302
|
+
for sample in inputs:
|
|
303
|
+
for cond in self.text_conditions:
|
|
304
|
+
text_batch[cond].append(sample.text.get(cond))
|
|
305
|
+
for attr, batch in text_batch.items():
|
|
306
|
+
output[attr] = self.conditioners[attr].tokenize(batch)
|
|
307
|
+
|
|
308
|
+
# Collate wav conditions
|
|
309
|
+
if self.wav_conditions:
|
|
310
|
+
wav_batch = collate_wavs(inputs, self.wav_conditions)
|
|
311
|
+
for attr, wav_cond in wav_batch.items():
|
|
312
|
+
output[attr] = self.conditioners[attr].tokenize(wav_cond)
|
|
313
|
+
|
|
314
|
+
return output
|
|
315
|
+
|
|
316
|
+
def forward(self, tokenized: dict[str, Any]) -> dict[str, ConditionType]:
|
|
317
|
+
output = {}
|
|
318
|
+
for attribute, inputs in tokenized.items():
|
|
319
|
+
condition, mask = self.conditioners[attribute](inputs)
|
|
320
|
+
output[attribute] = (condition, mask)
|
|
321
|
+
return output
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Standalone pure-torch equivalent of torchaudio.transforms.MelSpectrogram.
|
|
2
|
+
|
|
3
|
+
Matches torchaudio with mel_scale='htk', norm=None, win_length == n_fft.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
from torch import nn
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _hz_to_mel_htk(freq: torch.Tensor) -> torch.Tensor:
|
|
11
|
+
return 2595.0 * torch.log10(1.0 + freq / 700.0)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _mel_to_hz_htk(mel: torch.Tensor) -> torch.Tensor:
|
|
15
|
+
return 700.0 * (10 ** (mel / 2595.0) - 1.0)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def melscale_fbanks(
|
|
19
|
+
n_freqs: int,
|
|
20
|
+
f_min: float,
|
|
21
|
+
f_max: float,
|
|
22
|
+
n_mels: int,
|
|
23
|
+
sample_rate: int,
|
|
24
|
+
) -> torch.Tensor:
|
|
25
|
+
"""Triangular mel filterbank matching torchaudio.functional.melscale_fbanks
|
|
26
|
+
with mel_scale='htk' and norm=None. Returns a tensor of shape [n_freqs, n_mels]."""
|
|
27
|
+
all_freqs = torch.linspace(0, sample_rate // 2, n_freqs)
|
|
28
|
+
m_min = _hz_to_mel_htk(torch.tensor(float(f_min)))
|
|
29
|
+
m_max = _hz_to_mel_htk(torch.tensor(float(f_max)))
|
|
30
|
+
m_pts = torch.linspace(m_min.item(), m_max.item(), n_mels + 2)
|
|
31
|
+
f_pts = _mel_to_hz_htk(m_pts)
|
|
32
|
+
|
|
33
|
+
f_diff = f_pts[1:] - f_pts[:-1]
|
|
34
|
+
slopes = f_pts.unsqueeze(0) - all_freqs.unsqueeze(1)
|
|
35
|
+
down_slopes = -slopes[:, :-2] / f_diff[:-1]
|
|
36
|
+
up_slopes = slopes[:, 2:] / f_diff[1:]
|
|
37
|
+
return torch.maximum(torch.zeros(()), torch.minimum(down_slopes, up_slopes))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _Spectrogram(nn.Module):
|
|
41
|
+
"""Holds the STFT window so the safetensors key
|
|
42
|
+
`...mel_spec_transform.spectrogram.window` round-trips."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, n_fft: int):
|
|
45
|
+
super().__init__()
|
|
46
|
+
self.register_buffer("window", torch.hann_window(n_fft))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _MelScale(nn.Module):
|
|
50
|
+
"""Holds the mel filterbank so the safetensors key
|
|
51
|
+
`...mel_spec_transform.mel_scale.fb` round-trips."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, fb: torch.Tensor):
|
|
54
|
+
super().__init__()
|
|
55
|
+
self.register_buffer("fb", fb)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _MelSpectrogram(nn.Module):
|
|
59
|
+
"""Pure-torch equivalent of torchaudio.transforms.MelSpectrogram
|
|
60
|
+
(htk mel scale, no Slaney norm, win_length == n_fft)."""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
sample_rate: int,
|
|
65
|
+
n_fft: int,
|
|
66
|
+
hop_length: int,
|
|
67
|
+
n_mels: int,
|
|
68
|
+
power: float = 2.0,
|
|
69
|
+
center: bool = True,
|
|
70
|
+
pad_mode: str = "reflect",
|
|
71
|
+
):
|
|
72
|
+
super().__init__()
|
|
73
|
+
self.n_fft = n_fft
|
|
74
|
+
self.hop_length = hop_length
|
|
75
|
+
self.power = power
|
|
76
|
+
self.center = center
|
|
77
|
+
self.pad_mode = pad_mode
|
|
78
|
+
|
|
79
|
+
self.spectrogram = _Spectrogram(n_fft)
|
|
80
|
+
fb = melscale_fbanks(
|
|
81
|
+
n_freqs=n_fft // 2 + 1,
|
|
82
|
+
f_min=0.0,
|
|
83
|
+
f_max=sample_rate / 2.0,
|
|
84
|
+
n_mels=n_mels,
|
|
85
|
+
sample_rate=sample_rate,
|
|
86
|
+
)
|
|
87
|
+
self.mel_scale = _MelScale(fb)
|
|
88
|
+
|
|
89
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
90
|
+
leading = x.shape[:-1]
|
|
91
|
+
x = x.reshape(-1, x.shape[-1])
|
|
92
|
+
spec = torch.stft(
|
|
93
|
+
x,
|
|
94
|
+
n_fft=self.n_fft,
|
|
95
|
+
hop_length=self.hop_length,
|
|
96
|
+
win_length=self.n_fft,
|
|
97
|
+
window=self.spectrogram.window,
|
|
98
|
+
center=self.center,
|
|
99
|
+
pad_mode=self.pad_mode,
|
|
100
|
+
return_complex=True,
|
|
101
|
+
normalized=False,
|
|
102
|
+
onesided=True,
|
|
103
|
+
)
|
|
104
|
+
spec = spec.abs() ** self.power
|
|
105
|
+
mel = torch.matmul(spec.transpose(-1, -2), self.mel_scale.fb).transpose(-1, -2)
|
|
106
|
+
return mel.reshape(*leading, *mel.shape[-2:])
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Stateful module API.
|
|
2
|
+
|
|
3
|
+
Each :class:`StatefulModule` exposes :meth:`init_state` returning a dict of
|
|
4
|
+
per-module tensors. :func:`init_states` walks an ``nn.Module`` tree, calls
|
|
5
|
+
``init_state`` on every stateful submodule, and returns a ``dict[name -> state]``
|
|
6
|
+
that callers thread through ``forward`` via a ``model_state`` argument.
|
|
7
|
+
|
|
8
|
+
State is mutated only by :meth:`increment_step` (called explicitly via
|
|
9
|
+
:func:`increment_steps`) and by ``forward`` writing into preallocated buffers
|
|
10
|
+
at known offsets. No magic context manager, no implicit per-module storage.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from typing import Any
|
|
15
|
+
from torch import nn
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
State = dict[str, Any]
|
|
19
|
+
ModelState = dict[str, State]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StatefulModule(ABC, nn.Module):
|
|
23
|
+
def __init__(self, *args, **kwargs):
|
|
24
|
+
super().__init__(*args, **kwargs)
|
|
25
|
+
self._module_absolute_name: str | None = None
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def init_state(self, batch_size: int, sequence_length: int) -> State:
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
def increment_step(self, state: State, increment: int = 1) -> None:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
def get_state(self, model_state: ModelState | None) -> State | None:
|
|
35
|
+
if model_state is None or self._module_absolute_name is None:
|
|
36
|
+
return None
|
|
37
|
+
return model_state.get(self._module_absolute_name)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def init_states(model: nn.Module, batch_size: int, sequence_length: int) -> ModelState:
|
|
41
|
+
"""Allocate state for every :class:`StatefulModule` reachable from ``model``.
|
|
42
|
+
|
|
43
|
+
Side effect: each stateful submodule has its ``_module_absolute_name`` set
|
|
44
|
+
so subsequent ``get_state`` calls can find its slot.
|
|
45
|
+
"""
|
|
46
|
+
result: ModelState = {}
|
|
47
|
+
for module_name, module in model.named_modules():
|
|
48
|
+
if isinstance(module, StatefulModule):
|
|
49
|
+
module._module_absolute_name = module_name
|
|
50
|
+
result[module_name] = module.init_state(batch_size, sequence_length)
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def increment_steps(
|
|
55
|
+
model: nn.Module, model_state: ModelState, increment: int = 1
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Bump the step counter for every stateful submodule of ``model``.
|
|
58
|
+
|
|
59
|
+
Uses each module's ``_module_absolute_name`` (set by :func:`init_states`)
|
|
60
|
+
to look up its slot, so this works on subtrees even when ``init_states``
|
|
61
|
+
was called on a different root.
|
|
62
|
+
"""
|
|
63
|
+
for _, module in model.named_modules():
|
|
64
|
+
if (
|
|
65
|
+
isinstance(module, StatefulModule)
|
|
66
|
+
and module._module_absolute_name is not None
|
|
67
|
+
):
|
|
68
|
+
module.increment_step(model_state[module._module_absolute_name], increment)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Causal streaming transformer for muscriptor inference."""
|
|
2
|
+
|
|
3
|
+
from einops import rearrange
|
|
4
|
+
import torch
|
|
5
|
+
import torch.nn as nn
|
|
6
|
+
from torch.nn import functional as F
|
|
7
|
+
|
|
8
|
+
from muscriptor.modules.streaming import ModelState, State, StatefulModule
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_sin_embedding(
|
|
12
|
+
positions: torch.Tensor,
|
|
13
|
+
dim: int,
|
|
14
|
+
max_period: float = 10000,
|
|
15
|
+
dtype: torch.dtype = torch.float32,
|
|
16
|
+
) -> torch.Tensor:
|
|
17
|
+
assert dim % 2 == 0
|
|
18
|
+
half_dim = dim // 2
|
|
19
|
+
positions = positions.to(dtype)
|
|
20
|
+
adim = torch.arange(half_dim, device=positions.device, dtype=dtype).view(1, 1, -1)
|
|
21
|
+
max_period_tensor = torch.full([], max_period, device=positions.device, dtype=dtype)
|
|
22
|
+
phase = positions / (max_period_tensor ** (adim / (half_dim - 1)))
|
|
23
|
+
return torch.cat([torch.cos(phase), torch.sin(phase)], dim=-1)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StreamingMultiheadAttention(StatefulModule):
|
|
27
|
+
"""Causal multi-head self-attention with a preallocated KV cache."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
embed_dim: int,
|
|
32
|
+
num_heads: int,
|
|
33
|
+
device=None,
|
|
34
|
+
dtype=None,
|
|
35
|
+
):
|
|
36
|
+
super().__init__()
|
|
37
|
+
factory_kwargs = {"device": device, "dtype": dtype}
|
|
38
|
+
self.embed_dim = embed_dim
|
|
39
|
+
self.num_heads = num_heads
|
|
40
|
+
self.dim_per_head = embed_dim // num_heads
|
|
41
|
+
|
|
42
|
+
in_proj = nn.Linear(embed_dim, 3 * embed_dim, bias=False, **factory_kwargs)
|
|
43
|
+
self.in_proj_weight = in_proj.weight
|
|
44
|
+
self.in_proj_bias = in_proj.bias
|
|
45
|
+
self.out_proj = nn.Linear(embed_dim, embed_dim, bias=False, **factory_kwargs)
|
|
46
|
+
|
|
47
|
+
def init_state(self, batch_size: int, sequence_length: int) -> State:
|
|
48
|
+
weight = self.in_proj_weight
|
|
49
|
+
return {
|
|
50
|
+
"cache": torch.full(
|
|
51
|
+
(2, batch_size, sequence_length, self.num_heads, self.dim_per_head),
|
|
52
|
+
float("nan"),
|
|
53
|
+
device=weight.device,
|
|
54
|
+
dtype=weight.dtype,
|
|
55
|
+
),
|
|
56
|
+
"offset": torch.zeros(1, dtype=torch.long, device=weight.device),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def increment_step(self, state: State, increment: int = 1) -> None:
|
|
60
|
+
state["offset"] = state["offset"] + increment
|
|
61
|
+
|
|
62
|
+
def _complete_kv(self, k, v, state: State | None):
|
|
63
|
+
if state is None:
|
|
64
|
+
return k, v
|
|
65
|
+
cache = state["cache"]
|
|
66
|
+
end = int(state["offset"].item())
|
|
67
|
+
T = k.shape[1]
|
|
68
|
+
cache[0, :, end : end + T] = k
|
|
69
|
+
cache[1, :, end : end + T] = v
|
|
70
|
+
return cache[0, :, : end + T], cache[1, :, : end + T]
|
|
71
|
+
|
|
72
|
+
def forward(
|
|
73
|
+
self,
|
|
74
|
+
query: torch.Tensor,
|
|
75
|
+
model_state: ModelState | None = None,
|
|
76
|
+
):
|
|
77
|
+
state = self.get_state(model_state)
|
|
78
|
+
projected = nn.functional.linear(query, self.in_proj_weight)
|
|
79
|
+
packed = rearrange(projected, "b t (p h d) -> b t p h d", p=3, h=self.num_heads)
|
|
80
|
+
q, k, v = packed.unbind(dim=2)
|
|
81
|
+
|
|
82
|
+
k, v = self._complete_kv(k, v, state)
|
|
83
|
+
dtype = q.dtype
|
|
84
|
+
|
|
85
|
+
q_t = q.transpose(1, 2)
|
|
86
|
+
k_t = k.transpose(1, 2)
|
|
87
|
+
v_t = v.transpose(1, 2)
|
|
88
|
+
|
|
89
|
+
# Explicit bottom-right causal mask so streaming decode steps
|
|
90
|
+
# (T_q=1, T_k=cache_len) attend to all past tokens. PyTorch's
|
|
91
|
+
# is_causal=True uses top-left alignment and would mask out all
|
|
92
|
+
# cached tokens except position 0 when T_q < T_k.
|
|
93
|
+
T_q, T_k = q_t.shape[2], k_t.shape[2]
|
|
94
|
+
mask = torch.ones(T_q, T_k, dtype=torch.bool, device=query.device).tril(
|
|
95
|
+
T_k - T_q
|
|
96
|
+
)
|
|
97
|
+
attn_bias = torch.zeros(T_q, T_k, dtype=q.dtype, device=query.device)
|
|
98
|
+
attn_bias.masked_fill_(~mask, float("-inf"))
|
|
99
|
+
x = F.scaled_dot_product_attention(
|
|
100
|
+
q_t, k_t, v_t, attn_mask=attn_bias, dropout_p=0.0
|
|
101
|
+
)
|
|
102
|
+
x = x.transpose(1, 2).to(dtype)
|
|
103
|
+
|
|
104
|
+
x = rearrange(x, "b t h d -> b t (h d)")
|
|
105
|
+
x = self.out_proj(x)
|
|
106
|
+
return x
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class StreamingTransformerLayer(nn.Module):
|
|
110
|
+
"""Pre-norm transformer block: self-attention + GELU FFN."""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
d_model: int,
|
|
115
|
+
num_heads: int,
|
|
116
|
+
dim_feedforward: int = 2048,
|
|
117
|
+
device=None,
|
|
118
|
+
dtype=None,
|
|
119
|
+
):
|
|
120
|
+
super().__init__()
|
|
121
|
+
factory_kwargs = {"device": device, "dtype": dtype}
|
|
122
|
+
self.self_attn = StreamingMultiheadAttention(
|
|
123
|
+
embed_dim=d_model, num_heads=num_heads, **factory_kwargs
|
|
124
|
+
)
|
|
125
|
+
self.norm1 = nn.LayerNorm(d_model, eps=1e-5, **factory_kwargs)
|
|
126
|
+
self.norm2 = nn.LayerNorm(d_model, eps=1e-5, **factory_kwargs)
|
|
127
|
+
self.linear1 = nn.Linear(d_model, dim_feedforward, bias=False, **factory_kwargs)
|
|
128
|
+
self.linear2 = nn.Linear(dim_feedforward, d_model, bias=False, **factory_kwargs)
|
|
129
|
+
|
|
130
|
+
def forward(
|
|
131
|
+
self,
|
|
132
|
+
x: torch.Tensor,
|
|
133
|
+
model_state: ModelState | None = None,
|
|
134
|
+
):
|
|
135
|
+
x = x + self.self_attn(self.norm1(x), model_state=model_state)
|
|
136
|
+
x = x + self.linear2(F.gelu(self.linear1(self.norm2(x))))
|
|
137
|
+
return x
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class StreamingTransformer(StatefulModule):
|
|
141
|
+
"""Stack of causal streaming transformer layers with sinusoidal positions."""
|
|
142
|
+
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
d_model: int,
|
|
146
|
+
num_heads: int,
|
|
147
|
+
num_layers: int,
|
|
148
|
+
dim_feedforward: int = 2048,
|
|
149
|
+
max_period: float = 10_000,
|
|
150
|
+
device=None,
|
|
151
|
+
dtype=None,
|
|
152
|
+
):
|
|
153
|
+
super().__init__()
|
|
154
|
+
assert d_model % num_heads == 0
|
|
155
|
+
self.max_period = max_period
|
|
156
|
+
self.layers = nn.ModuleList(
|
|
157
|
+
[
|
|
158
|
+
StreamingTransformerLayer(
|
|
159
|
+
d_model=d_model,
|
|
160
|
+
num_heads=num_heads,
|
|
161
|
+
dim_feedforward=dim_feedforward,
|
|
162
|
+
device=device,
|
|
163
|
+
dtype=dtype,
|
|
164
|
+
)
|
|
165
|
+
for _ in range(num_layers)
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def init_state(self, batch_size: int, sequence_length: int) -> State:
|
|
170
|
+
device = self.layers[0].norm2.weight.device
|
|
171
|
+
return {
|
|
172
|
+
"offsets": torch.zeros(batch_size, dtype=torch.long, device=device),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
def increment_step(self, state: State, increment: int = 1) -> None:
|
|
176
|
+
state["offsets"] = state["offsets"] + increment
|
|
177
|
+
|
|
178
|
+
def forward(
|
|
179
|
+
self,
|
|
180
|
+
x: torch.Tensor,
|
|
181
|
+
prepend_length: int = 0,
|
|
182
|
+
model_state: ModelState | None = None,
|
|
183
|
+
):
|
|
184
|
+
del prepend_length # unused; positions come from state['offsets']
|
|
185
|
+
B, T, C = x.shape
|
|
186
|
+
state = self.get_state(model_state)
|
|
187
|
+
offsets = (
|
|
188
|
+
state["offsets"]
|
|
189
|
+
if state is not None
|
|
190
|
+
else torch.zeros(B, dtype=torch.long, device=x.device)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
positions = torch.arange(T, device=x.device).view(1, -1, 1)
|
|
194
|
+
positions = positions + offsets.view(-1, 1, 1)
|
|
195
|
+
pos_emb = create_sin_embedding(
|
|
196
|
+
positions, C, max_period=self.max_period, dtype=x.dtype
|
|
197
|
+
)
|
|
198
|
+
x = x + pos_emb * (positions >= 0).float()
|
|
199
|
+
|
|
200
|
+
for layer in self.layers:
|
|
201
|
+
x = layer(x, model_state=model_state)
|
|
202
|
+
return x
|