Wendy9805 commited on
Commit
f00909c
·
verified ·
1 Parent(s): 5e78617

Upload inference.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. inference.py +891 -0
inference.py ADDED
@@ -0,0 +1,891 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ PS4 — Target Speaker Extraction Inference Script
4
+ =================================================
5
+
6
+ Self-contained inference script for the PS4 TSE model.
7
+ No external dependencies beyond torch, torchaudio, and numpy.
8
+
9
+ Usage:
10
+ # Basic inference
11
+ python inference.py \\
12
+ --checkpoint checkpoint_epoch037.pt \\
13
+ --mix mix.wav \\
14
+ --enroll target_speaker.wav \\
15
+ --output result.wav
16
+
17
+ # Use GPU
18
+ python inference.py \\
19
+ --checkpoint checkpoint_epoch037.pt \\
20
+ --mix mix.wav \\
21
+ --enroll target.wav \\
22
+ --output result.wav \\
23
+ --device cuda
24
+
25
+ # Batch mode (process a directory of mixtures with one enrollment per file)
26
+ python inference.py \\
27
+ --checkpoint checkpoint_epoch037.pt \\
28
+ --mix-dir ./mixtures/ \\
29
+ --enroll-dir ./enrollments/ \\
30
+ --output-dir ./results/ \\
31
+ --device cuda
32
+
33
+ # List available CUDA devices
34
+ python inference.py --list-devices
35
+ """
36
+
37
+ import argparse
38
+ import os
39
+ import sys
40
+ from pathlib import Path
41
+ from typing import Optional, Tuple
42
+
43
+ import numpy as np
44
+ import torch
45
+ import torch.nn as nn
46
+ import torch.nn.functional as F
47
+ import torchaudio
48
+
49
+
50
+ # ============================================================================
51
+ # Helper: LinearLayer (used by SpeakerFuseLayer)
52
+ # ============================================================================
53
+
54
+ class LinearLayer(nn.Module):
55
+ """Simple linear layer with a dummy second argument for compatibility."""
56
+
57
+ def __init__(self, in_features, out_features, bias=True):
58
+ super().__init__()
59
+ self.linear = nn.Linear(in_features, out_features, bias)
60
+
61
+ def forward(self, x, dummy: Optional[torch.Tensor] = None):
62
+ return self.linear(x)
63
+
64
+
65
+ # ============================================================================
66
+ # Speaker helper modules
67
+ # ============================================================================
68
+
69
+ class PreEmphasis(nn.Module):
70
+ """Pre-emphasis filter: y(t) = x(t) - coef * x(t-1)."""
71
+
72
+ def __init__(self, coef: float = 0.97):
73
+ super().__init__()
74
+ self.coef = coef
75
+ self.register_buffer(
76
+ "flipped_filter",
77
+ torch.FloatTensor([-self.coef, 1.0]).unsqueeze(0).unsqueeze(0),
78
+ )
79
+
80
+ def forward(self, input: torch.Tensor) -> torch.Tensor:
81
+ input = input.unsqueeze(1)
82
+ input = F.pad(input, (1, 0), "reflect")
83
+ return F.conv1d(input, self.flipped_filter).squeeze(1)
84
+
85
+
86
+ class SpeakerTransform(nn.Module):
87
+ """Transform speaker embeddings through a series of 1x1 conv layers."""
88
+
89
+ def __init__(self, embed_dim=256, num_layers=3, hid_dim=128):
90
+ super().__init__()
91
+ layers = []
92
+ layers.append(nn.Conv1d(embed_dim, hid_dim, 1))
93
+ for _ in range(num_layers - 2):
94
+ layers.append(nn.Conv1d(hid_dim, hid_dim, 1))
95
+ layers.append(nn.Tanh())
96
+ layers.append(nn.Conv1d(hid_dim, embed_dim, 1))
97
+ self.transforms = nn.Sequential(*layers)
98
+
99
+ def forward(self, x):
100
+ if len(x.size()) == 2:
101
+ return self.transforms(x.unsqueeze(-1)).squeeze(-1)
102
+ return self.transforms(x)
103
+
104
+
105
+ class SpeakerFuseLayer(nn.Module):
106
+ """Fuse speaker embedding with audio features via various fusion strategies."""
107
+
108
+ def __init__(self, embed_dim=256, feat_dim=512, fuse_type="concat"):
109
+ super().__init__()
110
+ assert fuse_type in ["concat", "additive", "multiply", "FiLM", "None"]
111
+ self.fuse_type = fuse_type
112
+ if fuse_type == "concat":
113
+ self.fc = LinearLayer(embed_dim + feat_dim, feat_dim)
114
+ elif fuse_type in ("additive", "multiply"):
115
+ self.fc = LinearLayer(embed_dim, feat_dim)
116
+ elif fuse_type == "FiLM":
117
+ raise NotImplementedError("FiLM not supported in this standalone script")
118
+ else:
119
+ raise ValueError(f"Fuse type not defined: {fuse_type}")
120
+
121
+ def forward(self, x, embed):
122
+ if self.fuse_type == "concat":
123
+ if len(x.size()) == 3:
124
+ embed_t = embed.expand(-1, -1, x.size(2))
125
+ y = torch.cat([x, embed_t], 1)
126
+ y = torch.transpose(y, 1, 2)
127
+ x = torch.transpose(self.fc(y), 1, 2)
128
+ else:
129
+ embed_t = embed.expand(-1, x.size(1), -1, x.size(3))
130
+ y = torch.cat([x, embed_t], 2)
131
+ y = torch.transpose(y, 2, 3)
132
+ x = torch.transpose(self.fc(y), 2, 3).contiguous()
133
+ elif self.fuse_type == "additive":
134
+ if len(x.size()) == 3:
135
+ embed_t = embed.expand(-1, -1, x.size(2))
136
+ embed_t = torch.transpose(embed_t, 1, 2)
137
+ x = x + torch.transpose(self.fc(embed_t), 1, 2)
138
+ else:
139
+ embed_t = embed.expand(-1, x.size(1), -1, x.size(3))
140
+ embed_t = torch.transpose(embed_t, 2, 3)
141
+ x = x + torch.transpose(self.fc(embed_t), 2, 3)
142
+ elif self.fuse_type == "multiply":
143
+ if len(x.size()) == 3:
144
+ embed_t = embed.expand(-1, -1, x.size(2))
145
+ embed_t = torch.transpose(embed_t, 1, 2)
146
+ x = x * torch.transpose(self.fc(embed_t), 1, 2)
147
+ else:
148
+ embed_t = embed.expand(-1, x.size(1), -1, x.size(3))
149
+ embed_t = torch.transpose(embed_t, 2, 3)
150
+ x = x * torch.transpose(self.fc(embed_t), 2, 3)
151
+ else:
152
+ embed = embed.squeeze(-1)
153
+ x = self.fc(embed, x)
154
+ return x
155
+
156
+
157
+ # ============================================================================
158
+ # ECAPA-TDNN Speaker Encoder (for joint speaker embedding extraction)
159
+ # ============================================================================
160
+
161
+ class Conv1dReluBn(nn.Module):
162
+ """Conv1d + BatchNorm1d + ReLU."""
163
+
164
+ def __init__(self, in_channels, out_channels, kernel_size=1, stride=1,
165
+ padding=0, dilation=1, bias=True):
166
+ super().__init__()
167
+ self.conv = nn.Conv1d(in_channels, out_channels, kernel_size,
168
+ stride, padding, dilation, bias=bias)
169
+ self.bn = nn.BatchNorm1d(out_channels)
170
+
171
+ def forward(self, x):
172
+ return self.bn(F.relu(self.conv(x)))
173
+
174
+
175
+ class Res2Conv1dReluBn(nn.Module):
176
+ """Res2Conv1d + BatchNorm1d + ReLU."""
177
+
178
+ def __init__(self, channels, kernel_size=1, stride=1, padding=0,
179
+ dilation=1, bias=True, scale=4):
180
+ super().__init__()
181
+ assert channels % scale == 0, f"{channels} % {scale} != 0"
182
+ self.scale = scale
183
+ self.width = channels // scale
184
+ self.nums = scale if scale == 1 else scale - 1
185
+
186
+ self.convs = nn.ModuleList()
187
+ self.bns = nn.ModuleList()
188
+ for _ in range(self.nums):
189
+ self.convs.append(
190
+ nn.Conv1d(self.width, self.width, kernel_size,
191
+ stride, padding, dilation, bias=bias))
192
+ self.bns.append(nn.BatchNorm1d(self.width))
193
+
194
+ def forward(self, x):
195
+ out = []
196
+ spx = torch.split(x, self.width, 1)
197
+ sp = spx[0]
198
+ for i, (conv, bn) in enumerate(zip(self.convs, self.bns)):
199
+ if i >= 1:
200
+ sp = sp + spx[i]
201
+ sp = conv(sp)
202
+ sp = bn(F.relu(sp))
203
+ out.append(sp)
204
+ if self.scale != 1:
205
+ out.append(spx[self.nums])
206
+ return torch.cat(out, dim=1)
207
+
208
+
209
+ class SE_Connect(nn.Module):
210
+ """Squeeze-Excitation block for 1D."""
211
+
212
+ def __init__(self, channels, se_bottleneck_dim=128):
213
+ super().__init__()
214
+ self.linear1 = nn.Linear(channels, se_bottleneck_dim)
215
+ self.linear2 = nn.Linear(se_bottleneck_dim, channels)
216
+
217
+ def forward(self, x):
218
+ out = x.mean(dim=2)
219
+ out = F.relu(self.linear1(out))
220
+ out = torch.sigmoid(self.linear2(out))
221
+ return x * out.unsqueeze(2)
222
+
223
+
224
+ class SE_Res2Block(nn.Module):
225
+ """SE-Res2Block of the ECAPA-TDNN architecture."""
226
+
227
+ def __init__(self, channels, kernel_size, stride, padding, dilation, scale):
228
+ super().__init__()
229
+ self.se_res2block = nn.Sequential(
230
+ Conv1dReluBn(channels, channels, kernel_size=1, stride=1, padding=0),
231
+ Res2Conv1dReluBn(channels, kernel_size, stride, padding, dilation, scale=scale),
232
+ Conv1dReluBn(channels, channels, kernel_size=1, stride=1, padding=0),
233
+ SE_Connect(channels),
234
+ )
235
+
236
+ def forward(self, x):
237
+ return x + self.se_res2block(x)
238
+
239
+
240
+ class ASTP(nn.Module):
241
+ """Attentive statistics pooling: first used in ECAPA-TDNN."""
242
+
243
+ def __init__(self, in_dim, bottleneck_dim=128, global_context_att=False, **kwargs):
244
+ super().__init__()
245
+ self.in_dim = in_dim
246
+ self.global_context_att = global_context_att
247
+ if global_context_att:
248
+ self.linear1 = nn.Conv1d(in_dim * 3, bottleneck_dim, kernel_size=1)
249
+ else:
250
+ self.linear1 = nn.Conv1d(in_dim, bottleneck_dim, kernel_size=1)
251
+ self.linear2 = nn.Conv1d(bottleneck_dim, in_dim, kernel_size=1)
252
+
253
+ def forward(self, x):
254
+ if len(x.shape) == 4:
255
+ x = x.reshape(x.shape[0], x.shape[1] * x.shape[2], x.shape[3])
256
+ assert len(x.shape) == 3
257
+
258
+ if self.global_context_att:
259
+ context_mean = torch.mean(x, dim=-1, keepdim=True).expand_as(x)
260
+ context_std = torch.sqrt(torch.var(x, dim=-1, keepdim=True) + 1e-7).expand_as(x)
261
+ x_in = torch.cat((x, context_mean, context_std), dim=1)
262
+ else:
263
+ x_in = x
264
+
265
+ alpha = torch.tanh(self.linear1(x_in))
266
+ alpha = torch.softmax(self.linear2(alpha), dim=2)
267
+ mean = torch.sum(alpha * x, dim=2)
268
+ var = torch.sum(alpha * (x ** 2), dim=2) - mean ** 2
269
+ std = torch.sqrt(var.clamp(min=1e-7))
270
+ return torch.cat([mean, std], dim=1)
271
+
272
+ def get_out_dim(self):
273
+ return 2 * self.in_dim
274
+
275
+
276
+ class ECAPA_TDNN(nn.Module):
277
+ """ECAPA-TDNN speaker encoder."""
278
+
279
+ def __init__(self, channels=512, feat_dim=80, embed_dim=192,
280
+ pooling_func="ASTP", global_context_att=False, emb_bn=False):
281
+ super().__init__()
282
+ self.layer1 = Conv1dReluBn(feat_dim, channels, kernel_size=5, padding=2)
283
+ self.layer2 = SE_Res2Block(channels, kernel_size=3, stride=1, padding=2, dilation=2, scale=8)
284
+ self.layer3 = SE_Res2Block(channels, kernel_size=3, stride=1, padding=3, dilation=3, scale=8)
285
+ self.layer4 = SE_Res2Block(channels, kernel_size=3, stride=1, padding=4, dilation=4, scale=8)
286
+
287
+ cat_channels = channels * 3
288
+ out_channels = 512 * 3
289
+ self.conv = nn.Conv1d(cat_channels, out_channels, kernel_size=1)
290
+ self.pool = ASTP(in_dim=out_channels, global_context_att=global_context_att)
291
+ self.pool_out_dim = self.pool.get_out_dim()
292
+ self.bn = nn.BatchNorm1d(self.pool_out_dim)
293
+ self.linear = nn.Linear(self.pool_out_dim, embed_dim)
294
+ self.emb_bn = emb_bn
295
+ self.bn2 = nn.BatchNorm1d(embed_dim) if emb_bn else nn.Identity()
296
+
297
+ def forward(self, x):
298
+ x = x.permute(0, 2, 1) # (B, T, F) -> (B, F, T)
299
+ out1 = self.layer1(x)
300
+ out2 = self.layer2(out1)
301
+ out3 = self.layer3(out2)
302
+ out4 = self.layer4(out3)
303
+ out = torch.cat([out2, out3, out4], dim=1)
304
+ out = self.conv(out)
305
+ out = F.relu(out)
306
+ out = self.bn(self.pool(out))
307
+ out = self.linear(out)
308
+ if self.emb_bn:
309
+ out = self.bn2(out)
310
+ return out4, out # returns (frame_level, segment_level)
311
+
312
+
313
+ # ============================================================================
314
+ # BSRNN Legacy Model
315
+ # ============================================================================
316
+
317
+ class ResRNN(nn.Module):
318
+ """Residual LSTM with GroupNorm + projection."""
319
+
320
+ def __init__(self, input_size, hidden_size, bidirectional=True):
321
+ super().__init__()
322
+ self.input_size = input_size
323
+ self.hidden_size = hidden_size
324
+ self.eps = torch.finfo(torch.float32).eps
325
+ self.norm = nn.GroupNorm(1, input_size, self.eps)
326
+ self.rnn = nn.LSTM(input_size, hidden_size, 1, batch_first=True,
327
+ bidirectional=bidirectional)
328
+ self.proj = nn.Linear(hidden_size * 2, input_size)
329
+
330
+ def forward(self, input):
331
+ rnn_output, _ = self.rnn(self.norm(input).transpose(1, 2).contiguous())
332
+ rnn_output = self.proj(
333
+ rnn_output.contiguous().view(-1, rnn_output.shape[2])
334
+ ).view(input.shape[0], input.shape[2], input.shape[1])
335
+ return input + rnn_output.transpose(1, 2).contiguous()
336
+
337
+
338
+ class BSNet(nn.Module):
339
+ """Band-split network with intra-band and inter-band RNN."""
340
+
341
+ def __init__(self, in_channel, nband=7, bidirectional=True):
342
+ super().__init__()
343
+ self.nband = nband
344
+ self.feature_dim = in_channel // nband
345
+ self.band_rnn = ResRNN(self.feature_dim, self.feature_dim * 2,
346
+ bidirectional=bidirectional)
347
+ self.band_comm = ResRNN(self.feature_dim, self.feature_dim * 2,
348
+ bidirectional=bidirectional)
349
+
350
+ def forward(self, input, dummy: Optional[torch.Tensor] = None):
351
+ B, N, T = input.shape
352
+ band_output = self.band_rnn(
353
+ input.view(B * self.nband, self.feature_dim, -1)
354
+ ).view(B, self.nband, -1, T)
355
+ band_output = band_output.permute(0, 3, 2, 1).contiguous().view(
356
+ B * T, -1, self.nband)
357
+ output = self.band_comm(band_output).view(
358
+ B, T, -1, self.nband).permute(0, 3, 2, 1).contiguous()
359
+ return output.view(B, N, T)
360
+
361
+
362
+ class FuseSeparation(nn.Module):
363
+ """Separation module with speaker fusion at each repeat."""
364
+
365
+ def __init__(self, nband=7, num_repeat=6, feature_dim=128,
366
+ spk_emb_dim=256, spk_fuse_type="concat", multi_fuse=True):
367
+ super().__init__()
368
+ self.multi_fuse = multi_fuse
369
+ self.nband = nband
370
+ self.feature_dim = feature_dim
371
+ self.separation = nn.ModuleList([])
372
+ if self.multi_fuse:
373
+ for _ in range(num_repeat):
374
+ self.separation.append(
375
+ SpeakerFuseLayer(embed_dim=spk_emb_dim,
376
+ feat_dim=feature_dim,
377
+ fuse_type=spk_fuse_type))
378
+ self.separation.append(BSNet(nband * feature_dim, nband))
379
+ else:
380
+ self.separation.append(
381
+ SpeakerFuseLayer(embed_dim=spk_emb_dim,
382
+ feat_dim=feature_dim,
383
+ fuse_type=spk_fuse_type))
384
+ for _ in range(num_repeat):
385
+ self.separation.append(BSNet(nband * feature_dim, nband))
386
+
387
+ def forward(self, x, spk_embedding, nch: torch.Tensor = torch.tensor(1)):
388
+ batch_size = x.shape[0]
389
+ if self.multi_fuse:
390
+ for i, sep_func in enumerate(self.separation):
391
+ x = sep_func(x, spk_embedding)
392
+ if i % 2 == 0:
393
+ x = x.view(batch_size * nch,
394
+ self.nband * self.feature_dim, -1)
395
+ else:
396
+ x = x.view(batch_size * nch, self.nband,
397
+ self.feature_dim, -1)
398
+ else:
399
+ x = self.separation[0](x, spk_embedding)
400
+ x = x.view(batch_size * nch, self.nband * self.feature_dim, -1)
401
+ for idx, sep in enumerate(self.separation):
402
+ if idx > 0:
403
+ x = sep(x, spk_embedding)
404
+ x = x.view(batch_size * nch, self.nband, self.feature_dim, -1)
405
+ return x
406
+
407
+
408
+ class BSRNN(nn.Module):
409
+ """Legacy BSRNN with joint speaker encoder (flat-config format).
410
+
411
+ This is the exact model architecture used to train the PS4 checkpoint.
412
+ State dict keys: separator.separation.*, spk_model.layer*, mask.*, BN.*,
413
+ spk_encoder.*, preEmphasis.*
414
+ """
415
+
416
+ def __init__(
417
+ self,
418
+ spk_emb_dim=256,
419
+ sr=16000,
420
+ win=512,
421
+ stride=128,
422
+ feature_dim=128,
423
+ num_repeat=6,
424
+ use_spk_transform=True,
425
+ use_bidirectional=True,
426
+ spk_fuse_type="concat",
427
+ multi_fuse=True,
428
+ joint_training=True,
429
+ multi_task=False,
430
+ spksInTrain=251,
431
+ spk_model=None,
432
+ spk_model_init=None,
433
+ spk_model_freeze=False,
434
+ spk_args=None,
435
+ spk_feat=False,
436
+ feat_type="consistent",
437
+ ):
438
+ super().__init__()
439
+ self.sr = sr
440
+ self.win = win
441
+ self.stride = stride
442
+ self.group = self.win // 2
443
+ self.enc_dim = self.win // 2 + 1
444
+ self.feature_dim = feature_dim
445
+ self.eps = torch.finfo(torch.float32).eps
446
+ self.spk_emb_dim = spk_emb_dim
447
+ self.joint_training = joint_training
448
+ self.spk_feat = spk_feat
449
+ self.feat_type = feat_type
450
+ self.spk_model_freeze = spk_model_freeze
451
+ self.multi_task = multi_task
452
+
453
+ # Band split: 100Hz bins → 200Hz bins → 500Hz bins → 2kHz bins → rest
454
+ bandwidth_100 = int(np.floor(100 / (sr / 2.0) * self.enc_dim))
455
+ bandwidth_200 = int(np.floor(200 / (sr / 2.0) * self.enc_dim))
456
+ bandwidth_500 = int(np.floor(500 / (sr / 2.0) * self.enc_dim))
457
+ bandwidth_2k = int(np.floor(2000 / (sr / 2.0) * self.enc_dim))
458
+ self.band_width = [bandwidth_100] * 15
459
+ self.band_width += [bandwidth_200] * 10
460
+ self.band_width += [bandwidth_500] * 5
461
+ self.band_width += [bandwidth_2k] * 1
462
+ self.band_width.append(self.enc_dim - int(np.sum(self.band_width)))
463
+ self.nband = len(self.band_width)
464
+
465
+ # Speaker embedding transform
466
+ if use_spk_transform:
467
+ self.spk_transform = SpeakerTransform()
468
+ else:
469
+ self.spk_transform = nn.Identity()
470
+
471
+ # Joint speaker encoder
472
+ if joint_training:
473
+ spk_args = spk_args or {}
474
+ self.spk_model = ECAPA_TDNN_GLOB_c512(
475
+ feat_dim=spk_args.get("feat_dim", 80),
476
+ embed_dim=spk_args.get("embed_dim", 192),
477
+ pooling_func=spk_args.get("pooling_func", "ASTP"),
478
+ )
479
+ if spk_model_freeze:
480
+ for param in self.spk_model.parameters():
481
+ param.requires_grad = False
482
+ if not spk_feat:
483
+ if feat_type == "consistent":
484
+ self.preEmphasis = PreEmphasis()
485
+ self.spk_encoder = torchaudio.transforms.MelSpectrogram(
486
+ sample_rate=sr,
487
+ n_fft=win,
488
+ win_length=win,
489
+ hop_length=stride,
490
+ f_min=20,
491
+ window_fn=torch.hamming_window,
492
+ n_mels=spk_args.get("feat_dim", 80),
493
+ )
494
+ else:
495
+ self.preEmphasis = nn.Identity()
496
+ self.spk_encoder = nn.Identity()
497
+
498
+ if multi_task:
499
+ self.pred_linear = nn.Linear(spk_emb_dim, spksInTrain)
500
+ else:
501
+ self.pred_linear = nn.Identity()
502
+
503
+ # Band normalization
504
+ self.BN = nn.ModuleList([])
505
+ for i in range(self.nband):
506
+ self.BN.append(
507
+ nn.Sequential(
508
+ nn.GroupNorm(1, self.band_width[i] * 2, self.eps),
509
+ nn.Conv1d(self.band_width[i] * 2, self.feature_dim, 1),
510
+ )
511
+ )
512
+
513
+ # Separator
514
+ self.separator = FuseSeparation(
515
+ nband=self.nband,
516
+ num_repeat=num_repeat,
517
+ feature_dim=feature_dim,
518
+ spk_emb_dim=spk_emb_dim,
519
+ spk_fuse_type=spk_fuse_type,
520
+ multi_fuse=multi_fuse,
521
+ )
522
+
523
+ # Mask estimation
524
+ self.mask = nn.ModuleList([])
525
+ for i in range(self.nband):
526
+ self.mask.append(
527
+ nn.Sequential(
528
+ nn.GroupNorm(1, self.feature_dim, torch.finfo(torch.float32).eps),
529
+ nn.Conv1d(self.feature_dim, self.feature_dim * 4, 1),
530
+ nn.Tanh(),
531
+ nn.Conv1d(self.feature_dim * 4, self.feature_dim * 4, 1),
532
+ nn.Tanh(),
533
+ nn.Conv1d(self.feature_dim * 4, self.band_width[i] * 4, 1),
534
+ )
535
+ )
536
+
537
+ def train(self, mode: bool = True):
538
+ """Override train(): keep spk_model in eval mode when frozen."""
539
+ super().train(mode)
540
+ if self.spk_model_freeze and hasattr(self, "spk_model"):
541
+ self.spk_model.eval()
542
+ return self
543
+
544
+ def forward(self, input, embeddings):
545
+ """
546
+ Args:
547
+ input: (B, T) mixture waveform
548
+ embeddings: (B, T_enroll) enrollment waveform (will be processed
549
+ by the internal speaker encoder, or (B, D) pre-extracted
550
+ speaker embedding if spk_feat=True)
551
+ Returns:
552
+ s: (B, T) extracted target speaker waveform
553
+ _: dummy speaker label prediction (ignored at inference)
554
+ """
555
+ wav_input = input
556
+ spk_emb_input = embeddings
557
+ batch_size, nsample = wav_input.shape
558
+ nch = 1
559
+
560
+ # STFT
561
+ spec = torch.stft(
562
+ wav_input,
563
+ n_fft=self.win,
564
+ hop_length=self.stride,
565
+ window=torch.hann_window(self.win).to(wav_input.device).type(
566
+ wav_input.type()),
567
+ return_complex=True,
568
+ )
569
+ spec_RI = torch.stack([spec.real, spec.imag], 1)
570
+
571
+ # Band split
572
+ subband_spec = []
573
+ subband_mix_spec = []
574
+ band_idx = 0
575
+ for i in range(len(self.band_width)):
576
+ subband_spec.append(
577
+ spec_RI[:, :, band_idx:band_idx + self.band_width[i]].contiguous())
578
+ subband_mix_spec.append(
579
+ spec[:, band_idx:band_idx + self.band_width[i]])
580
+ band_idx += self.band_width[i]
581
+
582
+ # Band normalization
583
+ subband_feature = []
584
+ for i, bn_func in enumerate(self.BN):
585
+ subband_feature.append(
586
+ bn_func(subband_spec[i].view(batch_size * nch,
587
+ self.band_width[i] * 2, -1)))
588
+ subband_feature = torch.stack(subband_feature, 1)
589
+
590
+ predict_speaker_lable = torch.tensor(0.0).to(spk_emb_input.device)
591
+
592
+ # Joint speaker encoder
593
+ if self.joint_training:
594
+ if not self.spk_feat:
595
+ if self.feat_type == "consistent":
596
+ with torch.no_grad():
597
+ spk_emb_input = self.preEmphasis(spk_emb_input)
598
+ spk_emb_input = self.spk_encoder(spk_emb_input) + 1e-8
599
+ spk_emb_input = spk_emb_input.log()
600
+ spk_emb_input = spk_emb_input - torch.mean(
601
+ spk_emb_input, dim=-1, keepdim=True)
602
+ spk_emb_input = spk_emb_input.permute(0, 2, 1)
603
+
604
+ tmp_spk_emb_input = self.spk_model(spk_emb_input)
605
+ if isinstance(tmp_spk_emb_input, tuple):
606
+ spk_emb_input = tmp_spk_emb_input[-1]
607
+ else:
608
+ spk_emb_input = tmp_spk_emb_input
609
+ predict_speaker_lable = self.pred_linear(spk_emb_input)
610
+
611
+ spk_embedding = self.spk_transform(spk_emb_input)
612
+ spk_embedding = spk_embedding.unsqueeze(1).unsqueeze(3)
613
+
614
+ # Separation
615
+ sep_output = self.separator(subband_feature, spk_embedding,
616
+ torch.tensor(nch))
617
+
618
+ # Mask estimation and complex mask application
619
+ sep_subband_spec = []
620
+ for i, mask_func in enumerate(self.mask):
621
+ this_output = mask_func(sep_output[:, i]).view(
622
+ batch_size * nch, 2, 2, self.band_width[i], -1)
623
+ this_mask = this_output[:, 0] * torch.sigmoid(this_output[:, 1])
624
+ this_mask_real = this_mask[:, 0]
625
+ this_mask_imag = this_mask[:, 1]
626
+ est_spec_real = (subband_mix_spec[i].real * this_mask_real
627
+ - subband_mix_spec[i].imag * this_mask_imag)
628
+ est_spec_imag = (subband_mix_spec[i].real * this_mask_imag
629
+ + subband_mix_spec[i].imag * this_mask_real)
630
+ sep_subband_spec.append(
631
+ torch.complex(est_spec_real, est_spec_imag))
632
+
633
+ # iSTFT
634
+ est_spec = torch.cat(sep_subband_spec, 1)
635
+ output = torch.istft(
636
+ est_spec.view(batch_size * nch, self.enc_dim, -1),
637
+ n_fft=self.win,
638
+ hop_length=self.stride,
639
+ window=torch.hann_window(self.win).to(wav_input.device).type(
640
+ wav_input.type()),
641
+ length=nsample,
642
+ )
643
+ output = output.view(batch_size, nch, -1)
644
+ s = torch.squeeze(output, dim=1)
645
+ return s, predict_speaker_lable
646
+
647
+
648
+ def ECAPA_TDNN_GLOB_c512(feat_dim, embed_dim, pooling_func="ASTP", emb_bn=False):
649
+ """Factory function for ECAPA-TDNN with global context attention and 512 channels."""
650
+ return ECAPA_TDNN(
651
+ channels=512,
652
+ feat_dim=feat_dim,
653
+ embed_dim=embed_dim,
654
+ pooling_func=pooling_func,
655
+ global_context_att=True,
656
+ emb_bn=emb_bn,
657
+ )
658
+
659
+
660
+ # ============================================================================
661
+ # Checkpoint Loading
662
+ # ============================================================================
663
+
664
+ def build_model(device: torch.device) -> BSRNN:
665
+ """Build the PS4 BSRNN model with the exact training config parameters.
666
+
667
+ Returns:
668
+ BSRNN model in eval mode, moved to the specified device.
669
+ """
670
+ model = BSRNN(
671
+ feat_type="consistent",
672
+ feature_dim=128,
673
+ num_repeat=6,
674
+ spk_emb_dim=192,
675
+ spk_fuse_type="multiply",
676
+ multi_fuse=False,
677
+ spk_model="ECAPA_TDNN_GLOB_c512",
678
+ sr=16000,
679
+ win=512,
680
+ stride=128,
681
+ spk_args={"feat_dim": 80, "embed_dim": 192, "pooling_func": "ASTP"},
682
+ spk_model_freeze=True,
683
+ use_spk_transform=False,
684
+ joint_training=True,
685
+ multi_task=False,
686
+ spk_feat=False,
687
+ )
688
+ model.eval()
689
+ return model.to(device)
690
+
691
+
692
+ def load_checkpoint(path: str, model: nn.Module, device: torch.device):
693
+ """Load PS4 checkpoint weights into the model.
694
+
695
+ The checkpoint is saved by train.py as:
696
+ {"model": state_dict, "optimizer": ..., "scheduler": ..., ...}
697
+ """
698
+ print(f"[PS4] Loading checkpoint: {path}")
699
+ ckpt = torch.load(path, map_location=device)
700
+
701
+ # Handle various checkpoint formats
702
+ if isinstance(ckpt, dict) and "model" in ckpt:
703
+ state_dict = ckpt["model"]
704
+ elif isinstance(ckpt, dict) and "state_dict" in ckpt:
705
+ state_dict = ckpt["state_dict"]
706
+ else:
707
+ state_dict = ckpt
708
+
709
+ missing, unexpected = model.load_state_dict(state_dict, strict=False)
710
+ if missing:
711
+ print(f"[PS4] WARNING: missing keys ({len(missing)}): {missing[:5]}...")
712
+ if unexpected:
713
+ print(f"[PS4] WARNING: unexpected keys ({len(unexpected)}): {unexpected[:5]}...")
714
+ print(f"[PS4] Loaded successfully. "
715
+ f"Epoch: {ckpt.get('epoch', 'N/A')}, "
716
+ f"Step: {ckpt.get('step', 'N/A')}")
717
+ return model
718
+
719
+
720
+ # ============================================================================
721
+ # Inference
722
+ # ============================================================================
723
+
724
+ def load_audio(path: str, target_sr: int = 16000) -> torch.Tensor:
725
+ """Load audio at target sample rate.
726
+
727
+ Returns:
728
+ Tensor of shape (1, T) — mono, normalized to [-1, 1].
729
+ """
730
+ wav, sr = torchaudio.load(path)
731
+ if wav.size(0) > 1:
732
+ wav = wav.mean(dim=0, keepdim=True) # mono
733
+ if sr != target_sr:
734
+ wav = torchaudio.functional.resample(wav, sr, target_sr)
735
+ # Normalize
736
+ peak = wav.abs().max()
737
+ if peak > 0:
738
+ wav = wav / peak
739
+ return wav
740
+
741
+
742
+ def save_audio(path: str, wav: torch.Tensor, sr: int = 16000):
743
+ """Save audio tensor to file."""
744
+ torchaudio.save(path, wav.cpu(), sr)
745
+ print(f"[PS4] Saved: {path}")
746
+
747
+
748
+ def extract_speaker(
749
+ model: BSRNN,
750
+ mixture: torch.Tensor,
751
+ enrollment: torch.Tensor,
752
+ device: torch.device,
753
+ ) -> torch.Tensor:
754
+ """Run target speaker extraction.
755
+
756
+ Args:
757
+ model: Loaded BSRNN model.
758
+ mixture: (1, T_mix) mixture waveform, 16 kHz.
759
+ enrollment: (1, T_enroll) enrollment waveform, 16 kHz.
760
+ device: Computation device.
761
+
762
+ Returns:
763
+ (1, T_mix) extracted target speaker waveform.
764
+ """
765
+ with torch.no_grad():
766
+ mixture = mixture.to(device)
767
+ enrollment = enrollment.to(device)
768
+ extracted, _ = model(mixture, enrollment)
769
+ return extracted.cpu()
770
+
771
+
772
+ # ============================================================================
773
+ # CLI
774
+ # ============================================================================
775
+
776
+ def list_devices():
777
+ """Print available CUDA devices."""
778
+ print("Available devices:")
779
+ print(f" cpu")
780
+ if torch.cuda.is_available():
781
+ for i in range(torch.cuda.device_count()):
782
+ print(f" cuda:{i} {torch.cuda.get_device_name(i)}")
783
+ else:
784
+ print(" (no CUDA devices found)")
785
+
786
+
787
+ def main():
788
+ parser = argparse.ArgumentParser(
789
+ description="PS4 Target Speaker Extraction — Inference",
790
+ formatter_class=argparse.RawDescriptionHelpFormatter,
791
+ epilog="""
792
+ Examples:
793
+ # Single file
794
+ python inference.py --checkpoint checkpoint_epoch037.pt \\
795
+ --mix mix.wav --enroll target.wav --output result.wav
796
+
797
+ # Directory batch
798
+ python inference.py --checkpoint checkpoint_epoch037.pt \\
799
+ --mix-dir ./mixtures/ --enroll-dir ./enrollments/ --output-dir ./results/
800
+
801
+ # List devices
802
+ python inference.py --list-devices
803
+ """,
804
+ )
805
+ parser.add_argument("--checkpoint", type=str,
806
+ default="checkpoint_epoch037.pt",
807
+ help="Path to PS4 checkpoint (.pt)")
808
+ parser.add_argument("--mix", type=str, default=None,
809
+ help="Path to mixture audio (16 kHz mono WAV)")
810
+ parser.add_argument("--enroll", type=str, default=None,
811
+ help="Path to enrollment audio (16 kHz mono WAV)")
812
+ parser.add_argument("--output", type=str, default="output.wav",
813
+ help="Path to save extracted audio")
814
+ parser.add_argument("--mix-dir", type=str, default=None,
815
+ help="Directory of mixture audio files (batch mode)")
816
+ parser.add_argument("--enroll-dir", type=str, default=None,
817
+ help="Directory of enrollment audio files (batch mode, "
818
+ "must match mixture filenames)")
819
+ parser.add_argument("--output-dir", type=str, default=None,
820
+ help="Output directory (batch mode)")
821
+ parser.add_argument("--device", type=str, default="auto",
822
+ help="Device: 'auto', 'cpu', or 'cuda:N'")
823
+ parser.add_argument("--list-devices", action="store_true",
824
+ help="List available devices and exit")
825
+
826
+ args = parser.parse_args()
827
+
828
+ if args.list_devices:
829
+ list_devices()
830
+ return
831
+
832
+ # Device selection
833
+ if args.device == "auto":
834
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
835
+ else:
836
+ device = torch.device(args.device)
837
+ print(f"[PS4] Using device: {device}")
838
+
839
+ # Build model
840
+ print("[PS4] Building model...")
841
+ model = build_model(device)
842
+ load_checkpoint(args.checkpoint, model, device)
843
+ print(f"[PS4] Model parameters: {sum(p.numel() for p in model.parameters()):,}")
844
+
845
+ # Single file mode
846
+ if args.mix is not None and args.enroll is not None:
847
+ print(f"[PS4] Loading mixture: {args.mix}")
848
+ mix = load_audio(args.mix)
849
+ print(f"[PS4] Loading enrollment: {args.enroll}")
850
+ enroll = load_audio(args.enroll)
851
+ print(f"[PS4] Running extraction (mix: {mix.shape[-1]/16000:.1f}s, "
852
+ f"enroll: {enroll.shape[-1]/16000:.1f}s)...")
853
+ extracted = extract_speaker(model, mix, enroll, device)
854
+ save_audio(args.output, extracted)
855
+ return
856
+
857
+ # Batch mode
858
+ if args.mix_dir is not None and args.enroll_dir is not None and args.output_dir is not None:
859
+ mix_dir = Path(args.mix_dir)
860
+ enroll_dir = Path(args.enroll_dir)
861
+ output_dir = Path(args.output_dir)
862
+ output_dir.mkdir(parents=True, exist_ok=True)
863
+
864
+ mix_files = sorted(mix_dir.glob("*.wav"))
865
+ if not mix_files:
866
+ print(f"[PS4] No .wav files found in {mix_dir}")
867
+ return
868
+
869
+ print(f"[PS4] Batch mode: {len(mix_files)} files")
870
+ for mix_path in mix_files:
871
+ enroll_path = enroll_dir / mix_path.name
872
+ if not enroll_path.exists():
873
+ print(f"[PS4] Skipping {mix_path.name}: no matching enrollment")
874
+ continue
875
+ out_path = output_dir / mix_path.name
876
+ print(f"[PS4] Processing {mix_path.name}...", end=" ", flush=True)
877
+ mix = load_audio(str(mix_path))
878
+ enroll = load_audio(str(enroll_path))
879
+ extracted = extract_speaker(model, mix, enroll, device)
880
+ save_audio(str(out_path), extracted)
881
+ print("done")
882
+ return
883
+
884
+ # If neither mode is specified
885
+ parser.print_help()
886
+ print("\n[PS4] ERROR: Specify either --mix/--enroll (single) or "
887
+ "--mix-dir/--enroll-dir/--output-dir (batch).")
888
+
889
+
890
+ if __name__ == "__main__":
891
+ main()