Skip to content

Commit c396635

Browse files
committed
feat: add SHMC/SDP modules (estimators, shift_mouth_opening_utils, SDP)
1 parent c9793e1 commit c396635

7 files changed

Lines changed: 793 additions & 0 deletions

File tree

modules/estimators/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .inference import CurveEstimator

modules/estimators/inference.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import pathlib
2+
3+
import numpy as np
4+
import torch
5+
import torch.nn.functional as F
6+
import torchaudio.transforms
7+
import yaml
8+
from librosa.filters import mel as librosa_mel_fn
9+
10+
import utils
11+
from .nets import BiLSTMCurveEstimator
12+
13+
14+
class PitchAdjustableMelSpectrogram:
15+
def __init__(
16+
self,
17+
sample_rate=44100,
18+
n_fft=2048,
19+
win_length=2048,
20+
hop_length=512,
21+
f_min=40,
22+
f_max=16000,
23+
n_mels=128,
24+
center=False,
25+
):
26+
self.sample_rate = sample_rate
27+
self.n_fft = n_fft
28+
self.win_size = win_length
29+
self.hop_length = hop_length
30+
self.f_min = f_min
31+
self.f_max = f_max
32+
self.n_mels = n_mels
33+
self.center = center
34+
35+
self.mel_basis = {}
36+
self.hann_window = {}
37+
38+
def __call__(self, y, key_shift=0, speed=1.0):
39+
factor = 2 ** (key_shift / 12)
40+
n_fft_new = int(np.round(self.n_fft * factor))
41+
win_size_new = int(np.round(self.win_size * factor))
42+
hop_length = int(np.round(self.hop_length * speed))
43+
44+
mel_basis_key = f"{self.f_max}_{y.device}"
45+
if mel_basis_key not in self.mel_basis:
46+
mel = librosa_mel_fn(
47+
sr=self.sample_rate,
48+
n_fft=self.n_fft,
49+
n_mels=self.n_mels,
50+
fmin=self.f_min,
51+
fmax=self.f_max,
52+
)
53+
self.mel_basis[mel_basis_key] = torch.from_numpy(mel).float().to(y.device)
54+
55+
hann_window_key = f"{key_shift}_{y.device}"
56+
if hann_window_key not in self.hann_window:
57+
self.hann_window[hann_window_key] = torch.hann_window(
58+
win_size_new, device=y.device
59+
)
60+
61+
y = torch.nn.functional.pad(
62+
y.unsqueeze(1),
63+
(
64+
int((win_size_new - hop_length) // 2),
65+
int((win_size_new - hop_length+1) // 2),
66+
),
67+
mode="reflect",
68+
)
69+
y = y.squeeze(1)
70+
71+
spec = torch.stft(
72+
y,
73+
n_fft_new,
74+
hop_length=hop_length,
75+
win_length=win_size_new,
76+
window=self.hann_window[hann_window_key],
77+
center=self.center,
78+
pad_mode="reflect",
79+
normalized=False,
80+
onesided=True,
81+
return_complex=True,
82+
).abs()
83+
84+
if key_shift != 0:
85+
size = self.n_fft // 2 + 1
86+
resize = spec.size(1)
87+
if resize < size:
88+
spec = F.pad(spec, (0, 0, 0, size - resize))
89+
90+
spec = spec[:, :size, :] * self.win_size / win_size_new
91+
92+
spec = torch.matmul(self.mel_basis[mel_basis_key], spec)
93+
94+
return spec
95+
96+
97+
def dynamic_range_compression_torch(x, C=1, clip_val=1e-9):
98+
return torch.log(torch.clamp(x, min=clip_val) * C)
99+
100+
101+
class CurveEstimator:
102+
def __init__(self, model_path: pathlib.Path, device: str):
103+
if not isinstance(model_path, pathlib.Path):
104+
model_path = pathlib.Path(model_path)
105+
if not model_path.exists():
106+
raise FileNotFoundError(f"Curve estimator model path {model_path} does not exist.")
107+
config_path = model_path.with_name("config.yaml")
108+
if not config_path.exists():
109+
raise FileNotFoundError(f"Curve estimator config path {config_path} does not exist.")
110+
with open(config_path, "r", encoding="utf-8") as f:
111+
config = yaml.safe_load(f)
112+
dataset_args, model_args = config["dataset_args"], config["model_args"]
113+
self.device = device
114+
self.mel_spec_transform = PitchAdjustableMelSpectrogram(
115+
sample_rate=dataset_args["sample_rate"],
116+
n_fft=dataset_args["win_size"],
117+
win_length=dataset_args["win_size"],
118+
hop_length=dataset_args["hop_size"],
119+
f_min=dataset_args["f_min"],
120+
f_max=dataset_args["f_max"],
121+
n_mels=dataset_args["mel_bins"],
122+
center=True
123+
)
124+
self.model = BiLSTMCurveEstimator(
125+
**utils.filter_kwargs(model_args, BiLSTMCurveEstimator)
126+
)
127+
self.model.load_state_dict(torch.load(model_path, map_location="cpu"))
128+
self.model.eval()
129+
self.model.to(device)
130+
self.resample_kernels = {}
131+
132+
@torch.no_grad()
133+
def estimate(self, waveform: np.ndarray, sr: int, length: int) -> np.ndarray:
134+
waveform = torch.from_numpy(waveform).float().to(self.device).unsqueeze(0)
135+
if sr != self.mel_spec_transform.sample_rate:
136+
if sr not in self.resample_kernels:
137+
self.resample_kernels[sr] = torchaudio.transforms.Resample(
138+
orig_freq=sr,
139+
new_freq=self.mel_spec_transform.sample_rate,
140+
lowpass_filter_width=128
141+
).to(self.device)
142+
waveform = self.resample_kernels[sr](waveform)
143+
mel = dynamic_range_compression_torch(
144+
self.mel_spec_transform(waveform), clip_val=1e-5
145+
).transpose(1, 2)
146+
pred_curve = self.model.infer(mel).squeeze(0).cpu().numpy()
147+
pred_curve = np.interp(
148+
np.linspace(0, len(pred_curve), length),
149+
np.arange(len(pred_curve)),
150+
pred_curve
151+
).astype(np.float32)
152+
return pred_curve

modules/estimators/nets.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import torch
2+
import torch.nn as nn
3+
4+
5+
class BiLSTMCurveEstimator(nn.Module):
6+
"""
7+
BiLSTM-based Mel-spectrogram Prediction Encoderc in Curve Estimation
8+
9+
Args:
10+
in_dims (int): Number of input channels, should be same as the number of bins of mel-spectrogram.
11+
vmin (float): Minimum curve value.
12+
vmax (float): Maximum curve value.
13+
hidden_dims (int): Number of hidden dimensions.
14+
n_layers (int): Number of conformer layers.
15+
conv_dropout (float): Dropout rate of conv module, default 0.
16+
"""
17+
18+
def __init__(
19+
self,
20+
in_dims: int,
21+
vmin: float = 0.,
22+
vmax: float = 1.,
23+
conv_dims: int = 256,
24+
hidden_dims: int = 512,
25+
n_layers: int = 2,
26+
conv_dropout: float = 0.2,
27+
num_speakers: int = 50,
28+
):
29+
super().__init__()
30+
self.input_channels = in_dims
31+
self.hidden_dims = hidden_dims
32+
self.n_layers = n_layers
33+
self.vmin = vmin
34+
self.vmax = vmax
35+
36+
# Input stack, convert mel-spectrogram to hidden_dims
37+
self.input_stack = nn.Sequential(
38+
nn.Conv1d(in_dims, conv_dims, 3, 1, 1, bias=False),
39+
# nn.BatchNorm1d(hidden_dims),
40+
# nn.ReLU(),
41+
nn.GroupNorm(4, conv_dims),
42+
nn.LeakyReLU(),
43+
nn.Dropout(conv_dropout), # Rely on this dropout to construct samples
44+
nn.Conv1d(conv_dims, conv_dims, 3, 1, 1, bias=False)
45+
)
46+
# LSTM
47+
self.rnn = nn.LSTM(
48+
input_size=conv_dims,
49+
hidden_size=hidden_dims,
50+
num_layers=n_layers,
51+
bidirectional=True,
52+
batch_first=True
53+
)
54+
# Output
55+
self.output_proj = nn.Sequential(
56+
nn.Linear(hidden_dims * 2, hidden_dims),
57+
nn.ReLU(),
58+
# nn.Dropout(conv_dropout),
59+
nn.Linear(hidden_dims, 1),
60+
nn.Sigmoid()
61+
)
62+
63+
# post process
64+
# self.k_filter = nn.Parameter(torch.ones(num_speakers))
65+
self.p0 = (0.0 - self.vmin) / (self.vmax - self.vmin)
66+
self.k_filter = nn.Embedding(num_speakers, 1)
67+
nn.init.constant_(self.k_filter.weight, 0.0)
68+
69+
def forward(self, x: torch.Tensor, spk_id: torch.Tensor=None) -> torch.Tensor:
70+
"""
71+
Args:
72+
x (torch.Tensor): Input mel-spectrogram, shape (B, T, input_channels) or (B, T, mel_bins).
73+
return:
74+
torch.Tensor: Predicted curve, shape (B, T).
75+
"""
76+
self.rnn.flatten_parameters()
77+
x = self.input_stack(x.transpose(-1, -2)).transpose(-1, -2)
78+
x, _ = self.rnn(x)
79+
x = self.output_proj(x)
80+
if spk_id is not None:
81+
k_filter = self.k_filter(spk_id).view(-1,1,1)
82+
k_filter = 0.5 + 1.5 * torch.sigmoid(k_filter)
83+
k_filter = torch.clamp(k_filter, min=0.3, max=2.5)
84+
x = (x - self.p0) * k_filter + self.p0
85+
return x.squeeze(-1) # normalized curve (B, T)
86+
87+
def normalize(self, x: torch.Tensor) -> torch.Tensor:
88+
"""
89+
Args:
90+
x (torch.Tensor): Input curve, shape (B, T).
91+
return:
92+
torch.Tensor: Normalized curve, shape (B, T).
93+
"""
94+
x = (x - self.vmin) / (self.vmax - self.vmin)
95+
x = x.clamp(0., 1.)
96+
return x
97+
98+
def denormalize(self, x: torch.Tensor) -> torch.Tensor:
99+
"""
100+
Args:
101+
x (torch.Tensor): Input normalized curve, shape (B, T).
102+
return:
103+
torch.Tensor: Curve, shape (B, T).
104+
"""
105+
x = x * (self.vmax - self.vmin) + self.vmin
106+
return x
107+
108+
def infer(self, x: torch.Tensor) -> torch.Tensor:
109+
x = self.forward(x)
110+
curve = self.denormalize(x)
111+
return curve

modules/sdp/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .sdp import StochasticDurationPredictor
2+
from .transforms import piecewise_rational_quadratic_transform

0 commit comments

Comments
 (0)