Daporte commited on
Commit
abb10d2
·
verified ·
1 Parent(s): 380b209

Upload conformer_model.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. conformer_model.py +626 -0
conformer_model.py ADDED
@@ -0,0 +1,626 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The conformer that turns prosody features into a mel spectrogram.
2
+
3
+ Extracted verbatim from the training code -- `prosody_embeddings_to_mel` --
4
+ so the architecture here is the one the checkpoint was trained with rather
5
+ than a reimplementation of it. Only what inference touches: the model
6
+ classes and `generate()`, with no Trainer, dataset or metric code.
7
+
8
+ `generate()` is non-autoregressive. It sizes its output from the real input
9
+ length and takes a padding mask, so a phrase is predicted at its own
10
+ duration -- which is why this is the path the published numbers come from.
11
+ The ONNX export cannot do that: its input width is baked in at 2000 frames,
12
+ so every phrase is padded to ten seconds and the decoder attends over the
13
+ padding. Measured against the training pipeline's own predictions, 0.9985
14
+ correlation here against 0.9357 through the export.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import math
20
+
21
+ import torch
22
+ import torch.nn as nn
23
+ from torch.utils.checkpoint import checkpoint as grad_checkpoint
24
+
25
+
26
+ class SinusoidalPositionalEncoding(nn.Module):
27
+ def __init__(self, d_model, max_len=5000):
28
+ super().__init__()
29
+ pe = torch.zeros(max_len, d_model)
30
+ position = torch.arange(0, max_len).unsqueeze(1)
31
+ div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
32
+ pe[:, 0::2] = torch.sin(position * div_term)
33
+ pe[:, 1::2] = torch.cos(position * div_term)
34
+ self.register_buffer("pe", pe)
35
+
36
+ def forward(self, x):
37
+ # x is [B, T, D] (batch_first)
38
+ return x + self.pe[: x.size(1)]
39
+
40
+
41
+ # ----------------------------
42
+ # Conformer Block
43
+ # ----------------------------
44
+
45
+
46
+ class ConformerBlock(nn.Module):
47
+ """Single conformer block: FFN -> Self-Attention -> Conv -> FFN.
48
+
49
+ Combines local convolutions with self-attention for both local
50
+ detail and global context, at lower cost than a full transformer
51
+ decoder (conv handles local, attention handles global).
52
+ """
53
+ def __init__(self, d_model, nhead=4, kernel_size=5, dim_feedforward=None, dropout=0.1):
54
+ super().__init__()
55
+ dim_ff = dim_feedforward or d_model * 4
56
+
57
+ # First half-step FFN
58
+ self.ffn1_norm = nn.LayerNorm(d_model)
59
+ self.ffn1 = nn.Sequential(
60
+ nn.Linear(d_model, dim_ff), nn.GELU(), nn.Dropout(dropout),
61
+ nn.Linear(dim_ff, d_model), nn.Dropout(dropout),
62
+ )
63
+
64
+ # Self-attention
65
+ self.attn_norm = nn.LayerNorm(d_model)
66
+ self.self_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=True)
67
+ self.attn_dropout = nn.Dropout(dropout)
68
+
69
+ # Depthwise convolution module
70
+ self.conv_norm = nn.LayerNorm(d_model)
71
+ self.conv = nn.Sequential(
72
+ nn.Conv1d(d_model, d_model, 1), # pointwise
73
+ nn.GELU(),
74
+ nn.Conv1d(d_model, d_model, kernel_size, padding=kernel_size // 2, groups=d_model), # depthwise
75
+ nn.LayerNorm(d_model), # NOTE: applied after transpose in forward
76
+ nn.GELU(),
77
+ nn.Conv1d(d_model, d_model, 1), # pointwise
78
+ nn.Dropout(dropout),
79
+ )
80
+ # Replace the LayerNorm inside Sequential with identity — we apply it manually
81
+ self.conv_ln = self.conv[3]
82
+ self.conv[3] = nn.Identity()
83
+
84
+ # Second half-step FFN
85
+ self.ffn2_norm = nn.LayerNorm(d_model)
86
+ self.ffn2 = nn.Sequential(
87
+ nn.Linear(d_model, dim_ff), nn.GELU(), nn.Dropout(dropout),
88
+ nn.Linear(dim_ff, d_model), nn.Dropout(dropout),
89
+ )
90
+
91
+ self.final_norm = nn.LayerNorm(d_model)
92
+
93
+ def forward(self, x, padding_mask=None):
94
+ """
95
+ Args:
96
+ x: [B, T, D]
97
+ padding_mask: [B, T] with True for padding
98
+ Returns:
99
+ [B, T, D]
100
+ """
101
+ # Half-step FFN
102
+ x = x + 0.5 * self.ffn1(self.ffn1_norm(x))
103
+
104
+ # Self-attention
105
+ h = self.attn_norm(x)
106
+ attn_out, _ = self.self_attn(h, h, h, key_padding_mask=padding_mask)
107
+ x = x + self.attn_dropout(attn_out)
108
+
109
+ # Conv module
110
+ h = self.conv_norm(x)
111
+ h = h.transpose(1, 2) # [B, D, T]
112
+ # Apply conv layers, but handle LayerNorm which needs [B, T, D]
113
+ h = self.conv[0](h) # pointwise
114
+ h = self.conv[1](h) # GELU
115
+ h = self.conv[2](h) # depthwise
116
+ h = h.transpose(1, 2) # [B, T, D]
117
+ h = self.conv_ln(h) # LayerNorm
118
+ h = self.conv[4](h.transpose(1, 2)) # GELU (expects [B, D, T] from Conv1d but GELU is elementwise)
119
+ h = self.conv[5](h) # pointwise
120
+ h = self.conv[6](h) # dropout
121
+ h = h.transpose(1, 2) # [B, T, D]
122
+ x = x + h
123
+
124
+ # Half-step FFN
125
+ x = x + 0.5 * self.ffn2(self.ffn2_norm(x))
126
+
127
+ x = self.final_norm(x)
128
+
129
+ if padding_mask is not None:
130
+ x = x.masked_fill(padding_mask.unsqueeze(-1), 0.0)
131
+
132
+ return x
133
+
134
+
135
+ class ConformerDecoder(nn.Module):
136
+ """Stack of conformer blocks as a decoder."""
137
+ def __init__(self, d_model, num_layers=4, nhead=4, kernel_size=5, dim_feedforward=None, dropout=0.1):
138
+ super().__init__()
139
+ self.blocks = nn.ModuleList([
140
+ ConformerBlock(d_model, nhead, kernel_size, dim_feedforward, dropout)
141
+ for _ in range(num_layers)
142
+ ])
143
+
144
+ def forward(self, x, padding_mask=None):
145
+ for block in self.blocks:
146
+ x = block(x, padding_mask)
147
+ return x
148
+
149
+
150
+ # ----------------------------
151
+ # Model
152
+ # ----------------------------
153
+
154
+
155
+ class WindowedConformerBlock(ConformerBlock):
156
+ """ConformerBlock with windowed self-attention for O(T*W) memory instead of O(T²).
157
+
158
+ Splits the sequence into non-overlapping windows of size W and applies
159
+ self-attention independently within each window. The convolution module
160
+ still provides cross-window local context.
161
+ """
162
+ def __init__(self, d_model, nhead=4, kernel_size=5, dim_feedforward=None, dropout=0.1, attn_window=256):
163
+ super().__init__(d_model, nhead, kernel_size, dim_feedforward, dropout)
164
+ self.attn_window = attn_window
165
+
166
+ def forward(self, x, padding_mask=None):
167
+ """Same as ConformerBlock but with windowed attention."""
168
+ # Half-step FFN
169
+ x = x + 0.5 * self.ffn1(self.ffn1_norm(x))
170
+
171
+ # Windowed self-attention
172
+ h = self.attn_norm(x)
173
+ attn_out = self._windowed_attention(h, padding_mask)
174
+ x = x + self.attn_dropout(attn_out)
175
+
176
+ # Conv module (unchanged — provides cross-window context)
177
+ h = self.conv_norm(x)
178
+ h = h.transpose(1, 2) # [B, D, T]
179
+ h = self.conv[0](h) # pointwise
180
+ h = self.conv[1](h) # GELU
181
+ h = self.conv[2](h) # depthwise
182
+ h = h.transpose(1, 2) # [B, T, D]
183
+ h = self.conv_ln(h) # LayerNorm
184
+ h = self.conv[4](h.transpose(1, 2)) # GELU
185
+ h = self.conv[5](h) # pointwise
186
+ h = self.conv[6](h) # dropout
187
+ h = h.transpose(1, 2) # [B, T, D]
188
+ x = x + h
189
+
190
+ # Half-step FFN
191
+ x = x + 0.5 * self.ffn2(self.ffn2_norm(x))
192
+
193
+ x = self.final_norm(x)
194
+
195
+ if padding_mask is not None:
196
+ x = x.masked_fill(padding_mask.unsqueeze(-1), 0.0)
197
+
198
+ return x
199
+
200
+ def _windowed_attention(self, x, padding_mask):
201
+ """Self-attention within non-overlapping windows.
202
+
203
+ Args:
204
+ x: [B, T, D] — already normed
205
+ padding_mask: [B, T] with True for padding, or None
206
+
207
+ Returns:
208
+ [B, T, D]
209
+ """
210
+ B, T, D = x.shape
211
+ W = self.attn_window
212
+
213
+ # Short sequences: full attention (no windowing needed)
214
+ if T <= W:
215
+ out, _ = self.self_attn(x, x, x, key_padding_mask=padding_mask)
216
+ return out
217
+
218
+ # Pad sequence to a multiple of W
219
+ pad_len = (W - T % W) % W
220
+ if pad_len > 0:
221
+ x = torch.nn.functional.pad(x, (0, 0, 0, pad_len)) # pad time dim
222
+ if padding_mask is not None:
223
+ padding_mask = torch.nn.functional.pad(padding_mask, (0, pad_len), value=True)
224
+
225
+ T_padded = x.size(1)
226
+ num_windows = T_padded // W
227
+
228
+ # Reshape: [B, num_windows, W, D] -> [B*num_windows, W, D]
229
+ x_win = x.reshape(B * num_windows, W, D)
230
+
231
+ # Reshape mask: [B, num_windows, W] -> [B*num_windows, W]
232
+ mask_win = None
233
+ if padding_mask is not None:
234
+ mask_win = padding_mask.reshape(B * num_windows, W)
235
+ # If an entire window is padding, MHA will produce NaN.
236
+ # Detect all-padding windows and handle them.
237
+ all_padding = mask_win.all(dim=1) # [B*num_windows]
238
+ if all_padding.any():
239
+ # Temporarily unmask one position so MHA doesn't get empty input
240
+ mask_win = mask_win.clone()
241
+ mask_win[all_padding, 0] = False
242
+
243
+ attn_out, _ = self.self_attn(x_win, x_win, x_win, key_padding_mask=mask_win)
244
+
245
+ # Zero out windows that were entirely padding
246
+ if padding_mask is not None and all_padding.any():
247
+ attn_out[all_padding] = 0.0
248
+
249
+ # Reshape back: [B*num_windows, W, D] -> [B, T_padded, D] -> [B, T, D]
250
+ attn_out = attn_out.reshape(B, T_padded, D)[:, :T, :]
251
+ return attn_out
252
+
253
+
254
+ class WindowedConformerDecoder(nn.Module):
255
+ """Stack of windowed conformer blocks."""
256
+ def __init__(self, d_model, num_layers=4, nhead=4, kernel_size=5, dim_feedforward=None, dropout=0.1, attn_window=256):
257
+ super().__init__()
258
+ self.blocks = nn.ModuleList([
259
+ WindowedConformerBlock(d_model, nhead, kernel_size, dim_feedforward, dropout, attn_window)
260
+ for _ in range(num_layers)
261
+ ])
262
+
263
+ def forward(self, x, padding_mask=None):
264
+ for block in self.blocks:
265
+ x = block(x, padding_mask)
266
+ return x
267
+
268
+
269
+ # ----------------------------
270
+ # Convolutional Encoder
271
+ # ----------------------------
272
+
273
+
274
+ class ConvEncoder(nn.Module):
275
+ """Multi-layer 1D convolutional encoder with stride-2 downsampling.
276
+
277
+ Designed for smooth, low-dimensional prosodic signals (f0 + intensity).
278
+ Uses dilated convolutions to capture multi-scale temporal context efficiently,
279
+ with a final stride-2 layer to handle the 200 Hz → 100 Hz rate conversion.
280
+ """
281
+ def __init__(self, input_dim, d_model, num_layers=4, kernel_size=7, dropout=0.1):
282
+ super().__init__()
283
+ self.input_proj = nn.Conv1d(input_dim, d_model, kernel_size=1)
284
+
285
+ layers = []
286
+ for i in range(num_layers - 1):
287
+ dilation = 2 ** i # 1, 2, 4, 8... expanding receptive field
288
+ padding = (kernel_size - 1) * dilation // 2
289
+ layers.append(ConvBlock(d_model, kernel_size, dilation, padding, dropout))
290
+
291
+ self.layers = nn.ModuleList(layers)
292
+
293
+ # Final stride-2 conv for downsampling (200 Hz → 100 Hz)
294
+ self.downsample = nn.Conv1d(
295
+ d_model, d_model,
296
+ kernel_size=4, stride=2, padding=1,
297
+ )
298
+ self.downsample_norm = nn.LayerNorm(d_model)
299
+
300
+ def forward(self, x, padding_mask=None):
301
+ """
302
+ Args:
303
+ x: [B, T, input_dim]
304
+ padding_mask: [B, T] with True for padding positions
305
+
306
+ Returns:
307
+ out: [B, T//2, d_model]
308
+ out_mask: [B, T//2] or None
309
+ """
310
+ # [B, T, C] → [B, C, T] for Conv1d
311
+ h = x.transpose(1, 2)
312
+ h = self.input_proj(h)
313
+
314
+ # Zero out padded positions before conv
315
+ if padding_mask is not None:
316
+ h = h.masked_fill(padding_mask.unsqueeze(1), 0.0)
317
+
318
+ for layer in self.layers:
319
+ h = layer(h, padding_mask)
320
+
321
+ # Stride-2 downsampling
322
+ h = self.downsample(h) # [B, d_model, T//2]
323
+ h = h.transpose(1, 2) # [B, T//2, d_model]
324
+ h = self.downsample_norm(h)
325
+
326
+ # Compute output padding mask
327
+ out_mask = None
328
+ if padding_mask is not None:
329
+ T_out = h.size(1)
330
+ # A downsampled position is padding if both source frames were padding
331
+ T_in = padding_mask.size(1)
332
+ if T_in % 2 != 0:
333
+ padding_mask = torch.cat([
334
+ padding_mask,
335
+ torch.ones(padding_mask.size(0), 1, dtype=torch.bool, device=padding_mask.device)
336
+ ], dim=1)
337
+ mask_pairs = padding_mask.view(padding_mask.size(0), -1, 2)
338
+ out_mask = mask_pairs.all(dim=-1)[:, :T_out]
339
+
340
+ if out_mask is not None:
341
+ h = h.masked_fill(out_mask.unsqueeze(-1), 0.0)
342
+
343
+ return h, out_mask
344
+
345
+
346
+ class ConvBlock(nn.Module):
347
+ """Residual dilated conv block with pre-norm."""
348
+ def __init__(self, d_model, kernel_size, dilation, padding, dropout):
349
+ super().__init__()
350
+ self.norm = nn.LayerNorm(d_model)
351
+ self.conv = nn.Conv1d(
352
+ d_model, d_model,
353
+ kernel_size=kernel_size,
354
+ dilation=dilation,
355
+ padding=padding,
356
+ )
357
+ self.activation = nn.GELU()
358
+ self.dropout = nn.Dropout(dropout)
359
+ self.proj = nn.Conv1d(d_model, d_model, kernel_size=1)
360
+
361
+ def forward(self, x, padding_mask=None):
362
+ """
363
+ Args:
364
+ x: [B, d_model, T]
365
+ padding_mask: [B, T] with True for padding
366
+ Returns:
367
+ [B, d_model, T]
368
+ """
369
+ residual = x
370
+ # Pre-norm (need to transpose for LayerNorm)
371
+ h = x.transpose(1, 2) # [B, T, D]
372
+ h = self.norm(h)
373
+ h = h.transpose(1, 2) # [B, D, T]
374
+
375
+ h = self.conv(h)
376
+ h = self.activation(h)
377
+ h = self.dropout(h)
378
+ h = self.proj(h)
379
+ h = self.dropout(h)
380
+
381
+ out = residual + h
382
+
383
+ if padding_mask is not None:
384
+ out = out.masked_fill(padding_mask.unsqueeze(1), 0.0)
385
+
386
+ return out
387
+
388
+
389
+ # ----------------------------
390
+ # Model
391
+ # ----------------------------
392
+
393
+
394
+ class FeaturesToMelConv(nn.Module):
395
+ """Non-autoregressive conv-encoder + Conformer-decoder for f0+intensity to mel.
396
+
397
+ Architecture rationale:
398
+ - Conv encoder: F0 and intensity are smooth, slowly-varying 1D signals.
399
+ Dilated convolutions naturally capture multi-scale temporal patterns
400
+ (pitch contours, intensity envelopes) with the right inductive bias.
401
+ The stride-2 final layer handles the 200→100 Hz rate conversion
402
+ as part of the encoding, rather than as a separate step.
403
+ - Conformer decoder: Refines the downsampled features using both local
404
+ (depthwise conv) and global (self-attention) context to produce
405
+ the mel spectrogram. Self-attention here is justified because the
406
+ decoder operates at 100 Hz on d_model-dimensional features — a
407
+ richer representation where global dependencies matter.
408
+
409
+ Memory optimization: gradient checkpointing on decoder blocks (recomputes
410
+ activations during backward instead of storing them).
411
+ """
412
+ def __init__(
413
+ self,
414
+ input_dim=2,
415
+ mel_dim=14,
416
+ d_model=256,
417
+ nhead=4,
418
+ num_encoder_layers=4,
419
+ num_decoder_layers=4,
420
+ encoder_kernel_size=7,
421
+ conv_kernel_size=5,
422
+ dim_feedforward=None,
423
+ dropout=0.1,
424
+ max_len=16000,
425
+ loss_type="mse",
426
+ length_ratio=0.5,
427
+ gradient_checkpointing=True,
428
+ decoder_only=False,
429
+ attn_window=0,
430
+ ):
431
+ super().__init__()
432
+ self.d_model = d_model
433
+ self.mel_dim = mel_dim
434
+ self.max_len = max_len
435
+ self.length_ratio = length_ratio
436
+ self.gradient_checkpointing = gradient_checkpointing
437
+ self.decoder_only = decoder_only
438
+
439
+ if not decoder_only:
440
+ self.encoder = ConvEncoder(
441
+ input_dim=input_dim,
442
+ d_model=d_model,
443
+ num_layers=num_encoder_layers,
444
+ kernel_size=encoder_kernel_size,
445
+ dropout=dropout,
446
+ )
447
+ else:
448
+ # Decoder-only: simple linear projection + learned stride-2 downsample
449
+ self.input_proj = nn.Linear(input_dim, d_model)
450
+ self.downsample = nn.Linear(d_model * 2, d_model)
451
+
452
+ self.pos_decoder = SinusoidalPositionalEncoding(d_model, max_len)
453
+
454
+ dim_ff = dim_feedforward if dim_feedforward is not None else d_model * 4
455
+ if attn_window > 0:
456
+ self.decoder = WindowedConformerDecoder(
457
+ d_model=d_model,
458
+ num_layers=num_decoder_layers,
459
+ nhead=nhead,
460
+ kernel_size=conv_kernel_size,
461
+ dim_feedforward=dim_ff,
462
+ dropout=dropout,
463
+ attn_window=attn_window,
464
+ )
465
+ else:
466
+ self.decoder = ConformerDecoder(
467
+ d_model=d_model,
468
+ num_layers=num_decoder_layers,
469
+ nhead=nhead,
470
+ kernel_size=conv_kernel_size,
471
+ dim_feedforward=dim_ff,
472
+ dropout=dropout,
473
+ )
474
+
475
+ self.output_proj = nn.Linear(d_model, mel_dim)
476
+
477
+ if loss_type == "mse":
478
+ self.criterion = nn.MSELoss(reduction="none")
479
+ elif loss_type == "l1":
480
+ self.criterion = nn.L1Loss(reduction="none")
481
+ elif loss_type == "smoothl1":
482
+ self.criterion = nn.SmoothL1Loss(reduction="none")
483
+ else:
484
+ raise ValueError(f"Unknown loss_type={loss_type}")
485
+
486
+ def _run_decoder(self, tgt, padding_mask):
487
+ """Run decoder with optional gradient checkpointing per block."""
488
+ if self.gradient_checkpointing and self.training:
489
+ for block in self.decoder.blocks:
490
+ tgt = grad_checkpoint(block, tgt, padding_mask, use_reentrant=False)
491
+ return tgt
492
+ else:
493
+ return self.decoder(tgt, padding_mask=padding_mask)
494
+
495
+ def _encode(self, inputs, input_padding_mask):
496
+ """Encode inputs, returns (encoded, mask) at output frame rate."""
497
+ if not self.decoder_only:
498
+ return self.encoder(inputs, input_padding_mask)
499
+
500
+ # Decoder-only path: project + pair-merge downsample
501
+ B, T, _ = inputs.shape
502
+ h = self.input_proj(inputs) # [B, T, d_model]
503
+
504
+ # Pad to even length
505
+ if T % 2 != 0:
506
+ h = torch.cat([h, torch.zeros(B, 1, self.d_model, device=h.device)], dim=1)
507
+ if input_padding_mask is not None:
508
+ input_padding_mask = torch.cat([
509
+ input_padding_mask,
510
+ torch.ones(B, 1, dtype=torch.bool, device=h.device)
511
+ ], dim=1)
512
+ T = T + 1
513
+
514
+ # Merge pairs: [B, T, D] -> [B, T//2, 2*D] -> [B, T//2, D]
515
+ h = h.view(B, T // 2, 2 * self.d_model)
516
+ h = self.downsample(h)
517
+
518
+ # Compute output mask
519
+ out_mask = None
520
+ if input_padding_mask is not None:
521
+ mask_pairs = input_padding_mask.view(B, T // 2, 2)
522
+ out_mask = mask_pairs.all(dim=-1)
523
+
524
+ if out_mask is not None:
525
+ h = h.masked_fill(out_mask.unsqueeze(-1), 0.0)
526
+
527
+ return h, out_mask
528
+
529
+ def forward(self, inputs, labels, input_padding_mask=None, label_padding_mask=None):
530
+ """
531
+ Args:
532
+ inputs: [B, T_src, input_dim] - f0 + intensity at 200 Hz
533
+ labels: [B, T_tgt, mel_dim] - ground truth mel at 100 Hz
534
+ input_padding_mask: [B, T_src] with True for padding
535
+ label_padding_mask: [B, T_tgt] with True for padding
536
+
537
+ Returns:
538
+ dict with 'loss' and 'logits'
539
+ """
540
+ B = labels.size(0)
541
+ T_tgt = labels.size(1)
542
+
543
+ # Encode + downsample
544
+ enc_out, enc_mask = self._encode(inputs, input_padding_mask)
545
+
546
+ # Align to target length
547
+ if enc_out.size(1) < T_tgt:
548
+ pad_len = T_tgt - enc_out.size(1)
549
+ enc_out = torch.cat([enc_out, torch.zeros(B, pad_len, self.d_model, device=enc_out.device)], dim=1)
550
+ if enc_mask is not None:
551
+ enc_mask = torch.cat([enc_mask, torch.ones(B, pad_len, dtype=torch.bool, device=enc_out.device)], dim=1)
552
+ elif enc_out.size(1) > T_tgt:
553
+ enc_out = enc_out[:, :T_tgt, :]
554
+ if enc_mask is not None:
555
+ enc_mask = enc_mask[:, :T_tgt]
556
+
557
+ # Add positional encoding for decoder
558
+ tgt = enc_out * math.sqrt(self.d_model)
559
+ tgt = self.pos_decoder(tgt)
560
+
561
+ # Conformer decoder refines the features
562
+ output = self._run_decoder(tgt, label_padding_mask)
563
+
564
+ logits = self.output_proj(output) # [B, T_tgt, mel_dim]
565
+
566
+ # Compute loss
567
+ loss = self.criterion(logits, labels)
568
+ if label_padding_mask is not None:
569
+ active_mask = ~label_padding_mask
570
+ denom = (active_mask.sum() * logits.size(-1)).clamp_min(1).to(loss.dtype)
571
+ loss = (loss * active_mask.unsqueeze(-1)).sum() / (denom + 1e-8)
572
+ else:
573
+ loss = loss.mean()
574
+
575
+ return {"loss": loss, "logits": logits}
576
+
577
+ def generate(self, inputs, input_padding_mask=None, max_steps=None, length_ratio=None, return_lengths=False):
578
+ """Non-autoregressive parallel generation."""
579
+ B = inputs.size(0)
580
+ device = inputs.device
581
+
582
+ if length_ratio is None:
583
+ length_ratio = self.length_ratio
584
+
585
+ if max_steps is None:
586
+ if input_padding_mask is not None:
587
+ input_lengths = (~input_padding_mask).sum(dim=1)
588
+ target_lengths = (input_lengths.float() * length_ratio).round().long()
589
+ T_gen = int(target_lengths.max().item())
590
+ else:
591
+ input_lengths = torch.full((B,), inputs.size(1), device=device, dtype=torch.long)
592
+ target_lengths = (input_lengths.float() * length_ratio).round().long()
593
+ T_gen = int(target_lengths[0].item())
594
+ else:
595
+ T_gen = max_steps
596
+ target_lengths = torch.full((B,), T_gen, device=device, dtype=torch.long)
597
+
598
+ T_gen = max(1, min(T_gen, self.max_len))
599
+
600
+ output_padding_mask = torch.zeros(B, T_gen, dtype=torch.bool, device=device)
601
+ for i in range(B):
602
+ if target_lengths[i] < T_gen:
603
+ output_padding_mask[i, target_lengths[i]:] = True
604
+
605
+ enc_out, _ = self._encode(inputs, input_padding_mask)
606
+
607
+ if enc_out.size(1) < T_gen:
608
+ pad_len = T_gen - enc_out.size(1)
609
+ enc_out = torch.cat([enc_out, torch.zeros(B, pad_len, self.d_model, device=device)], dim=1)
610
+ elif enc_out.size(1) > T_gen:
611
+ enc_out = enc_out[:, :T_gen, :]
612
+
613
+ tgt = enc_out * math.sqrt(self.d_model)
614
+ tgt = self.pos_decoder(tgt)
615
+
616
+ output = self.decoder(tgt, padding_mask=output_padding_mask)
617
+ generated = self.output_proj(output)
618
+
619
+ if return_lengths:
620
+ return generated, target_lengths, output_padding_mask
621
+ return generated
622
+
623
+
624
+ # ----------------------------
625
+ # Dataset
626
+ # ----------------------------