From 2aa62f39e5daec1f302bfd35549a1ad90aa13569 Mon Sep 17 00:00:00 2001 From: MaxFish <16432329+MaxMax2016@users.noreply.github.com> Date: Mon, 21 Aug 2023 14:48:55 +0800 Subject: [PATCH] 20230821 --- README.md | 198 +++++++ assets/grad_svc_loss.jpg | Bin 0 -> 111461 bytes assets/grad_svc_mel.jpg | Bin 0 -> 175954 bytes bigvgan/LICENSE | 21 + bigvgan/README.md | 138 +++++ bigvgan/configs/nsf_bigvgan.yaml | 60 ++ bigvgan/model/__init__.py | 1 + bigvgan/model/alias/__init__.py | 6 + bigvgan/model/alias/act.py | 129 ++++ bigvgan/model/alias/filter.py | 95 +++ bigvgan/model/alias/resample.py | 49 ++ bigvgan/model/bigv.py | 64 ++ bigvgan/model/generator.py | 143 +++++ bigvgan/model/nsf.py | 394 ++++++++++++ bigvgan_pretrain/README.md | 5 + configs/base.yaml | 40 ++ grad/LICENSE | 19 + grad/__init__.py | 0 grad/base.py | 29 + grad/diffusion.py | 273 +++++++++ grad/encoder.py | 300 ++++++++++ grad/model.py | 125 ++++ grad/utils.py | 99 ++++ grad_extend/data.py | 133 +++++ grad_extend/train.py | 163 +++++ grad_extend/utils.py | 73 +++ grad_pretrain/README.md | 3 + gvc_export.py | 48 ++ gvc_inference.py | 153 +++++ gvc_inference_wave.py | 71 +++ gvc_trainer.py | 30 + hubert/__init__.py | 0 hubert/hubert_model.py | 229 +++++++ hubert/inference.py | 67 +++ hubert_pretrain/README.md | 3 + pitch/__init__.py | 1 + pitch/inference.py | 54 ++ prepare/preprocess_a.py | 58 ++ prepare/preprocess_f0.py | 64 ++ prepare/preprocess_hubert.py | 58 ++ prepare/preprocess_random.py | 23 + prepare/preprocess_speaker.py | 103 ++++ prepare/preprocess_speaker_ave.py | 54 ++ prepare/preprocess_spec.py | 52 ++ prepare/preprocess_train.py | 56 ++ prepare/preprocess_zzz.py | 30 + requirements.txt | 10 + speaker/__init__.py | 0 speaker/config.py | 64 ++ speaker/infer.py | 108 ++++ speaker/models/__init__.py | 0 speaker/models/lstm.py | 131 ++++ speaker/models/resnet.py | 212 +++++++ speaker/utils/__init__.py | 0 speaker/utils/audio.py | 822 +++++++++++++++++++++++++ speaker/utils/coqpit.py | 954 ++++++++++++++++++++++++++++++ speaker/utils/io.py | 198 +++++++ speaker/utils/shared_configs.py | 342 +++++++++++ speaker_pretrain/README.md | 5 + speaker_pretrain/config.json | 104 ++++ spec/inference.py | 113 ++++ 61 files changed, 6777 insertions(+) create mode 100644 README.md create mode 100644 assets/grad_svc_loss.jpg create mode 100644 assets/grad_svc_mel.jpg create mode 100644 bigvgan/LICENSE create mode 100644 bigvgan/README.md create mode 100644 bigvgan/configs/nsf_bigvgan.yaml create mode 100644 bigvgan/model/__init__.py create mode 100644 bigvgan/model/alias/__init__.py create mode 100644 bigvgan/model/alias/act.py create mode 100644 bigvgan/model/alias/filter.py create mode 100644 bigvgan/model/alias/resample.py create mode 100644 bigvgan/model/bigv.py create mode 100644 bigvgan/model/generator.py create mode 100644 bigvgan/model/nsf.py create mode 100644 bigvgan_pretrain/README.md create mode 100644 configs/base.yaml create mode 100644 grad/LICENSE create mode 100644 grad/__init__.py create mode 100644 grad/base.py create mode 100644 grad/diffusion.py create mode 100644 grad/encoder.py create mode 100644 grad/model.py create mode 100644 grad/utils.py create mode 100644 grad_extend/data.py create mode 100644 grad_extend/train.py create mode 100644 grad_extend/utils.py create mode 100644 grad_pretrain/README.md create mode 100644 gvc_export.py create mode 100644 gvc_inference.py create mode 100644 gvc_inference_wave.py create mode 100644 gvc_trainer.py create mode 100644 hubert/__init__.py create mode 100644 hubert/hubert_model.py create mode 100644 hubert/inference.py create mode 100644 hubert_pretrain/README.md create mode 100644 pitch/__init__.py create mode 100644 pitch/inference.py create mode 100644 prepare/preprocess_a.py create mode 100644 prepare/preprocess_f0.py create mode 100644 prepare/preprocess_hubert.py create mode 100644 prepare/preprocess_random.py create mode 100644 prepare/preprocess_speaker.py create mode 100644 prepare/preprocess_speaker_ave.py create mode 100644 prepare/preprocess_spec.py create mode 100644 prepare/preprocess_train.py create mode 100644 prepare/preprocess_zzz.py create mode 100644 requirements.txt create mode 100644 speaker/__init__.py create mode 100644 speaker/config.py create mode 100644 speaker/infer.py create mode 100644 speaker/models/__init__.py create mode 100644 speaker/models/lstm.py create mode 100644 speaker/models/resnet.py create mode 100644 speaker/utils/__init__.py create mode 100644 speaker/utils/audio.py create mode 100644 speaker/utils/coqpit.py create mode 100644 speaker/utils/io.py create mode 100644 speaker/utils/shared_configs.py create mode 100644 speaker_pretrain/README.md create mode 100644 speaker_pretrain/config.json create mode 100644 spec/inference.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..160ccf9 --- /dev/null +++ b/README.md @@ -0,0 +1,198 @@ +
+

Grad-SVC based Grad-TTS from HUAWEI Noah's Ark Lab

+ +This project is named as Grad-SVC, or GVC for short. Its core technology is diffusion, but so different from other diffusion based SVC models. Codes are adapted from Grad-TTS and so-vits-svc-5.0. So the features from so-vits-svc-5.0 will be used in this project. + +The project will be completed in the coming months ~~~ +
+ +## Setup Environment +1. Install project dependencies + + ```shell + pip install -r requirements.txt + ``` + +2. Download the Timbre Encoder: [Speaker-Encoder by @mueller91](https://drive.google.com/drive/folders/15oeBYf6Qn1edONkVLXe82MzdIi3O_9m3), put `best_model.pth.tar` into `speaker_pretrain/`. + +3. Download [hubert_soft model](https://github.com/bshall/hubert/releases/tag/v0.1),put `hubert-soft-0d54a1f4.pt` into `hubert_pretrain/`. + +4. Download pretrained [nsf_bigvgan_pretrain_32K.pth](https://github.com/PlayVoice/NSF-BigVGAN/releases/augment), and put it into `bigvgan_pretrain/`. + +5. Download pretrain model [gvc.pretrain.pth](), and put it into `grad_pretrain/`. + ```shell + python gvc_inference.py --config configs/base.yaml --model ./grad_pretrain/gvc.pretrain.pth --spk ./configs/singers/singer0001.npy --wave test.wav + ``` + +## Dataset preparation +Put the dataset into the `data_raw` directory following the structure below. +``` +data_raw +├───speaker0 +│ ├───000001.wav +│ ├───... +│ └───000xxx.wav +└───speaker1 + ├───000001.wav + ├───... + └───000xxx.wav +``` + +## Data preprocessing +After preprocessing you will get an output with following structure. +``` +data_gvc/ +└── waves-16k +│ └── speaker0 +│ │ ├── 000001.wav +│ │ └── 000xxx.wav +│ └── speaker1 +│ ├── 000001.wav +│ └── 000xxx.wav +└── waves-32k +│ └── speaker0 +│ │ ├── 000001.wav +│ │ └── 000xxx.wav +│ └── speaker1 +│ ├── 000001.wav +│ └── 000xxx.wav +└── mel +│ └── speaker0 +│ │ ├── 000001.mel.pt +│ │ └── 000xxx.mel.pt +│ └── speaker1 +│ ├── 000001.mel.pt +│ └── 000xxx.mel.pt +└── pitch +│ └── speaker0 +│ │ ├── 000001.pit.npy +│ │ └── 000xxx.pit.npy +│ └── speaker1 +│ ├── 000001.pit.npy +│ └── 000xxx.pit.npy +└── hubert +│ └── speaker0 +│ │ ├── 000001.vec.npy +│ │ └── 000xxx.vec.npy +│ └── speaker1 +│ ├── 000001.vec.npy +│ └── 000xxx.vec.npy +└── speaker +│ └── speaker0 +│ │ ├── 000001.spk.npy +│ │ └── 000xxx.spk.npy +│ └── speaker1 +│ ├── 000001.spk.npy +│ └── 000xxx.spk.npy +└── singer + ├── speaker0.spk.npy + └── speaker1.spk.npy +``` + +1. Re-sampling + - Generate audio with a sampling rate of 16000Hz in `./data_gvc/waves-16k` + ``` + python prepare/preprocess_a.py -w ./data_raw -o ./data_gvc/waves-16k -s 16000 + ``` + + - Generate audio with a sampling rate of 32000Hz in `./data_gvc/waves-32k` + ``` + python prepare/preprocess_a.py -w ./data_raw -o ./data_gvc/waves-32k -s 32000 + ``` +2. Use 16K audio to extract pitch + ``` + python prepare/preprocess_f0.py -w data_gvc/waves-16k/ -p data_gvc/pitch + ``` +3. use 32k audio to extract mel + ``` + python prepare/preprocess_spec.py -w data_gvc/waves-32k/ -s data_gvc/mel + ``` +4. Use 16K audio to extract hubert + ``` + python prepare/preprocess_hubert.py -w data_gvc/waves-16k/ -v data_gvc/hubert + ``` +5. Use 16k audio to extract timbre code + ``` + python prepare/preprocess_speaker.py data_gvc/waves-16k/ data_gvc/speaker + ``` +6. Extract the average value of the timbre code for inference + ``` + python prepare/preprocess_speaker_ave.py data_gvc/speaker/ data_gvc/singer + ``` +8. Use 32k audio to generate training index + ``` + python prepare/preprocess_train.py + ``` +9. Training file debugging + ``` + python prepare/preprocess_zzz.py + ``` + +## Train +1. Start training + ``` + python gvc_trainer.py + ``` +2. Resume training + ``` + python gvc_trainer.py -p logs/grad_svc/grad_svc_***.pth + ``` +3. Log visualization + ``` + tensorboard --logdir logs/ + ``` + +## Loss +![grad_svc_loss](./assets/grad_svc_loss.jpg) + +![grad_svc_mel](./assets/grad_svc_mel.jpg) + +## Inference + +1. Export inference model + ``` + python gvc_export.py --checkpoint_path logs/grad_svc/grad_svc_***.pt + ``` + +2. Inference + - if there is no need to adjust `f0`, just run the following command. + ``` + python gvc_inference.py --model gvc.pth --spk ./data_gvc/singer/your_singer.spk.npy --wave test.wav --shift 0 + ``` + - if `f0` will be adjusted manually, follow the steps: + + 1. use hubert to extract content vector + ``` + python hubert/inference.py -w test.wav -v test.vec.npy + ``` + 2. extract the F0 parameter to the csv text format + ``` + python pitch/inference.py -w test.wav -p test.csv + ``` + 3. final inference + ``` + python gvc_inference.py --model gvc.pth --spk ./data_gvc/singer/your_singer.spk.npy --wave test.wav --vec test.vec.npy --pit test.csv --shift 0 + ``` + +3. Convert mel to wave + ``` + python gvc_inference_wave.py --mel gvc_out.mel.pt --pit gvc_tmp.pit.csv + ``` + +## Code sources and references + +https://github.com/huawei-noah/Speech-Backbones/blob/main/Grad-TTS + +https://github.com/facebookresearch/speech-resynthesis [paper](https://arxiv.org/abs/2104.00355) + +https://github.com/jaywalnut310/vits [paper](https://arxiv.org/abs/2106.06103) + +https://github.com/NVIDIA/BigVGAN [paper](https://arxiv.org/abs/2206.04658) + +https://github.com/mindslab-ai/univnet [paper](https://arxiv.org/abs/2106.07889) + +https://github.com/mozilla/TTS + +https://github.com/bshall/soft-vc + +https://github.com/maxrmorrison/torchcrepe diff --git a/assets/grad_svc_loss.jpg b/assets/grad_svc_loss.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ceea76cc8005ca6270988edae7ce1219d92a232f GIT binary patch literal 111461 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 literal 0 HcmV?d00001 diff --git a/assets/grad_svc_mel.jpg b/assets/grad_svc_mel.jpg new file mode 100644 index 0000000000000000000000000000000000000000..476a9a640fa8c73b850af2d5bd0edec2ef64e272 GIT binary patch literal 175954 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 +!wN$GdyCe_W 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 z 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 literal 0 HcmV?d00001 diff --git a/bigvgan/LICENSE b/bigvgan/LICENSE new file mode 100644 index 0000000..328ed6e --- /dev/null +++ b/bigvgan/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 PlayVoice + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bigvgan/README.md b/bigvgan/README.md new file mode 100644 index 0000000..7816c1e --- /dev/null +++ b/bigvgan/README.md @@ -0,0 +1,138 @@ +

    +

    Neural Source-Filter BigVGAN

    + Just For Fun +
    + +![nsf_bigvgan_mel](https://github.com/PlayVoice/NSF-BigVGAN/assets/16432329/eebb8dca-a8d3-4e69-b02c-632a3a1cdd6a) + +## Dataset preparation + +Put the dataset into the data_raw directory according to the following file structure +```shell +data_raw +├───speaker0 +│ ├───000001.wav +│ ├───... +│ └───000xxx.wav +└───speaker1 + ├───000001.wav + ├───... + └───000xxx.wav +``` + +## Install dependencies + +- 1 software dependency + + > pip install -r requirements.txt + +- 2 download [release](https://github.com/PlayVoice/NSF-BigVGAN/releases/tag/debug) model, and test + + > python nsf_bigvgan_inference.py --config configs/nsf_bigvgan.yaml --model nsf_bigvgan_g.pth --wave test.wav + +## Data preprocessing + +- 1, re-sampling: 32kHz + + > python prepare/preprocess_a.py -w ./data_raw -o ./data_bigvgan/waves-32k + +- 3, extract pitch + + > python prepare/preprocess_f0.py -w data_bigvgan/waves-32k/ -p data_bigvgan/pitch + +- 4, extract mel: [100, length] + + > python prepare/preprocess_spec.py -w data_bigvgan/waves-32k/ -s data_bigvgan/mel + +- 5, generate training index + + > python prepare/preprocess_train.py + +```shell +data_bigvgan/ +│ +└── waves-32k +│ └── speaker0 +│ │ ├── 000001.wav +│ │ └── 000xxx.wav +│ └── speaker1 +│ ├── 000001.wav +│ └── 000xxx.wav +└── pitch +│ └── speaker0 +│ │ ├── 000001.pit.npy +│ │ └── 000xxx.pit.npy +│ └── speaker1 +│ ├── 000001.pit.npy +│ └── 000xxx.pit.npy +└── mel + └── speaker0 + │ ├── 000001.mel.pt + │ └── 000xxx.mel.pt + └── speaker1 + ├── 000001.mel.pt + └── 000xxx.mel.pt + +``` + +## Train + +- 1, start training + + > python nsf_bigvgan_trainer.py -c configs/nsf_bigvgan.yaml -n nsf_bigvgan + +- 2, resume training + + > python nsf_bigvgan_trainer.py -c configs/nsf_bigvgan.yaml -n nsf_bigvgan -p chkpt/nsf_bigvgan/***.pth + +- 3, view log + + > tensorboard --logdir logs/ + + +## Inference + +- 1, export inference model + + > python nsf_bigvgan_export.py --config configs/maxgan.yaml --checkpoint_path chkpt/nsf_bigvgan/***.pt + +- 2, extract mel + + > python spec/inference.py -w test.wav -m test.mel.pt + +- 3, extract F0 + + > python pitch/inference.py -w test.wav -p test.csv + +- 4, infer + + > python nsf_bigvgan_inference.py --config configs/nsf_bigvgan.yaml --model nsf_bigvgan_g.pth --wave test.wav + + or + + > python nsf_bigvgan_inference.py --config configs/nsf_bigvgan.yaml --model nsf_bigvgan_g.pth --mel test.mel.pt --pit test.csv + +## Augmentation of mel +For the over smooth output of acoustic model, we use gaussian blur for mel when train vocoder +``` +# gaussian blur +model_b = get_gaussian_kernel(kernel_size=5, sigma=2, channels=1).to(device) +# mel blur +mel_b = mel[:, None, :, :] +mel_b = model_b(mel_b) +mel_b = torch.squeeze(mel_b, 1) +mel_r = torch.rand(1).to(device) * 0.5 +mel_b = (1 - mel_r) * mel_b + mel_r * mel +# generator +optim_g.zero_grad() +fake_audio = model_g(mel_b, pit) +``` +![mel_gaussian_blur](https://github.com/PlayVoice/NSF-BigVGAN/assets/16432329/7fa96ef7-5e3b-4ae6-bc61-9b6da3b9d0b9) + +## Source of code and References + +https://github.com/nii-yamagishilab/project-NN-Pytorch-scripts/tree/master/project/01-nsf + +https://github.com/mindslab-ai/univnet [[paper]](https://arxiv.org/abs/2106.07889) + +https://github.com/NVIDIA/BigVGAN [[paper]](https://arxiv.org/abs/2206.04658) \ No newline at end of file diff --git a/bigvgan/configs/nsf_bigvgan.yaml b/bigvgan/configs/nsf_bigvgan.yaml new file mode 100644 index 0000000..809b129 --- /dev/null +++ b/bigvgan/configs/nsf_bigvgan.yaml @@ -0,0 +1,60 @@ +data: + train_file: 'files/train.txt' + val_file: 'files/valid.txt' +############################# +train: + num_workers: 4 + batch_size: 8 + optimizer: 'adam' + seed: 1234 + adam: + lr: 0.0002 + beta1: 0.8 + beta2: 0.99 + mel_lamb: 5 + stft_lamb: 2.5 + pretrain: '' + lora: False +############################# +audio: + n_mel_channels: 100 + segment_length: 12800 # Should be multiple of 320 + filter_length: 1024 + hop_length: 320 # WARNING: this can't be changed. + win_length: 1024 + sampling_rate: 32000 + mel_fmin: 40.0 + mel_fmax: 16000.0 +############################# +gen: + mel_channels: 100 + upsample_rates: [5,4,2,2,2,2] + upsample_kernel_sizes: [15,8,4,4,4,4] + upsample_initial_channel: 320 + resblock_kernel_sizes: [3,7,11] + resblock_dilation_sizes: [[1,3,5], [1,3,5], [1,3,5]] +############################# +mpd: + periods: [2,3,5,7,11] + kernel_size: 5 + stride: 3 + use_spectral_norm: False + lReLU_slope: 0.2 +############################# +mrd: + resolutions: "[(1024, 120, 600), (2048, 240, 1200), (4096, 480, 2400), (512, 50, 240)]" # (filter_length, hop_length, win_length) + use_spectral_norm: False + lReLU_slope: 0.2 +############################# +dist_config: + dist_backend: "nccl" + dist_url: "tcp://localhost:54321" + world_size: 1 +############################# +log: + info_interval: 100 + eval_interval: 1000 + save_interval: 10000 + num_audio: 6 + pth_dir: 'chkpt' + log_dir: 'logs' diff --git a/bigvgan/model/__init__.py b/bigvgan/model/__init__.py new file mode 100644 index 0000000..986a0cf --- /dev/null +++ b/bigvgan/model/__init__.py @@ -0,0 +1 @@ +from .alias.act import SnakeAlias \ No newline at end of file diff --git a/bigvgan/model/alias/__init__.py b/bigvgan/model/alias/__init__.py new file mode 100644 index 0000000..a2318b6 --- /dev/null +++ b/bigvgan/model/alias/__init__.py @@ -0,0 +1,6 @@ +# Adapted from https://github.com/junjun3518/alias-free-torch under the Apache License 2.0 +# LICENSE is in incl_licenses directory. + +from .filter import * +from .resample import * +from .act import * \ No newline at end of file diff --git a/bigvgan/model/alias/act.py b/bigvgan/model/alias/act.py new file mode 100644 index 0000000..308344f --- /dev/null +++ b/bigvgan/model/alias/act.py @@ -0,0 +1,129 @@ +# Adapted from https://github.com/junjun3518/alias-free-torch under the Apache License 2.0 +# LICENSE is in incl_licenses directory. + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from torch import sin, pow +from torch.nn import Parameter +from .resample import UpSample1d, DownSample1d + + +class Activation1d(nn.Module): + def __init__(self, + activation, + up_ratio: int = 2, + down_ratio: int = 2, + up_kernel_size: int = 12, + down_kernel_size: int = 12): + super().__init__() + self.up_ratio = up_ratio + self.down_ratio = down_ratio + self.act = activation + self.upsample = UpSample1d(up_ratio, up_kernel_size) + self.downsample = DownSample1d(down_ratio, down_kernel_size) + + # x: [B,C,T] + def forward(self, x): + x = self.upsample(x) + x = self.act(x) + x = self.downsample(x) + + return x + + +class SnakeBeta(nn.Module): + ''' + A modified Snake function which uses separate parameters for the magnitude of the periodic components + Shape: + - Input: (B, C, T) + - Output: (B, C, T), same shape as the input + Parameters: + - alpha - trainable parameter that controls frequency + - beta - trainable parameter that controls magnitude + References: + - This activation function is a modified version based on this paper by Liu Ziyin, Tilman Hartwig, Masahito Ueda: + https://arxiv.org/abs/2006.08195 + Examples: + >>> a1 = snakebeta(256) + >>> x = torch.randn(256) + >>> x = a1(x) + ''' + + def __init__(self, in_features, alpha=1.0, alpha_trainable=True, alpha_logscale=False): + ''' + Initialization. + INPUT: + - in_features: shape of the input + - alpha - trainable parameter that controls frequency + - beta - trainable parameter that controls magnitude + alpha is initialized to 1 by default, higher values = higher-frequency. + beta is initialized to 1 by default, higher values = higher-magnitude. + alpha will be trained along with the rest of your model. + ''' + super(SnakeBeta, self).__init__() + self.in_features = in_features + # initialize alpha + self.alpha_logscale = alpha_logscale + if self.alpha_logscale: # log scale alphas initialized to zeros + self.alpha = Parameter(torch.zeros(in_features) * alpha) + self.beta = Parameter(torch.zeros(in_features) * alpha) + else: # linear scale alphas initialized to ones + self.alpha = Parameter(torch.ones(in_features) * alpha) + self.beta = Parameter(torch.ones(in_features) * alpha) + self.alpha.requires_grad = alpha_trainable + self.beta.requires_grad = alpha_trainable + self.no_div_by_zero = 0.000000001 + + def forward(self, x): + ''' + Forward pass of the function. + Applies the function to the input elementwise. + SnakeBeta = x + 1/b * sin^2 (xa) + ''' + alpha = self.alpha.unsqueeze( + 0).unsqueeze(-1) # line up with x to [B, C, T] + beta = self.beta.unsqueeze(0).unsqueeze(-1) + if self.alpha_logscale: + alpha = torch.exp(alpha) + beta = torch.exp(beta) + x = x + (1.0 / (beta + self.no_div_by_zero)) * pow(sin(x * alpha), 2) + return x + + +class Mish(nn.Module): + """ + Mish activation function is proposed in "Mish: A Self + Regularized Non-Monotonic Neural Activation Function" + paper, https://arxiv.org/abs/1908.08681. + """ + + def __init__(self): + super().__init__() + + def forward(self, x): + return x * torch.tanh(F.softplus(x)) + + +class SnakeAlias(nn.Module): + def __init__(self, + channels, + up_ratio: int = 2, + down_ratio: int = 2, + up_kernel_size: int = 12, + down_kernel_size: int = 12): + super().__init__() + self.up_ratio = up_ratio + self.down_ratio = down_ratio + self.act = SnakeBeta(channels, alpha_logscale=True) + self.upsample = UpSample1d(up_ratio, up_kernel_size) + self.downsample = DownSample1d(down_ratio, down_kernel_size) + + # x: [B,C,T] + def forward(self, x): + x = self.upsample(x) + x = self.act(x) + x = self.downsample(x) + + return x \ No newline at end of file diff --git a/bigvgan/model/alias/filter.py b/bigvgan/model/alias/filter.py new file mode 100644 index 0000000..7ad6ea8 --- /dev/null +++ b/bigvgan/model/alias/filter.py @@ -0,0 +1,95 @@ +# Adapted from https://github.com/junjun3518/alias-free-torch under the Apache License 2.0 +# LICENSE is in incl_licenses directory. + +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +if 'sinc' in dir(torch): + sinc = torch.sinc +else: + # This code is adopted from adefossez's julius.core.sinc under the MIT License + # https://adefossez.github.io/julius/julius/core.html + # LICENSE is in incl_licenses directory. + def sinc(x: torch.Tensor): + """ + Implementation of sinc, i.e. sin(pi * x) / (pi * x) + __Warning__: Different to julius.sinc, the input is multiplied by `pi`! + """ + return torch.where(x == 0, + torch.tensor(1., device=x.device, dtype=x.dtype), + torch.sin(math.pi * x) / math.pi / x) + + +# This code is adopted from adefossez's julius.lowpass.LowPassFilters under the MIT License +# https://adefossez.github.io/julius/julius/lowpass.html +# LICENSE is in incl_licenses directory. +def kaiser_sinc_filter1d(cutoff, half_width, kernel_size): # return filter [1,1,kernel_size] + even = (kernel_size % 2 == 0) + half_size = kernel_size // 2 + + #For kaiser window + delta_f = 4 * half_width + A = 2.285 * (half_size - 1) * math.pi * delta_f + 7.95 + if A > 50.: + beta = 0.1102 * (A - 8.7) + elif A >= 21.: + beta = 0.5842 * (A - 21)**0.4 + 0.07886 * (A - 21.) + else: + beta = 0. + window = torch.kaiser_window(kernel_size, beta=beta, periodic=False) + + # ratio = 0.5/cutoff -> 2 * cutoff = 1 / ratio + if even: + time = (torch.arange(-half_size, half_size) + 0.5) + else: + time = torch.arange(kernel_size) - half_size + if cutoff == 0: + filter_ = torch.zeros_like(time) + else: + filter_ = 2 * cutoff * window * sinc(2 * cutoff * time) + # Normalize filter to have sum = 1, otherwise we will have a small leakage + # of the constant component in the input signal. + filter_ /= filter_.sum() + filter = filter_.view(1, 1, kernel_size) + + return filter + + +class LowPassFilter1d(nn.Module): + def __init__(self, + cutoff=0.5, + half_width=0.6, + stride: int = 1, + padding: bool = True, + padding_mode: str = 'replicate', + kernel_size: int = 12): + # kernel_size should be even number for stylegan3 setup, + # in this implementation, odd number is also possible. + super().__init__() + if cutoff < -0.: + raise ValueError("Minimum cutoff must be larger than zero.") + if cutoff > 0.5: + raise ValueError("A cutoff above 0.5 does not make sense.") + self.kernel_size = kernel_size + self.even = (kernel_size % 2 == 0) + self.pad_left = kernel_size // 2 - int(self.even) + self.pad_right = kernel_size // 2 + self.stride = stride + self.padding = padding + self.padding_mode = padding_mode + filter = kaiser_sinc_filter1d(cutoff, half_width, kernel_size) + self.register_buffer("filter", filter) + + #input [B, C, T] + def forward(self, x): + _, C, _ = x.shape + + if self.padding: + x = F.pad(x, (self.pad_left, self.pad_right), + mode=self.padding_mode) + out = F.conv1d(x, self.filter.expand(C, -1, -1), + stride=self.stride, groups=C) + + return out \ No newline at end of file diff --git a/bigvgan/model/alias/resample.py b/bigvgan/model/alias/resample.py new file mode 100644 index 0000000..750e6c3 --- /dev/null +++ b/bigvgan/model/alias/resample.py @@ -0,0 +1,49 @@ +# Adapted from https://github.com/junjun3518/alias-free-torch under the Apache License 2.0 +# LICENSE is in incl_licenses directory. + +import torch.nn as nn +from torch.nn import functional as F +from .filter import LowPassFilter1d +from .filter import kaiser_sinc_filter1d + + +class UpSample1d(nn.Module): + def __init__(self, ratio=2, kernel_size=None): + super().__init__() + self.ratio = ratio + self.kernel_size = int(6 * ratio // 2) * 2 if kernel_size is None else kernel_size + self.stride = ratio + self.pad = self.kernel_size // ratio - 1 + self.pad_left = self.pad * self.stride + (self.kernel_size - self.stride) // 2 + self.pad_right = self.pad * self.stride + (self.kernel_size - self.stride + 1) // 2 + filter = kaiser_sinc_filter1d(cutoff=0.5 / ratio, + half_width=0.6 / ratio, + kernel_size=self.kernel_size) + self.register_buffer("filter", filter) + + # x: [B, C, T] + def forward(self, x): + _, C, _ = x.shape + + x = F.pad(x, (self.pad, self.pad), mode='replicate') + x = self.ratio * F.conv_transpose1d( + x, self.filter.expand(C, -1, -1), stride=self.stride, groups=C) + x = x[..., self.pad_left:-self.pad_right] + + return x + + +class DownSample1d(nn.Module): + def __init__(self, ratio=2, kernel_size=None): + super().__init__() + self.ratio = ratio + self.kernel_size = int(6 * ratio // 2) * 2 if kernel_size is None else kernel_size + self.lowpass = LowPassFilter1d(cutoff=0.5 / ratio, + half_width=0.6 / ratio, + stride=ratio, + kernel_size=self.kernel_size) + + def forward(self, x): + xx = self.lowpass(x) + + return xx \ No newline at end of file diff --git a/bigvgan/model/bigv.py b/bigvgan/model/bigv.py new file mode 100644 index 0000000..029362c --- /dev/null +++ b/bigvgan/model/bigv.py @@ -0,0 +1,64 @@ +import torch +import torch.nn as nn + +from torch.nn import Conv1d +from torch.nn.utils import weight_norm, remove_weight_norm +from .alias.act import SnakeAlias + + +def init_weights(m, mean=0.0, std=0.01): + classname = m.__class__.__name__ + if classname.find("Conv") != -1: + m.weight.data.normal_(mean, std) + + +def get_padding(kernel_size, dilation=1): + return int((kernel_size*dilation - dilation)/2) + + +class AMPBlock(torch.nn.Module): + def __init__(self, channels, kernel_size=3, dilation=(1, 3, 5)): + super(AMPBlock, self).__init__() + self.convs1 = nn.ModuleList([ + weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[0], + padding=get_padding(kernel_size, dilation[0]))), + weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[1], + padding=get_padding(kernel_size, dilation[1]))), + weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[2], + padding=get_padding(kernel_size, dilation[2]))) + ]) + self.convs1.apply(init_weights) + + self.convs2 = nn.ModuleList([ + weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1, + padding=get_padding(kernel_size, 1))), + weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1, + padding=get_padding(kernel_size, 1))), + weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1, + padding=get_padding(kernel_size, 1))) + ]) + self.convs2.apply(init_weights) + + # total number of conv layers + self.num_layers = len(self.convs1) + len(self.convs2) + + # periodic nonlinearity with snakebeta function and anti-aliasing + self.activations = nn.ModuleList([ + SnakeAlias(channels) for _ in range(self.num_layers) + ]) + + def forward(self, x): + acts1, acts2 = self.activations[::2], self.activations[1::2] + for c1, c2, a1, a2 in zip(self.convs1, self.convs2, acts1, acts2): + xt = a1(x) + xt = c1(xt) + xt = a2(xt) + xt = c2(xt) + x = xt + x + return x + + def remove_weight_norm(self): + for l in self.convs1: + remove_weight_norm(l) + for l in self.convs2: + remove_weight_norm(l) \ No newline at end of file diff --git a/bigvgan/model/generator.py b/bigvgan/model/generator.py new file mode 100644 index 0000000..3406c32 --- /dev/null +++ b/bigvgan/model/generator.py @@ -0,0 +1,143 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import numpy as np + +from torch.nn import Conv1d +from torch.nn import ConvTranspose1d +from torch.nn.utils import weight_norm +from torch.nn.utils import remove_weight_norm + +from .nsf import SourceModuleHnNSF +from .bigv import init_weights, AMPBlock, SnakeAlias + + +class Generator(torch.nn.Module): + # this is our main BigVGAN model. Applies anti-aliased periodic activation for resblocks. + def __init__(self, hp): + super(Generator, self).__init__() + self.hp = hp + self.num_kernels = len(hp.gen.resblock_kernel_sizes) + self.num_upsamples = len(hp.gen.upsample_rates) + # pre conv + self.conv_pre = nn.utils.weight_norm( + Conv1d(hp.gen.mel_channels, hp.gen.upsample_initial_channel, 7, 1, padding=3)) + # nsf + self.f0_upsamp = torch.nn.Upsample( + scale_factor=np.prod(hp.gen.upsample_rates)) + self.m_source = SourceModuleHnNSF(sampling_rate=hp.audio.sampling_rate) + self.noise_convs = nn.ModuleList() + # transposed conv-based upsamplers. does not apply anti-aliasing + self.ups = nn.ModuleList() + for i, (u, k) in enumerate(zip(hp.gen.upsample_rates, hp.gen.upsample_kernel_sizes)): + # print(f'ups: {i} {k}, {u}, {(k - u) // 2}') + # base + self.ups.append( + weight_norm( + ConvTranspose1d( + hp.gen.upsample_initial_channel // (2 ** i), + hp.gen.upsample_initial_channel // (2 ** (i + 1)), + k, + u, + padding=(k - u) // 2) + ) + ) + # nsf + if i + 1 < len(hp.gen.upsample_rates): + stride_f0 = np.prod(hp.gen.upsample_rates[i + 1:]) + stride_f0 = int(stride_f0) + self.noise_convs.append( + Conv1d( + 1, + hp.gen.upsample_initial_channel // (2 ** (i + 1)), + kernel_size=stride_f0 * 2, + stride=stride_f0, + padding=stride_f0 // 2, + ) + ) + else: + self.noise_convs.append( + Conv1d(1, hp.gen.upsample_initial_channel // + (2 ** (i + 1)), kernel_size=1) + ) + + # residual blocks using anti-aliased multi-periodicity composition modules (AMP) + self.resblocks = nn.ModuleList() + for i in range(len(self.ups)): + ch = hp.gen.upsample_initial_channel // (2 ** (i + 1)) + for k, d in zip(hp.gen.resblock_kernel_sizes, hp.gen.resblock_dilation_sizes): + self.resblocks.append(AMPBlock(ch, k, d)) + + # post conv + self.activation_post = SnakeAlias(ch) + self.conv_post = Conv1d(ch, 1, 7, 1, padding=3, bias=False) + # weight initialization + self.ups.apply(init_weights) + + def forward(self, x, f0, train=True): + # nsf + f0 = f0[:, None] + f0 = self.f0_upsamp(f0).transpose(1, 2) + har_source = self.m_source(f0) + har_source = har_source.transpose(1, 2) + # pre conv + if train: + x = x + torch.randn_like(x) * 0.1 # Perturbation + x = self.conv_pre(x) + x = x * torch.tanh(F.softplus(x)) + + for i in range(self.num_upsamples): + # upsampling + x = self.ups[i](x) + # nsf + x_source = self.noise_convs[i](har_source) + x = x + x_source + # AMP blocks + xs = None + for j in range(self.num_kernels): + if xs is None: + xs = self.resblocks[i * self.num_kernels + j](x) + else: + xs += self.resblocks[i * self.num_kernels + j](x) + x = xs / self.num_kernels + + # post conv + x = self.activation_post(x) + x = self.conv_post(x) + x = torch.tanh(x) + return x + + def remove_weight_norm(self): + for l in self.ups: + remove_weight_norm(l) + for l in self.resblocks: + l.remove_weight_norm() + remove_weight_norm(self.conv_pre) + + def eval(self, inference=False): + super(Generator, self).eval() + # don't remove weight norm while validation in training loop + if inference: + self.remove_weight_norm() + + def inference(self, mel, f0): + MAX_WAV_VALUE = 32768.0 + audio = self.forward(mel, f0, False) + audio = audio.squeeze() # collapse all dimension except time axis + audio = MAX_WAV_VALUE * audio + audio = audio.clamp(min=-MAX_WAV_VALUE, max=MAX_WAV_VALUE-1) + audio = audio.short() + return audio + + def pitch2wav(self, f0): + MAX_WAV_VALUE = 32768.0 + # nsf + f0 = f0[:, None] + f0 = self.f0_upsamp(f0).transpose(1, 2) + har_source = self.m_source(f0) + audio = har_source.transpose(1, 2) + audio = audio.squeeze() # collapse all dimension except time axis + audio = MAX_WAV_VALUE * audio + audio = audio.clamp(min=-MAX_WAV_VALUE, max=MAX_WAV_VALUE-1) + audio = audio.short() + return audio diff --git a/bigvgan/model/nsf.py b/bigvgan/model/nsf.py new file mode 100644 index 0000000..1e9e6c7 --- /dev/null +++ b/bigvgan/model/nsf.py @@ -0,0 +1,394 @@ +import torch +import numpy as np +import sys +import torch.nn.functional as torch_nn_func + + +class PulseGen(torch.nn.Module): + """Definition of Pulse train generator + + There are many ways to implement pulse generator. + Here, PulseGen is based on SinGen. For a perfect + """ + + def __init__(self, samp_rate, pulse_amp=0.1, noise_std=0.003, voiced_threshold=0): + super(PulseGen, self).__init__() + self.pulse_amp = pulse_amp + self.sampling_rate = samp_rate + self.voiced_threshold = voiced_threshold + self.noise_std = noise_std + self.l_sinegen = SineGen( + self.sampling_rate, + harmonic_num=0, + sine_amp=self.pulse_amp, + noise_std=0, + voiced_threshold=self.voiced_threshold, + flag_for_pulse=True, + ) + + def forward(self, f0): + """Pulse train generator + pulse_train, uv = forward(f0) + input F0: tensor(batchsize=1, length, dim=1) + f0 for unvoiced steps should be 0 + output pulse_train: tensor(batchsize=1, length, dim) + output uv: tensor(batchsize=1, length, 1) + + Note: self.l_sine doesn't make sure that the initial phase of + a voiced segment is np.pi, the first pulse in a voiced segment + may not be at the first time step within a voiced segment + """ + with torch.no_grad(): + sine_wav, uv, noise = self.l_sinegen(f0) + + # sine without additive noise + pure_sine = sine_wav - noise + + # step t corresponds to a pulse if + # sine[t] > sine[t+1] & sine[t] > sine[t-1] + # & sine[t-1], sine[t+1], and sine[t] are voiced + # or + # sine[t] is voiced, sine[t-1] is unvoiced + # we use torch.roll to simulate sine[t+1] and sine[t-1] + sine_1 = torch.roll(pure_sine, shifts=1, dims=1) + uv_1 = torch.roll(uv, shifts=1, dims=1) + uv_1[:, 0, :] = 0 + sine_2 = torch.roll(pure_sine, shifts=-1, dims=1) + uv_2 = torch.roll(uv, shifts=-1, dims=1) + uv_2[:, -1, :] = 0 + + loc = (pure_sine > sine_1) * (pure_sine > sine_2) \ + * (uv_1 > 0) * (uv_2 > 0) * (uv > 0) \ + + (uv_1 < 1) * (uv > 0) + + # pulse train without noise + pulse_train = pure_sine * loc + + # additive noise to pulse train + # note that noise from sinegen is zero in voiced regions + pulse_noise = torch.randn_like(pure_sine) * self.noise_std + + # with additive noise on pulse, and unvoiced regions + pulse_train += pulse_noise * loc + pulse_noise * (1 - uv) + return pulse_train, sine_wav, uv, pulse_noise + + +class SignalsConv1d(torch.nn.Module): + """Filtering input signal with time invariant filter + Note: FIRFilter conducted filtering given fixed FIR weight + SignalsConv1d convolves two signals + Note: this is based on torch.nn.functional.conv1d + + """ + + def __init__(self): + super(SignalsConv1d, self).__init__() + + def forward(self, signal, system_ir): + """output = forward(signal, system_ir) + + signal: (batchsize, length1, dim) + system_ir: (length2, dim) + + output: (batchsize, length1, dim) + """ + if signal.shape[-1] != system_ir.shape[-1]: + print("Error: SignalsConv1d expects shape:") + print("signal (batchsize, length1, dim)") + print("system_id (batchsize, length2, dim)") + print("But received signal: {:s}".format(str(signal.shape))) + print(" system_ir: {:s}".format(str(system_ir.shape))) + sys.exit(1) + padding_length = system_ir.shape[0] - 1 + groups = signal.shape[-1] + + # pad signal on the left + signal_pad = torch_nn_func.pad(signal.permute(0, 2, 1), (padding_length, 0)) + # prepare system impulse response as (dim, 1, length2) + # also flip the impulse response + ir = torch.flip(system_ir.unsqueeze(1).permute(2, 1, 0), dims=[2]) + # convolute + output = torch_nn_func.conv1d(signal_pad, ir, groups=groups) + return output.permute(0, 2, 1) + + +class CyclicNoiseGen_v1(torch.nn.Module): + """CyclicnoiseGen_v1 + Cyclic noise with a single parameter of beta. + Pytorch v1 implementation assumes f_t is also fixed + """ + + def __init__(self, samp_rate, noise_std=0.003, voiced_threshold=0): + super(CyclicNoiseGen_v1, self).__init__() + self.samp_rate = samp_rate + self.noise_std = noise_std + self.voiced_threshold = voiced_threshold + + self.l_pulse = PulseGen( + samp_rate, + pulse_amp=1.0, + noise_std=noise_std, + voiced_threshold=voiced_threshold, + ) + self.l_conv = SignalsConv1d() + + def noise_decay(self, beta, f0mean): + """decayed_noise = noise_decay(beta, f0mean) + decayed_noise = n[t]exp(-t * f_mean / beta / samp_rate) + + beta: (dim=1) or (batchsize=1, 1, dim=1) + f0mean (batchsize=1, 1, dim=1) + + decayed_noise (batchsize=1, length, dim=1) + """ + with torch.no_grad(): + # exp(-1.0 n / T) < 0.01 => n > -log(0.01)*T = 4.60*T + # truncate the noise when decayed by -40 dB + length = 4.6 * self.samp_rate / f0mean + length = length.int() + time_idx = torch.arange(0, length, device=beta.device) + time_idx = time_idx.unsqueeze(0).unsqueeze(2) + time_idx = time_idx.repeat(beta.shape[0], 1, beta.shape[2]) + + noise = torch.randn(time_idx.shape, device=beta.device) + + # due to Pytorch implementation, use f0_mean as the f0 factor + decay = torch.exp(-time_idx * f0mean / beta / self.samp_rate) + return noise * self.noise_std * decay + + def forward(self, f0s, beta): + """Producde cyclic-noise""" + # pulse train + pulse_train, sine_wav, uv, noise = self.l_pulse(f0s) + pure_pulse = pulse_train - noise + + # decayed_noise (length, dim=1) + if (uv < 1).all(): + # all unvoiced + cyc_noise = torch.zeros_like(sine_wav) + else: + f0mean = f0s[uv > 0].mean() + + decayed_noise = self.noise_decay(beta, f0mean)[0, :, :] + # convolute + cyc_noise = self.l_conv(pure_pulse, decayed_noise) + + # add noise in invoiced segments + cyc_noise = cyc_noise + noise * (1.0 - uv) + return cyc_noise, pulse_train, sine_wav, uv, noise + + +class SineGen(torch.nn.Module): + """Definition of sine generator + SineGen(samp_rate, harmonic_num = 0, + sine_amp = 0.1, noise_std = 0.003, + voiced_threshold = 0, + flag_for_pulse=False) + + samp_rate: sampling rate in Hz + harmonic_num: number of harmonic overtones (default 0) + sine_amp: amplitude of sine-wavefrom (default 0.1) + noise_std: std of Gaussian noise (default 0.003) + voiced_thoreshold: F0 threshold for U/V classification (default 0) + flag_for_pulse: this SinGen is used inside PulseGen (default False) + + Note: when flag_for_pulse is True, the first time step of a voiced + segment is always sin(np.pi) or cos(0) + """ + + def __init__( + self, + samp_rate, + harmonic_num=0, + sine_amp=0.1, + noise_std=0.003, + voiced_threshold=0, + flag_for_pulse=False, + ): + super(SineGen, self).__init__() + self.sine_amp = sine_amp + self.noise_std = noise_std + self.harmonic_num = harmonic_num + self.dim = self.harmonic_num + 1 + self.sampling_rate = samp_rate + self.voiced_threshold = voiced_threshold + self.flag_for_pulse = flag_for_pulse + + def _f02uv(self, f0): + # generate uv signal + uv = torch.ones_like(f0) + uv = uv * (f0 > self.voiced_threshold) + return uv + + def _f02sine(self, f0_values): + """f0_values: (batchsize, length, dim) + where dim indicates fundamental tone and overtones + """ + # convert to F0 in rad. The interger part n can be ignored + # because 2 * np.pi * n doesn't affect phase + rad_values = (f0_values / self.sampling_rate) % 1 + + # initial phase noise (no noise for fundamental component) + rand_ini = torch.rand( + f0_values.shape[0], f0_values.shape[2], device=f0_values.device + ) + rand_ini[:, 0] = 0 + rad_values[:, 0, :] = rad_values[:, 0, :] + rand_ini + + # instantanouse phase sine[t] = sin(2*pi \sum_i=1 ^{t} rad) + if not self.flag_for_pulse: + # for normal case + + # To prevent torch.cumsum numerical overflow, + # it is necessary to add -1 whenever \sum_k=1^n rad_value_k > 1. + # Buffer tmp_over_one_idx indicates the time step to add -1. + # This will not change F0 of sine because (x-1) * 2*pi = x * 2*pi + tmp_over_one = torch.cumsum(rad_values, 1) % 1 + tmp_over_one_idx = (tmp_over_one[:, 1:, :] - tmp_over_one[:, :-1, :]) < 0 + cumsum_shift = torch.zeros_like(rad_values) + cumsum_shift[:, 1:, :] = tmp_over_one_idx * -1.0 + + sines = torch.sin( + torch.cumsum(rad_values + cumsum_shift, dim=1) * 2 * np.pi + ) + else: + # If necessary, make sure that the first time step of every + # voiced segments is sin(pi) or cos(0) + # This is used for pulse-train generation + + # identify the last time step in unvoiced segments + uv = self._f02uv(f0_values) + uv_1 = torch.roll(uv, shifts=-1, dims=1) + uv_1[:, -1, :] = 1 + u_loc = (uv < 1) * (uv_1 > 0) + + # get the instantanouse phase + tmp_cumsum = torch.cumsum(rad_values, dim=1) + # different batch needs to be processed differently + for idx in range(f0_values.shape[0]): + temp_sum = tmp_cumsum[idx, u_loc[idx, :, 0], :] + temp_sum[1:, :] = temp_sum[1:, :] - temp_sum[0:-1, :] + # stores the accumulation of i.phase within + # each voiced segments + tmp_cumsum[idx, :, :] = 0 + tmp_cumsum[idx, u_loc[idx, :, 0], :] = temp_sum + + # rad_values - tmp_cumsum: remove the accumulation of i.phase + # within the previous voiced segment. + i_phase = torch.cumsum(rad_values - tmp_cumsum, dim=1) + + # get the sines + sines = torch.cos(i_phase * 2 * np.pi) + return sines + + def forward(self, f0): + """sine_tensor, uv = forward(f0) + input F0: tensor(batchsize=1, length, dim=1) + f0 for unvoiced steps should be 0 + output sine_tensor: tensor(batchsize=1, length, dim) + output uv: tensor(batchsize=1, length, 1) + """ + with torch.no_grad(): + f0_buf = torch.zeros(f0.shape[0], f0.shape[1], self.dim, device=f0.device) + # fundamental component + f0_buf[:, :, 0] = f0[:, :, 0] + for idx in np.arange(self.harmonic_num): + # idx + 2: the (idx+1)-th overtone, (idx+2)-th harmonic + f0_buf[:, :, idx + 1] = f0_buf[:, :, 0] * (idx + 2) + + # generate sine waveforms + sine_waves = self._f02sine(f0_buf) * self.sine_amp + + # generate uv signal + # uv = torch.ones(f0.shape) + # uv = uv * (f0 > self.voiced_threshold) + uv = self._f02uv(f0) + + # noise: for unvoiced should be similar to sine_amp + # std = self.sine_amp/3 -> max value ~ self.sine_amp + # . for voiced regions is self.noise_std + noise_amp = uv * self.noise_std + (1 - uv) * self.sine_amp / 3 + noise = noise_amp * torch.randn_like(sine_waves) + + # first: set the unvoiced part to 0 by uv + # then: additive noise + sine_waves = sine_waves * uv + noise + return sine_waves + + +class SourceModuleCycNoise_v1(torch.nn.Module): + """SourceModuleCycNoise_v1 + SourceModule(sampling_rate, noise_std=0.003, voiced_threshod=0) + sampling_rate: sampling_rate in Hz + + noise_std: std of Gaussian noise (default: 0.003) + voiced_threshold: threshold to set U/V given F0 (default: 0) + + cyc, noise, uv = SourceModuleCycNoise_v1(F0_upsampled, beta) + F0_upsampled (batchsize, length, 1) + beta (1) + cyc (batchsize, length, 1) + noise (batchsize, length, 1) + uv (batchsize, length, 1) + """ + + def __init__(self, sampling_rate, noise_std=0.003, voiced_threshod=0): + super(SourceModuleCycNoise_v1, self).__init__() + self.sampling_rate = sampling_rate + self.noise_std = noise_std + self.l_cyc_gen = CyclicNoiseGen_v1(sampling_rate, noise_std, voiced_threshod) + + def forward(self, f0_upsamped, beta): + """ + cyc, noise, uv = SourceModuleCycNoise_v1(F0, beta) + F0_upsampled (batchsize, length, 1) + beta (1) + cyc (batchsize, length, 1) + noise (batchsize, length, 1) + uv (batchsize, length, 1) + """ + # source for harmonic branch + cyc, pulse, sine, uv, add_noi = self.l_cyc_gen(f0_upsamped, beta) + + # source for noise branch, in the same shape as uv + noise = torch.randn_like(uv) * self.noise_std / 3 + return cyc, noise, uv + + +class SourceModuleHnNSF(torch.nn.Module): + def __init__( + self, + sampling_rate=32000, + sine_amp=0.1, + add_noise_std=0.003, + voiced_threshod=0, + ): + super(SourceModuleHnNSF, self).__init__() + harmonic_num = 10 + self.sine_amp = sine_amp + self.noise_std = add_noise_std + + # to produce sine waveforms + self.l_sin_gen = SineGen( + sampling_rate, harmonic_num, sine_amp, add_noise_std, voiced_threshod + ) + + # to merge source harmonics into a single excitation + self.l_tanh = torch.nn.Tanh() + self.register_buffer('merge_w', torch.FloatTensor([[ + 0.2942, -0.2243, 0.0033, -0.0056, -0.0020, -0.0046, + 0.0221, -0.0083, -0.0241, -0.0036, -0.0581]])) + self.register_buffer('merge_b', torch.FloatTensor([0.0008])) + + def forward(self, x): + """ + Sine_source = SourceModuleHnNSF(F0_sampled) + F0_sampled (batchsize, length, 1) + Sine_source (batchsize, length, 1) + """ + # source for harmonic branch + sine_wavs = self.l_sin_gen(x) + sine_wavs = torch_nn_func.linear( + sine_wavs, self.merge_w) + self.merge_b + sine_merge = self.l_tanh(sine_wavs) + return sine_merge diff --git a/bigvgan_pretrain/README.md b/bigvgan_pretrain/README.md new file mode 100644 index 0000000..0bf434e --- /dev/null +++ b/bigvgan_pretrain/README.md @@ -0,0 +1,5 @@ +Path for: + + nsf_bigvgan_pretrain_32K.pth + + DownLoad link:https://github.com/PlayVoice/NSF-BigVGAN/releases/tag/augment diff --git a/configs/base.yaml b/configs/base.yaml new file mode 100644 index 0000000..a7e52cc --- /dev/null +++ b/configs/base.yaml @@ -0,0 +1,40 @@ +train: + seed: 37 + train_files: "files/train.txt" + valid_files: "files/valid.txt" + log_dir: 'logs/grad_svc' + n_epochs: 10000 + learning_rate: 1e-4 + batch_size: 16 + test_size: 4 + test_step: 1 + save_step: 1 + pretrain: "" +############################# +data: + segment_size: 16000 # WARNING: base on hop_length + max_wav_value: 32768.0 + sampling_rate: 32000 + filter_length: 1024 + hop_length: 320 + win_length: 1024 + mel_channels: 100 + mel_fmin: 40.0 + mel_fmax: 16000.0 +############################# +grad: + n_mels: 100 + n_vecs: 256 + n_pits: 256 + n_spks: 256 + n_embs: 64 + + # encoder parameters + n_enc_channels: 192 + filter_channels: 768 + + # decoder parameters + dec_dim: 64 + beta_min: 0.05 + beta_max: 20.0 + pe_scale: 1000 diff --git a/grad/LICENSE b/grad/LICENSE new file mode 100644 index 0000000..e1c1351 --- /dev/null +++ b/grad/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 Huawei Technologies Co., Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/grad/__init__.py b/grad/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/grad/base.py b/grad/base.py new file mode 100644 index 0000000..7294dcb --- /dev/null +++ b/grad/base.py @@ -0,0 +1,29 @@ +import numpy as np +import torch + + +class BaseModule(torch.nn.Module): + def __init__(self): + super(BaseModule, self).__init__() + + @property + def nparams(self): + """ + Returns number of trainable parameters of the module. + """ + num_params = 0 + for name, param in self.named_parameters(): + if param.requires_grad: + num_params += np.prod(param.detach().cpu().numpy().shape) + return num_params + + + def relocate_input(self, x: list): + """ + Relocates provided tensors to the same device set for the module. + """ + device = next(self.parameters()).device + for i in range(len(x)): + if isinstance(x[i], torch.Tensor) and x[i].device != device: + x[i] = x[i].to(device) + return x diff --git a/grad/diffusion.py b/grad/diffusion.py new file mode 100644 index 0000000..3462999 --- /dev/null +++ b/grad/diffusion.py @@ -0,0 +1,273 @@ +import math +import torch +from einops import rearrange +from grad.base import BaseModule + + +class Mish(BaseModule): + def forward(self, x): + return x * torch.tanh(torch.nn.functional.softplus(x)) + + +class Upsample(BaseModule): + def __init__(self, dim): + super(Upsample, self).__init__() + self.conv = torch.nn.ConvTranspose2d(dim, dim, 4, 2, 1) + + def forward(self, x): + return self.conv(x) + + +class Downsample(BaseModule): + def __init__(self, dim): + super(Downsample, self).__init__() + self.conv = torch.nn.Conv2d(dim, dim, 3, 2, 1) + + def forward(self, x): + return self.conv(x) + + +class Rezero(BaseModule): + def __init__(self, fn): + super(Rezero, self).__init__() + self.fn = fn + self.g = torch.nn.Parameter(torch.zeros(1)) + + def forward(self, x): + return self.fn(x) * self.g + + +class Block(BaseModule): + def __init__(self, dim, dim_out, groups=8): + super(Block, self).__init__() + self.block = torch.nn.Sequential(torch.nn.Conv2d(dim, dim_out, 3, + padding=1), torch.nn.GroupNorm( + groups, dim_out), Mish()) + + def forward(self, x, mask): + output = self.block(x * mask) + return output * mask + + +class ResnetBlock(BaseModule): + def __init__(self, dim, dim_out, time_emb_dim, groups=8): + super(ResnetBlock, self).__init__() + self.mlp = torch.nn.Sequential(Mish(), torch.nn.Linear(time_emb_dim, + dim_out)) + + self.block1 = Block(dim, dim_out, groups=groups) + self.block2 = Block(dim_out, dim_out, groups=groups) + if dim != dim_out: + self.res_conv = torch.nn.Conv2d(dim, dim_out, 1) + else: + self.res_conv = torch.nn.Identity() + + def forward(self, x, mask, time_emb): + h = self.block1(x, mask) + h += self.mlp(time_emb).unsqueeze(-1).unsqueeze(-1) + h = self.block2(h, mask) + output = h + self.res_conv(x * mask) + return output + + +class LinearAttention(BaseModule): + def __init__(self, dim, heads=4, dim_head=32): + super(LinearAttention, self).__init__() + self.heads = heads + hidden_dim = dim_head * heads + self.to_qkv = torch.nn.Conv2d(dim, hidden_dim * 3, 1, bias=False) + self.to_out = torch.nn.Conv2d(hidden_dim, dim, 1) + + def forward(self, x): + b, c, h, w = x.shape + qkv = self.to_qkv(x) + q, k, v = rearrange(qkv, 'b (qkv heads c) h w -> qkv b heads c (h w)', + heads = self.heads, qkv=3) + k = k.softmax(dim=-1) + context = torch.einsum('bhdn,bhen->bhde', k, v) + out = torch.einsum('bhde,bhdn->bhen', context, q) + out = rearrange(out, 'b heads c (h w) -> b (heads c) h w', + heads=self.heads, h=h, w=w) + return self.to_out(out) + + +class Residual(BaseModule): + def __init__(self, fn): + super(Residual, self).__init__() + self.fn = fn + + def forward(self, x, *args, **kwargs): + output = self.fn(x, *args, **kwargs) + x + return output + + +class SinusoidalPosEmb(BaseModule): + def __init__(self, dim): + super(SinusoidalPosEmb, self).__init__() + self.dim = dim + + def forward(self, x, scale=1000): + device = x.device + half_dim = self.dim // 2 + emb = math.log(10000) / (half_dim - 1) + emb = torch.exp(torch.arange(half_dim, device=device).float() * -emb) + emb = scale * x.unsqueeze(1) * emb.unsqueeze(0) + emb = torch.cat((emb.sin(), emb.cos()), dim=-1) + return emb + + +class GradLogPEstimator2d(BaseModule): + def __init__(self, dim, dim_mults=(1, 2, 4), emb_dim=64, n_mels=100, + groups=8, pe_scale=1000): + super(GradLogPEstimator2d, self).__init__() + self.dim = dim + self.dim_mults = dim_mults + self.emb_dim = emb_dim + self.groups = groups + self.pe_scale = pe_scale + + self.spk_mlp = torch.nn.Sequential(torch.nn.Linear(emb_dim, emb_dim * 4), Mish(), + torch.nn.Linear(emb_dim * 4, n_mels)) + self.time_pos_emb = SinusoidalPosEmb(dim) + self.mlp = torch.nn.Sequential(torch.nn.Linear(dim, dim * 4), Mish(), + torch.nn.Linear(dim * 4, dim)) + + dims = [2 + 1, *map(lambda m: dim * m, dim_mults)] + in_out = list(zip(dims[:-1], dims[1:])) + self.downs = torch.nn.ModuleList([]) + self.ups = torch.nn.ModuleList([]) + num_resolutions = len(in_out) + + for ind, (dim_in, dim_out) in enumerate(in_out): # 2 downs + is_last = ind >= (num_resolutions - 1) + self.downs.append(torch.nn.ModuleList([ + ResnetBlock(dim_in, dim_out, time_emb_dim=dim), + ResnetBlock(dim_out, dim_out, time_emb_dim=dim), + Residual(Rezero(LinearAttention(dim_out))), + Downsample(dim_out) if not is_last else torch.nn.Identity()])) + + mid_dim = dims[-1] + self.mid_block1 = ResnetBlock(mid_dim, mid_dim, time_emb_dim=dim) + self.mid_attn = Residual(Rezero(LinearAttention(mid_dim))) + self.mid_block2 = ResnetBlock(mid_dim, mid_dim, time_emb_dim=dim) + + for ind, (dim_in, dim_out) in enumerate(reversed(in_out[1:])): # 2 ups + self.ups.append(torch.nn.ModuleList([ + ResnetBlock(dim_out * 2, dim_in, time_emb_dim=dim), + ResnetBlock(dim_in, dim_in, time_emb_dim=dim), + Residual(Rezero(LinearAttention(dim_in))), + Upsample(dim_in)])) + self.final_block = Block(dim, dim) + self.final_conv = torch.nn.Conv2d(dim, 1, 1) + + def forward(self, spk, x, mask, mu, t): + s = self.spk_mlp(spk) + + t = self.time_pos_emb(t, scale=self.pe_scale) + t = self.mlp(t) + + s = s.unsqueeze(-1).repeat(1, 1, x.shape[-1]) + x = torch.stack([mu, x, s], 1) + mask = mask.unsqueeze(1) + + hiddens = [] + masks = [mask] + for resnet1, resnet2, attn, downsample in self.downs: + mask_down = masks[-1] + x = resnet1(x, mask_down, t) + x = resnet2(x, mask_down, t) + x = attn(x) + hiddens.append(x) + x = downsample(x * mask_down) + masks.append(mask_down[:, :, :, ::2]) + + masks = masks[:-1] + mask_mid = masks[-1] + x = self.mid_block1(x, mask_mid, t) + x = self.mid_attn(x) + x = self.mid_block2(x, mask_mid, t) + + for resnet1, resnet2, attn, upsample in self.ups: + mask_up = masks.pop() + x = torch.cat((x, hiddens.pop()), dim=1) + x = resnet1(x, mask_up, t) + x = resnet2(x, mask_up, t) + x = attn(x) + x = upsample(x * mask_up) + + x = self.final_block(x, mask) + output = self.final_conv(x * mask) + + return (output * mask).squeeze(1) + + +def get_noise(t, beta_init, beta_term, cumulative=False): + if cumulative: + noise = beta_init*t + 0.5*(beta_term - beta_init)*(t**2) + else: + noise = beta_init + (beta_term - beta_init)*t + return noise + + +class Diffusion(BaseModule): + def __init__(self, n_mels, dim, emb_dim=64, + beta_min=0.05, beta_max=20, pe_scale=1000): + super(Diffusion, self).__init__() + self.n_mels = n_mels + self.beta_min = beta_min + self.beta_max = beta_max + self.estimator = GradLogPEstimator2d(dim, + n_mels=n_mels, + emb_dim=emb_dim, + pe_scale=pe_scale) + + def forward_diffusion(self, mel, mask, mu, t): + time = t.unsqueeze(-1).unsqueeze(-1) + cum_noise = get_noise(time, self.beta_min, self.beta_max, cumulative=True) + mean = mel*torch.exp(-0.5*cum_noise) + mu*(1.0 - torch.exp(-0.5*cum_noise)) + variance = 1.0 - torch.exp(-cum_noise) + z = torch.randn(mel.shape, dtype=mel.dtype, device=mel.device, + requires_grad=False) + xt = mean + z * torch.sqrt(variance) + return xt * mask, z * mask + + @torch.no_grad() + def reverse_diffusion(self, spk, z, mask, mu, n_timesteps, stoc=False): + h = 1.0 / n_timesteps + xt = z * mask + for i in range(n_timesteps): + t = (1.0 - (i + 0.5)*h) * torch.ones(z.shape[0], dtype=z.dtype, + device=z.device) + time = t.unsqueeze(-1).unsqueeze(-1) + noise_t = get_noise(time, self.beta_min, self.beta_max, + cumulative=False) + if stoc: # adds stochastic term + dxt_det = 0.5 * (mu - xt) - self.estimator(spk, xt, mask, mu, t) + dxt_det = dxt_det * noise_t * h + dxt_stoc = torch.randn(z.shape, dtype=z.dtype, device=z.device, + requires_grad=False) + dxt_stoc = dxt_stoc * torch.sqrt(noise_t * h) + dxt = dxt_det + dxt_stoc + else: + dxt = 0.5 * (mu - xt - self.estimator(spk, xt, mask, mu, t)) + dxt = dxt * noise_t * h + xt = (xt - dxt) * mask + return xt + + @torch.no_grad() + def forward(self, spk, z, mask, mu, n_timesteps, stoc=False): + return self.reverse_diffusion(spk, z, mask, mu, n_timesteps, stoc) + + def loss_t(self, spk, mel, mask, mu, t): + xt, z = self.forward_diffusion(mel, mask, mu, t) + time = t.unsqueeze(-1).unsqueeze(-1) + cum_noise = get_noise(time, self.beta_min, self.beta_max, cumulative=True) + noise_estimation = self.estimator(spk, xt, mask, mu, t) + noise_estimation *= torch.sqrt(1.0 - torch.exp(-cum_noise)) + loss = torch.sum((noise_estimation + z)**2) / (torch.sum(mask)*self.n_mels) + return loss, xt + + def compute_loss(self, spk, mel, mask, mu, offset=1e-5): + t = torch.rand(mel.shape[0], dtype=mel.dtype, device=mel.device, requires_grad=False) + t = torch.clamp(t, offset, 1.0 - offset) + return self.loss_t(spk, mel, mask, mu, t) diff --git a/grad/encoder.py b/grad/encoder.py new file mode 100644 index 0000000..cb1e118 --- /dev/null +++ b/grad/encoder.py @@ -0,0 +1,300 @@ +import math +import torch + +from grad.base import BaseModule +from grad.utils import sequence_mask, convert_pad_shape + + +class LayerNorm(BaseModule): + def __init__(self, channels, eps=1e-4): + super(LayerNorm, self).__init__() + self.channels = channels + self.eps = eps + + self.gamma = torch.nn.Parameter(torch.ones(channels)) + self.beta = torch.nn.Parameter(torch.zeros(channels)) + + def forward(self, x): + n_dims = len(x.shape) + mean = torch.mean(x, 1, keepdim=True) + variance = torch.mean((x - mean)**2, 1, keepdim=True) + + x = (x - mean) * torch.rsqrt(variance + self.eps) + + shape = [1, -1] + [1] * (n_dims - 2) + x = x * self.gamma.view(*shape) + self.beta.view(*shape) + return x + + +class ConvReluNorm(BaseModule): + def __init__(self, in_channels, hidden_channels, out_channels, kernel_size, + n_layers, p_dropout): + super(ConvReluNorm, self).__init__() + self.in_channels = in_channels + self.hidden_channels = hidden_channels + self.out_channels = out_channels + self.kernel_size = kernel_size + self.n_layers = n_layers + self.p_dropout = p_dropout + + self.conv_pre = torch.nn.Conv1d(in_channels, hidden_channels, + kernel_size, padding=kernel_size//2) + self.conv_layers = torch.nn.ModuleList() + self.norm_layers = torch.nn.ModuleList() + self.conv_layers.append(torch.nn.Conv1d(hidden_channels, hidden_channels, + kernel_size, padding=kernel_size//2)) + self.norm_layers.append(LayerNorm(hidden_channels)) + self.relu_drop = torch.nn.Sequential(torch.nn.ReLU(), torch.nn.Dropout(p_dropout)) + for _ in range(n_layers - 1): + self.conv_layers.append(torch.nn.Conv1d(hidden_channels, hidden_channels, + kernel_size, padding=kernel_size//2)) + self.norm_layers.append(LayerNorm(hidden_channels)) + self.proj = torch.nn.Conv1d(hidden_channels, out_channels, 1) + self.proj.weight.data.zero_() + self.proj.bias.data.zero_() + + def forward(self, x, x_mask): + x = self.conv_pre(x) + x_org = x + for i in range(self.n_layers): + x = self.conv_layers[i](x * x_mask) + x = self.norm_layers[i](x) + x = self.relu_drop(x) + x = x_org + self.proj(x) + return x * x_mask + + +class MultiHeadAttention(BaseModule): + def __init__(self, channels, out_channels, n_heads, window_size=None, + heads_share=True, p_dropout=0.0, proximal_bias=False, + proximal_init=False): + super(MultiHeadAttention, self).__init__() + assert channels % n_heads == 0 + + self.channels = channels + self.out_channels = out_channels + self.n_heads = n_heads + self.window_size = window_size + self.heads_share = heads_share + self.proximal_bias = proximal_bias + self.p_dropout = p_dropout + self.attn = None + + self.k_channels = channels // n_heads + self.conv_q = torch.nn.Conv1d(channels, channels, 1) + self.conv_k = torch.nn.Conv1d(channels, channels, 1) + self.conv_v = torch.nn.Conv1d(channels, channels, 1) + if window_size is not None: + n_heads_rel = 1 if heads_share else n_heads + rel_stddev = self.k_channels**-0.5 + self.emb_rel_k = torch.nn.Parameter(torch.randn(n_heads_rel, + window_size * 2 + 1, self.k_channels) * rel_stddev) + self.emb_rel_v = torch.nn.Parameter(torch.randn(n_heads_rel, + window_size * 2 + 1, self.k_channels) * rel_stddev) + self.conv_o = torch.nn.Conv1d(channels, out_channels, 1) + self.drop = torch.nn.Dropout(p_dropout) + + torch.nn.init.xavier_uniform_(self.conv_q.weight) + torch.nn.init.xavier_uniform_(self.conv_k.weight) + if proximal_init: + self.conv_k.weight.data.copy_(self.conv_q.weight.data) + self.conv_k.bias.data.copy_(self.conv_q.bias.data) + torch.nn.init.xavier_uniform_(self.conv_v.weight) + + def forward(self, x, c, attn_mask=None): + q = self.conv_q(x) + k = self.conv_k(c) + v = self.conv_v(c) + + x, self.attn = self.attention(q, k, v, mask=attn_mask) + + x = self.conv_o(x) + return x + + def attention(self, query, key, value, mask=None): + b, d, t_s, t_t = (*key.size(), query.size(2)) + query = query.view(b, self.n_heads, self.k_channels, t_t).transpose(2, 3) + key = key.view(b, self.n_heads, self.k_channels, t_s).transpose(2, 3) + value = value.view(b, self.n_heads, self.k_channels, t_s).transpose(2, 3) + + scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(self.k_channels) + if self.window_size is not None: + assert t_s == t_t, "Relative attention is only available for self-attention." + key_relative_embeddings = self._get_relative_embeddings(self.emb_rel_k, t_s) + rel_logits = self._matmul_with_relative_keys(query, key_relative_embeddings) + rel_logits = self._relative_position_to_absolute_position(rel_logits) + scores_local = rel_logits / math.sqrt(self.k_channels) + scores = scores + scores_local + if self.proximal_bias: + assert t_s == t_t, "Proximal bias is only available for self-attention." + scores = scores + self._attention_bias_proximal(t_s).to(device=scores.device, + dtype=scores.dtype) + if mask is not None: + scores = scores.masked_fill(mask == 0, -1e4) + p_attn = torch.nn.functional.softmax(scores, dim=-1) + p_attn = self.drop(p_attn) + output = torch.matmul(p_attn, value) + if self.window_size is not None: + relative_weights = self._absolute_position_to_relative_position(p_attn) + value_relative_embeddings = self._get_relative_embeddings(self.emb_rel_v, t_s) + output = output + self._matmul_with_relative_values(relative_weights, + value_relative_embeddings) + output = output.transpose(2, 3).contiguous().view(b, d, t_t) + return output, p_attn + + def _matmul_with_relative_values(self, x, y): + ret = torch.matmul(x, y.unsqueeze(0)) + return ret + + def _matmul_with_relative_keys(self, x, y): + ret = torch.matmul(x, y.unsqueeze(0).transpose(-2, -1)) + return ret + + def _get_relative_embeddings(self, relative_embeddings, length): + pad_length = max(length - (self.window_size + 1), 0) + slice_start_position = max((self.window_size + 1) - length, 0) + slice_end_position = slice_start_position + 2 * length - 1 + if pad_length > 0: + padded_relative_embeddings = torch.nn.functional.pad( + relative_embeddings, convert_pad_shape([[0, 0], + [pad_length, pad_length], [0, 0]])) + else: + padded_relative_embeddings = relative_embeddings + used_relative_embeddings = padded_relative_embeddings[:, + slice_start_position:slice_end_position] + return used_relative_embeddings + + def _relative_position_to_absolute_position(self, x): + batch, heads, length, _ = x.size() + x = torch.nn.functional.pad(x, convert_pad_shape([[0,0],[0,0],[0,0],[0,1]])) + x_flat = x.view([batch, heads, length * 2 * length]) + x_flat = torch.nn.functional.pad(x_flat, convert_pad_shape([[0,0],[0,0],[0,length-1]])) + x_final = x_flat.view([batch, heads, length+1, 2*length-1])[:, :, :length, length-1:] + return x_final + + def _absolute_position_to_relative_position(self, x): + batch, heads, length, _ = x.size() + x = torch.nn.functional.pad(x, convert_pad_shape([[0, 0], [0, 0], [0, 0], [0, length-1]])) + x_flat = x.view([batch, heads, length**2 + length*(length - 1)]) + x_flat = torch.nn.functional.pad(x_flat, convert_pad_shape([[0, 0], [0, 0], [length, 0]])) + x_final = x_flat.view([batch, heads, length, 2*length])[:,:,:,1:] + return x_final + + def _attention_bias_proximal(self, length): + r = torch.arange(length, dtype=torch.float32) + diff = torch.unsqueeze(r, 0) - torch.unsqueeze(r, 1) + return torch.unsqueeze(torch.unsqueeze(-torch.log1p(torch.abs(diff)), 0), 0) + + +class FFN(BaseModule): + def __init__(self, in_channels, out_channels, filter_channels, kernel_size, + p_dropout=0.0): + super(FFN, self).__init__() + self.in_channels = in_channels + self.out_channels = out_channels + self.filter_channels = filter_channels + self.kernel_size = kernel_size + self.p_dropout = p_dropout + + self.conv_1 = torch.nn.Conv1d(in_channels, filter_channels, kernel_size, + padding=kernel_size//2) + self.conv_2 = torch.nn.Conv1d(filter_channels, out_channels, kernel_size, + padding=kernel_size//2) + self.drop = torch.nn.Dropout(p_dropout) + + def forward(self, x, x_mask): + x = self.conv_1(x * x_mask) + x = torch.relu(x) + x = self.drop(x) + x = self.conv_2(x * x_mask) + return x * x_mask + + +class Encoder(BaseModule): + def __init__(self, hidden_channels, filter_channels, n_heads, n_layers, + kernel_size=1, p_dropout=0.0, window_size=None, **kwargs): + super(Encoder, self).__init__() + self.hidden_channels = hidden_channels + self.filter_channels = filter_channels + self.n_heads = n_heads + self.n_layers = n_layers + self.kernel_size = kernel_size + self.p_dropout = p_dropout + self.window_size = window_size + + self.drop = torch.nn.Dropout(p_dropout) + self.attn_layers = torch.nn.ModuleList() + self.norm_layers_1 = torch.nn.ModuleList() + self.ffn_layers = torch.nn.ModuleList() + self.norm_layers_2 = torch.nn.ModuleList() + for _ in range(self.n_layers): + self.attn_layers.append(MultiHeadAttention(hidden_channels, hidden_channels, + n_heads, window_size=window_size, p_dropout=p_dropout)) + self.norm_layers_1.append(LayerNorm(hidden_channels)) + self.ffn_layers.append(FFN(hidden_channels, hidden_channels, + filter_channels, kernel_size, p_dropout=p_dropout)) + self.norm_layers_2.append(LayerNorm(hidden_channels)) + + def forward(self, x, x_mask): + attn_mask = x_mask.unsqueeze(2) * x_mask.unsqueeze(-1) + for i in range(self.n_layers): + x = x * x_mask + y = self.attn_layers[i](x, x, attn_mask) + y = self.drop(y) + x = self.norm_layers_1[i](x + y) + y = self.ffn_layers[i](x, x_mask) + y = self.drop(y) + x = self.norm_layers_2[i](x + y) + x = x * x_mask + return x + + +class TextEncoder(BaseModule): + def __init__(self, n_vecs, n_mels, n_embs, + n_channels, + filter_channels, + n_heads=2, + n_layers=6, + kernel_size=3, + p_dropout=0.1, + window_size=4): + super(TextEncoder, self).__init__() + self.n_vecs = n_vecs + self.n_mels = n_mels + self.n_embs = n_embs + self.n_channels = n_channels + self.filter_channels = filter_channels + self.n_heads = n_heads + self.n_layers = n_layers + self.kernel_size = kernel_size + self.p_dropout = p_dropout + self.window_size = window_size + + self.prenet = ConvReluNorm(n_vecs, + n_channels, + n_channels, + kernel_size=5, + n_layers=3, + p_dropout=0.5) + + self.encoder = Encoder(n_channels + n_embs + n_embs, + filter_channels, + n_heads, + n_layers, + kernel_size, + p_dropout, + window_size=window_size) + + self.proj_m = torch.nn.Conv1d(n_channels + n_embs + n_embs, n_mels, 1) + + def forward(self, x_lengths, x, pit, spk): + x_mask = torch.unsqueeze(sequence_mask(x_lengths, x.size(2)), 1).to(x.dtype) + # despeaker + x = self.prenet(x, x_mask) + # pitch + speaker + spk = spk.unsqueeze(-1).repeat(1, 1, x.shape[-1]) + x = torch.cat([x, pit], dim=1) + x = torch.cat([x, spk], dim=1) + x = self.encoder(x, x_mask) + mu = self.proj_m(x) * x_mask + return mu, x_mask diff --git a/grad/model.py b/grad/model.py new file mode 100644 index 0000000..9ce962e --- /dev/null +++ b/grad/model.py @@ -0,0 +1,125 @@ +import math +import torch + +from grad.base import BaseModule +from grad.encoder import TextEncoder +from grad.diffusion import Diffusion +from grad.utils import f0_to_coarse, rand_ids_segments, slice_segments + + +class GradTTS(BaseModule): + def __init__(self, n_mels, n_vecs, n_pits, n_spks, n_embs, + n_enc_channels, filter_channels, + dec_dim, beta_min, beta_max, pe_scale): + super(GradTTS, self).__init__() + # common + self.n_mels = n_mels + self.n_vecs = n_vecs + self.n_spks = n_spks + self.n_embs = n_embs + # encoder + self.n_enc_channels = n_enc_channels + self.filter_channels = filter_channels + # decoder + self.dec_dim = dec_dim + self.beta_min = beta_min + self.beta_max = beta_max + self.pe_scale = pe_scale + + self.pit_emb = torch.nn.Embedding(n_pits, n_embs) + self.spk_emb = torch.nn.Linear(n_spks, n_embs) + self.encoder = TextEncoder(n_vecs, + n_mels, + n_embs, + n_enc_channels, + filter_channels) + self.decoder = Diffusion(n_mels, dec_dim, n_embs, beta_min, beta_max, pe_scale) + + @torch.no_grad() + def forward(self, lengths, vec, pit, spk, n_timesteps, temperature=1.0, stoc=False): + """ + Generates mel-spectrogram from vec. Returns: + 1. encoder outputs + 2. decoder outputs + + Args: + lengths (torch.Tensor): lengths of texts in batch. + vec (torch.Tensor): batch of speech vec + pit (torch.Tensor): batch of speech pit + spk (torch.Tensor): batch of speaker + + n_timesteps (int): number of steps to use for reverse diffusion in decoder. + temperature (float, optional): controls variance of terminal distribution. + stoc (bool, optional): flag that adds stochastic term to the decoder sampler. + Usually, does not provide synthesis improvements. + """ + lengths, vec, pit, spk = self.relocate_input([lengths, vec, pit, spk]) + + # Get pitch embedding + pit = self.pit_emb(f0_to_coarse(pit)) + + # Get speaker embedding + spk = self.spk_emb(spk) + + # Transpose + vec = torch.transpose(vec, 1, -1) + pit = torch.transpose(pit, 1, -1) + + # Get encoder_outputs `mu_x` + mu_x, mask_x = self.encoder(lengths, vec, pit, spk) + encoder_outputs = mu_x + + # Sample latent representation from terminal distribution N(mu_y, I) + z = mu_x + torch.randn_like(mu_x, device=mu_x.device) / temperature + # Generate sample by performing reverse dynamics + decoder_outputs = self.decoder(spk, z, mask_x, mu_x, n_timesteps, stoc) + + return encoder_outputs, decoder_outputs + + def compute_loss(self, lengths, vec, pit, spk, mel, out_size): + """ + Computes 2 losses: + 1. prior loss: loss between mel-spectrogram and encoder outputs. + 2. diffusion loss: loss between gaussian noise and its reconstruction by diffusion-based decoder. + + Args: + lengths (torch.Tensor): lengths of texts in batch. + vec (torch.Tensor): batch of speech vec + pit (torch.Tensor): batch of speech pit + spk (torch.Tensor): batch of speaker + mel (torch.Tensor): batch of corresponding mel-spectrogram + + out_size (int, optional): length (in mel's sampling rate) of segment to cut, on which decoder will be trained. + Should be divisible by 2^{num of UNet downsamplings}. Needed to increase batch size. + """ + lengths, vec, pit, spk, mel = self.relocate_input([lengths, vec, pit, spk, mel]) + + # Get pitch embedding + pit = self.pit_emb(f0_to_coarse(pit)) + + # Get speaker embedding + spk = self.spk_emb(spk) + + # Transpose + vec = torch.transpose(vec, 1, -1) + pit = torch.transpose(pit, 1, -1) + + # Get encoder_outputs `mu_x` + mu_x, mask_x = self.encoder(lengths, vec, pit, spk) + + # Cut a small segment of mel-spectrogram in order to increase batch size + if not isinstance(out_size, type(None)): + ids = rand_ids_segments(lengths, out_size) + mel = slice_segments(mel, ids, out_size) + + mask_y = slice_segments(mask_x, ids, out_size) + mu_y = slice_segments(mu_x, ids, out_size) + + # Compute loss of score-based decoder + diff_loss, xt = self.decoder.compute_loss(spk, mel, mask_y, mu_y) + + # Compute loss between aligned encoder outputs and mel-spectrogram + prior_loss = torch.sum(0.5 * ((mel - mu_y) ** 2 + math.log(2 * math.pi)) * mask_y) + prior_loss = prior_loss / (torch.sum(mask_y) * self.n_mels) + + return prior_loss, diff_loss diff --git a/grad/utils.py b/grad/utils.py new file mode 100644 index 0000000..8432525 --- /dev/null +++ b/grad/utils.py @@ -0,0 +1,99 @@ +import torch +import numpy as np +import inspect + + +def sequence_mask(length, max_length=None): + if max_length is None: + max_length = length.max() + x = torch.arange(int(max_length), dtype=length.dtype, device=length.device) + return x.unsqueeze(0) < length.unsqueeze(1) + + +def fix_len_compatibility(length, num_downsamplings_in_unet=2): + while True: + if length % (2**num_downsamplings_in_unet) == 0: + return length + length += 1 + + +def convert_pad_shape(pad_shape): + l = pad_shape[::-1] + pad_shape = [item for sublist in l for item in sublist] + return pad_shape + + +def generate_path(duration, mask): + device = duration.device + + b, t_x, t_y = mask.shape + cum_duration = torch.cumsum(duration, 1) + path = torch.zeros(b, t_x, t_y, dtype=mask.dtype).to(device=device) + + cum_duration_flat = cum_duration.view(b * t_x) + path = sequence_mask(cum_duration_flat, t_y).to(mask.dtype) + path = path.view(b, t_x, t_y) + path = path - torch.nn.functional.pad(path, convert_pad_shape([[0, 0], + [1, 0], [0, 0]]))[:, :-1] + path = path * mask + return path + + +def duration_loss(logw, logw_, lengths): + loss = torch.sum((logw - logw_)**2) / torch.sum(lengths) + return loss + + +f0_bin = 256 +f0_max = 1100.0 +f0_min = 50.0 +f0_mel_min = 1127 * np.log(1 + f0_min / 700) +f0_mel_max = 1127 * np.log(1 + f0_max / 700) + + +def f0_to_coarse(f0): + is_torch = isinstance(f0, torch.Tensor) + f0_mel = 1127 * (1 + f0 / 700).log() if is_torch else 1127 * \ + np.log(1 + f0 / 700) + f0_mel[f0_mel > 0] = (f0_mel[f0_mel > 0] - f0_mel_min) * \ + (f0_bin - 2) / (f0_mel_max - f0_mel_min) + 1 + + f0_mel[f0_mel <= 1] = 1 + f0_mel[f0_mel > f0_bin - 1] = f0_bin - 1 + f0_coarse = ( + f0_mel + 0.5).long() if is_torch else np.rint(f0_mel).astype(np.int) + assert f0_coarse.max() <= 255 and f0_coarse.min( + ) >= 1, (f0_coarse.max(), f0_coarse.min()) + return f0_coarse + + +def rand_ids_segments(lengths, segment_size=200): + b = lengths.shape[0] + ids_str_max = lengths - segment_size + ids_str = (torch.rand([b]).to(device=lengths.device) * ids_str_max).to(dtype=torch.long) + return ids_str + + +def slice_segments(x, ids_str, segment_size=200): + ret = torch.zeros_like(x[:, :, :segment_size]) + for i in range(x.size(0)): + idx_str = ids_str[i] + idx_end = idx_str + segment_size + ret[i] = x[i, :, idx_str:idx_end] + return ret + + +def retrieve_name(var): + for fi in reversed(inspect.stack()): + names = [var_name for var_name, + var_val in fi.frame.f_locals.items() if var_val is var] + if len(names) > 0: + return names[0] + + +Debug_Enable = True + + +def debug_shapes(var): + if Debug_Enable: + print(retrieve_name(var), var.shape) diff --git a/grad_extend/data.py b/grad_extend/data.py new file mode 100644 index 0000000..b75625a --- /dev/null +++ b/grad_extend/data.py @@ -0,0 +1,133 @@ +import os +import random +import numpy as np + +import torch + +from grad.utils import fix_len_compatibility +from grad_extend.utils import parse_filelist + + +class TextMelSpeakerDataset(torch.utils.data.Dataset): + def __init__(self, filelist_path): + super().__init__() + self.filelist = parse_filelist(filelist_path, split_char='|') + self._filter() + print(f'----------{len(self.filelist)}----------') + + def _filter(self): + items_new = [] + # segment = 200 + items_min = 250 # 10ms * 250 = 2.5 S + items_max = 500 # 10ms * 400 = 5.0 S + for mel, vec, pit, spk in self.filelist: + if not os.path.isfile(mel): + continue + if not os.path.isfile(vec): + continue + if not os.path.isfile(pit): + continue + if not os.path.isfile(spk): + continue + temp = np.load(pit) + usel = int(temp.shape[0] - 1) # useful length + if (usel < items_min): + continue + if (usel >= items_max): + usel = items_max + items_new.append([mel, vec, pit, spk, usel]) + self.filelist = items_new + + def get_triplet(self, item): + # print(item) + mel = item[0] + vec = item[1] + pit = item[2] + spk = item[3] + use = item[4] + + mel = torch.load(mel) + vec = np.load(vec) + vec = np.repeat(vec, 2, 0) # 320 VEC -> 160 * 2 + pit = np.load(pit) + spk = np.load(spk) + + vec = torch.FloatTensor(vec) + pit = torch.FloatTensor(pit) + spk = torch.FloatTensor(spk) + + len_vec = vec.size()[0] - 2 # for safe + len_pit = pit.size()[0] + len_min = min(len_pit, len_vec) + + mel = mel[:, :len_min] + vec = vec[:len_min, :] + pit = pit[:len_min] + + if len_min > use: + max_frame_start = vec.size(0) - use - 1 + frame_start = random.randint(0, max_frame_start) + frame_end = frame_start + use + + mel = mel[:, frame_start:frame_end] + vec = vec[frame_start:frame_end, :] + pit = pit[frame_start:frame_end] + # print(mel.shape) + # print(vec.shape) + # print(pit.shape) + # print(spk.shape) + return (mel, vec, pit, spk) + + def __getitem__(self, index): + mel, vec, pit, spk = self.get_triplet(self.filelist[index]) + item = {'mel': mel, 'vec': vec, 'pit': pit, 'spk': spk} + return item + + def __len__(self): + return len(self.filelist) + + def sample_test_batch(self, size): + idx = np.random.choice(range(len(self)), size=size, replace=False) + test_batch = [] + for index in idx: + test_batch.append(self.__getitem__(index)) + return test_batch + + +class TextMelSpeakerBatchCollate(object): + # mel: [freq, length] + # vec: [len, 256] + # pit: [len] + # spk: [256] + def __call__(self, batch): + B = len(batch) + mel_max_length = max([item['mel'].shape[-1] for item in batch]) + max_length = fix_len_compatibility(mel_max_length) + + d_mel = batch[0]['mel'].shape[0] + d_vec = batch[0]['vec'].shape[1] + d_spk = batch[0]['spk'].shape[0] + # print("d_mel", d_mel) + # print("d_vec", d_vec) + # print("d_spk", d_spk) + mel = torch.zeros((B, d_mel, max_length), dtype=torch.float32) + vec = torch.zeros((B, max_length, d_vec), dtype=torch.float32) + pit = torch.zeros((B, max_length), dtype=torch.float32) + spk = torch.zeros((B, d_spk), dtype=torch.float32) + lengths = torch.LongTensor(B) + + for i, item in enumerate(batch): + y_, x_, p_, s_ = item['mel'], item['vec'], item['pit'], item['spk'] + + mel[i, :, :y_.shape[1]] = y_ + vec[i, :x_.shape[0], :] = x_ + pit[i, :p_.shape[0]] = p_ + spk[i] = s_ + + lengths[i] = y_.shape[1] + # print("lengths", lengths.shape) + # print("vec", vec.shape) + # print("pit", pit.shape) + # print("spk", spk.shape) + # print("mel", mel.shape) + return {'lengths': lengths, 'vec': vec, 'pit': pit, 'spk': spk, 'mel': mel} diff --git a/grad_extend/train.py b/grad_extend/train.py new file mode 100644 index 0000000..32301ed --- /dev/null +++ b/grad_extend/train.py @@ -0,0 +1,163 @@ +import os +import torch +import numpy as np + +from torch.utils.data import DataLoader +from torch.utils.tensorboard import SummaryWriter + +from tqdm import tqdm +from grad_extend.data import TextMelSpeakerDataset, TextMelSpeakerBatchCollate +from grad_extend.utils import plot_tensor, save_plot, load_model +from grad.utils import fix_len_compatibility +from grad.model import GradTTS + + +# 200 frames +out_size = fix_len_compatibility(200) + + +def train(hps, chkpt_path=None): + + print('Initializing logger...') + logger = SummaryWriter(log_dir=hps.train.log_dir) + + print('Initializing data loaders...') + train_dataset = TextMelSpeakerDataset(hps.train.train_files) + batch_collate = TextMelSpeakerBatchCollate() + loader = DataLoader(dataset=train_dataset, + batch_size=hps.train.batch_size, + collate_fn=batch_collate, + drop_last=True, + num_workers=8, + shuffle=True) + test_dataset = TextMelSpeakerDataset(hps.train.valid_files) + + print('Initializing model...') + model = GradTTS(hps.grad.n_mels, hps.grad.n_vecs, hps.grad.n_pits, hps.grad.n_spks, hps.grad.n_embs, + hps.grad.n_enc_channels, hps.grad.filter_channels, + hps.grad.dec_dim, hps.grad.beta_min, hps.grad.beta_max, hps.grad.pe_scale).cuda() + print('Number of encoder parameters = %.2fm' % (model.encoder.nparams/1e6)) + print('Number of decoder parameters = %.2fm' % (model.decoder.nparams/1e6)) + + # Load Pretrain + if os.path.isfile(hps.train.pretrain): + logger.info("Start from Grad_SVC pretrain model: %s" % hps.train.pretrain) + checkpoint = torch.load(hps.train.pretrain, map_location='cpu') + load_model(model, checkpoint['model']) + + print('Initializing optimizer...') + optim = torch.optim.Adam(params=model.parameters(), lr=hps.train.learning_rate) + + initepoch = 1 + iteration = 0 + + # Load Continue + if chkpt_path is not None: + logger.info("Resuming from checkpoint: %s" % chkpt_path) + checkpoint = torch.load(chkpt_path, map_location='cpu') + model.load_state_dict(checkpoint['model']) + optim.load_state_dict(checkpoint['optim']) + initepoch = checkpoint['epoch'] + iteration = checkpoint['steps'] + + print('Logging test batch...') + test_batch = test_dataset.sample_test_batch(size=hps.train.test_size) + for i, item in enumerate(test_batch): + mel = item['mel'] + logger.add_image(f'image_{i}/ground_truth', plot_tensor(mel.squeeze()), + global_step=0, dataformats='HWC') + save_plot(mel.squeeze(), f'{hps.train.log_dir}/original_{i}.png') + + print('Start training...') + + for epoch in range(initepoch, hps.train.n_epochs + 1): + model.eval() + print('Synthesis...') + + if epoch % hps.train.test_step == 0: + with torch.no_grad(): + for i, item in enumerate(test_batch): + l_vec = item['vec'].shape[0] + d_vec = item['vec'].shape[1] + + lengths_fix = fix_len_compatibility(l_vec) + lengths = torch.LongTensor([l_vec]).cuda() + + vec = torch.zeros((1, lengths_fix, d_vec), dtype=torch.float32).cuda() + pit = torch.zeros((1, lengths_fix), dtype=torch.float32).cuda() + spk = item['spk'].to(torch.float32).unsqueeze(0).cuda() + vec[0, :l_vec, :] = item['vec'] + pit[0, :l_vec] = item['pit'] + + y_enc, y_dec = model(lengths, vec, pit, spk, n_timesteps=50) + + logger.add_image(f'image_{i}/generated_enc', + plot_tensor(y_enc.squeeze().cpu()), + global_step=iteration, dataformats='HWC') + logger.add_image(f'image_{i}/generated_dec', + plot_tensor(y_dec.squeeze().cpu()), + global_step=iteration, dataformats='HWC') + save_plot(y_enc.squeeze().cpu(), + f'{hps.train.log_dir}/generated_enc_{i}.png') + save_plot(y_dec.squeeze().cpu(), + f'{hps.train.log_dir}/generated_dec_{i}.png') + + model.train() + + prior_losses = [] + diff_losses = [] + with tqdm(loader, total=len(train_dataset)//hps.train.batch_size) as progress_bar: + for batch in progress_bar: + model.zero_grad() + + lengths = batch['lengths'].cuda() + vec = batch['vec'].cuda() + pit = batch['pit'].cuda() + spk = batch['spk'].cuda() + mel = batch['mel'].cuda() + + prior_loss, diff_loss = model.compute_loss(lengths, vec, pit, spk, + mel, out_size=out_size) + loss = sum([prior_loss, diff_loss]) + loss.backward() + + enc_grad_norm = torch.nn.utils.clip_grad_norm_(model.encoder.parameters(), + max_norm=1) + dec_grad_norm = torch.nn.utils.clip_grad_norm_(model.decoder.parameters(), + max_norm=1) + optim.step() + + logger.add_scalar('training/prior_loss', prior_loss, + global_step=iteration) + logger.add_scalar('training/diffusion_loss', diff_loss, + global_step=iteration) + logger.add_scalar('training/encoder_grad_norm', enc_grad_norm, + global_step=iteration) + logger.add_scalar('training/decoder_grad_norm', dec_grad_norm, + global_step=iteration) + + msg = f'Epoch: {epoch}, iteration: {iteration} | prior_loss: {prior_loss.item():.3f}, diff_loss: {diff_loss.item():.3f}' + progress_bar.set_description(msg) + + prior_losses.append(prior_loss.item()) + diff_losses.append(diff_loss.item()) + iteration += 1 + + msg = 'Epoch %d: ' % (epoch) + msg += '| prior loss = %.3f ' % np.mean(prior_losses) + msg += '| diffusion loss = %.3f\n' % np.mean(diff_losses) + with open(f'{hps.train.log_dir}/train.log', 'a') as f: + f.write(msg) + + if epoch % hps.train.save_step > 0: + continue + + save_path = f"{hps.train.log_dir}/grad_svc_{epoch}.pt" + torch.save({ + 'model': model.state_dict(), + 'optim': optim.state_dict(), + 'epoch': epoch, + 'steps': iteration, + + }, save_path) + logger.info("Saved checkpoint to: %s" % save_path) diff --git a/grad_extend/utils.py b/grad_extend/utils.py new file mode 100644 index 0000000..d7cf538 --- /dev/null +++ b/grad_extend/utils.py @@ -0,0 +1,73 @@ +import os +import glob +import numpy as np +import matplotlib.pyplot as plt + +import torch + + +def parse_filelist(filelist_path, split_char="|"): + with open(filelist_path, encoding='utf-8') as f: + filepaths_and_text = [line.strip().split(split_char) for line in f] + return filepaths_and_text + + +def load_model(model, saved_state_dict): + state_dict = model.state_dict() + new_state_dict = {} + for k, v in state_dict.items(): + try: + new_state_dict[k] = saved_state_dict[k] + except: + print("%s is not in the checkpoint" % k) + new_state_dict[k] = v + model.load_state_dict(new_state_dict) + return model + + +def latest_checkpoint_path(dir_path, regex="grad_svc_*.pt"): + f_list = glob.glob(os.path.join(dir_path, regex)) + f_list.sort(key=lambda f: int("".join(filter(str.isdigit, f)))) + x = f_list[-1] + return x + + +def load_checkpoint(logdir, model, num=None): + if num is None: + model_path = latest_checkpoint_path(logdir, regex="grad_svc_*.pt") + else: + model_path = os.path.join(logdir, f"grad_svc_{num}.pt") + print(f'Loading checkpoint {model_path}...') + model_dict = torch.load(model_path, map_location=lambda loc, storage: loc) + model.load_state_dict(model_dict, strict=False) + return model + + +def save_figure_to_numpy(fig): + data = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep='') + data = data.reshape(fig.canvas.get_width_height()[::-1] + (3,)) + return data + + +def plot_tensor(tensor): + plt.style.use('default') + fig, ax = plt.subplots(figsize=(12, 3)) + im = ax.imshow(tensor, aspect="auto", origin="lower", interpolation='none') + plt.colorbar(im, ax=ax) + plt.tight_layout() + fig.canvas.draw() + data = save_figure_to_numpy(fig) + plt.close() + return data + + +def save_plot(tensor, savepath): + plt.style.use('default') + fig, ax = plt.subplots(figsize=(12, 3)) + im = ax.imshow(tensor, aspect="auto", origin="lower", interpolation='none') + plt.colorbar(im, ax=ax) + plt.tight_layout() + fig.canvas.draw() + plt.savefig(savepath) + plt.close() + return diff --git a/grad_pretrain/README.md b/grad_pretrain/README.md new file mode 100644 index 0000000..8811790 --- /dev/null +++ b/grad_pretrain/README.md @@ -0,0 +1,3 @@ +Path for: + + gvc.pretrain.pth \ No newline at end of file diff --git a/gvc_export.py b/gvc_export.py new file mode 100644 index 0000000..7c58e75 --- /dev/null +++ b/gvc_export.py @@ -0,0 +1,48 @@ +import sys,os +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +import torch +import argparse +from omegaconf import OmegaConf +from grad.model import GradTTS + + +def load_model(checkpoint_path, model): + assert os.path.isfile(checkpoint_path) + checkpoint_dict = torch.load(checkpoint_path, map_location="cpu") + saved_state_dict = checkpoint_dict["model"] + + state_dict = model.state_dict() + new_state_dict = {} + for k, v in state_dict.items(): + try: + new_state_dict[k] = saved_state_dict[k] + except: + print("%s is not in the checkpoint" % k) + new_state_dict[k] = v + model.load_state_dict(new_state_dict) + + +def main(args): + hps = OmegaConf.load(args.config) + + print('Initializing Grad-TTS...') + model = GradTTS(hps.grad.n_mels, hps.grad.n_vecs, hps.grad.n_pits, hps.grad.n_spks, hps.grad.n_embs, + hps.grad.n_enc_channels, hps.grad.filter_channels, + hps.grad.dec_dim, hps.grad.beta_min, hps.grad.beta_max, hps.grad.pe_scale) + print('Number of encoder parameters = %.2fm' % (model.encoder.nparams/1e6)) + print('Number of decoder parameters = %.2fm' % (model.decoder.nparams/1e6)) + + load_model(args.checkpoint_path, model) + torch.save({'model': model.state_dict()}, "gvc.pth") + torch.save({'model': model.state_dict()}, "gvc.pretrain.pth") + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--config', type=str, default='./configs/base.yaml', + help="yaml file for config.") + parser.add_argument('-p', '--checkpoint_path', type=str, required=True, + help="path of checkpoint pt file for evaluation") + args = parser.parse_args() + + main(args) diff --git a/gvc_inference.py b/gvc_inference.py new file mode 100644 index 0000000..332b984 --- /dev/null +++ b/gvc_inference.py @@ -0,0 +1,153 @@ +import sys,os +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +import torch +import argparse +import numpy as np + +from omegaconf import OmegaConf +from pitch import load_csv_pitch +from spec.inference import print_mel + +from grad.utils import fix_len_compatibility +from grad.model import GradTTS + + +def load_gvc_model(checkpoint_path, model): + assert os.path.isfile(checkpoint_path) + checkpoint_dict = torch.load(checkpoint_path, map_location="cpu") + saved_state_dict = checkpoint_dict + state_dict = model.state_dict() + new_state_dict = {} + for k, v in state_dict.items(): + try: + new_state_dict[k] = saved_state_dict[k] + except: + print("%s is not in the checkpoint" % k) + new_state_dict[k] = v + model.load_state_dict(new_state_dict) + return model + + +@torch.no_grad() +def gvc_main(device, model, _vec, _pit, spk): + l_vec = _vec.shape[0] + d_vec = _vec.shape[1] + lengths_fix = fix_len_compatibility(l_vec) + lengths = torch.LongTensor([l_vec]).to(device) + vec = torch.zeros((1, lengths_fix, d_vec), dtype=torch.float32).to(device) + pit = torch.zeros((1, lengths_fix), dtype=torch.float32).to(device) + vec[0, :l_vec, :] = _vec + pit[0, :l_vec] = _pit + y_enc, y_dec = model(lengths, vec, pit, spk, n_timesteps=50) + y_dec = y_dec.squeeze(0) + y_dec = y_dec[:, :l_vec] + return y_dec + + +def main(args): + + if (args.vec == None): + args.vec = "gvc_tmp.vec.npy" + print( + f"Auto run : python hubert/inference.py -w {args.wave} -v {args.vec}") + os.system(f"python hubert/inference.py -w {args.wave} -v {args.vec}") + + if (args.pit == None): + args.pit = "gvc_tmp.pit.csv" + print( + f"Auto run : python pitch/inference.py -w {args.wave} -p {args.pit}") + os.system(f"python pitch/inference.py -w {args.wave} -p {args.pit}") + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + hps = OmegaConf.load(args.config) + + print('Initializing Grad-TTS...') + model = GradTTS(hps.grad.n_mels, hps.grad.n_vecs, hps.grad.n_pits, hps.grad.n_spks, hps.grad.n_embs, + hps.grad.n_enc_channels, hps.grad.filter_channels, + hps.grad.dec_dim, hps.grad.beta_min, hps.grad.beta_max, hps.grad.pe_scale) + print('Number of encoder parameters = %.2fm' % (model.encoder.nparams/1e6)) + print('Number of decoder parameters = %.2fm' % (model.decoder.nparams/1e6)) + + load_gvc_model(args.model, model) + model.eval() + model.to(device) + + spk = np.load(args.spk) + spk = torch.FloatTensor(spk) + + vec = np.load(args.vec) + vec = np.repeat(vec, 2, 0) + vec = torch.FloatTensor(vec) + + pit = load_csv_pitch(args.pit) + pit = np.array(pit) + pit = pit * 2 ** (args.shift / 12) + pit = torch.FloatTensor(pit) + + len_pit = pit.size()[0] + len_vec = vec.size()[0] + len_min = min(len_pit, len_vec) + pit = pit[:len_min] + vec = vec[:len_min, :] + + with torch.no_grad(): + spk = spk.unsqueeze(0).to(device) + + all_frame = len_min + hop_frame = 8 + out_chunk = 2400 # 24 S + out_index = 0 + out_mel = None + + while (out_index < all_frame): + if (out_index == 0): # start frame + cut_s = 0 + cut_s_out = 0 + else: + cut_s = out_index - hop_frame + cut_s_out = hop_frame + + if (out_index + out_chunk + hop_frame > all_frame): # end frame + cut_e = all_frame + cut_e_out = -1 + else: + cut_e = out_index + out_chunk + hop_frame + cut_e_out = -1 * hop_frame + + sub_vec = vec[cut_s:cut_e, :].to(device) + sub_pit = pit[cut_s:cut_e].to(device) + + sub_out = gvc_main(device, model, sub_vec, sub_pit, spk) + sub_out = sub_out[:, cut_s_out:cut_e_out] + + out_index = out_index + out_chunk + if out_mel == None: + out_mel = sub_out + else: + out_mel = torch.cat((out_mel, sub_out), -1) + if cut_e == all_frame: + break + + torch.save(out_mel, "gvc_out.mel.pt") + print_mel(out_mel, "gvc_out.mel.png") + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--config', type=str, default='./configs/base.yaml', + help="yaml file for config.") + parser.add_argument('--model', type=str, required=True, + help="path of model for evaluation") + parser.add_argument('--wave', type=str, required=True, + help="Path of raw audio.") + parser.add_argument('--spk', type=str, required=True, + help="Path of speaker.") + parser.add_argument('--vec', type=str, + help="Path of hubert vector.") + parser.add_argument('--pit', type=str, + help="Path of pitch csv file.") + parser.add_argument('--shift', type=int, default=0, + help="Pitch shift key.") + args = parser.parse_args() + + main(args) diff --git a/gvc_inference_wave.py b/gvc_inference_wave.py new file mode 100644 index 0000000..9ed17b8 --- /dev/null +++ b/gvc_inference_wave.py @@ -0,0 +1,71 @@ +import sys,os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import torch +import argparse + +from omegaconf import OmegaConf +from scipy.io.wavfile import write +from bigvgan.model.generator import Generator +from pitch import load_csv_pitch + + +def load_bigv_model(checkpoint_path, model): + assert os.path.isfile(checkpoint_path) + checkpoint_dict = torch.load(checkpoint_path, map_location="cpu") + saved_state_dict = checkpoint_dict["model_g"] + state_dict = model.state_dict() + new_state_dict = {} + for k, v in state_dict.items(): + try: + new_state_dict[k] = saved_state_dict[k] + except: + print("%s is not in the checkpoint" % k) + new_state_dict[k] = v + model.load_state_dict(new_state_dict) + return model + + +def main(args): + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + hp = OmegaConf.load(args.config) + model = Generator(hp) + load_bigv_model(args.model, model) + model.eval() + model.to(device) + + mel = torch.load(args.mel) + + pit = load_csv_pitch(args.pit) + pit = torch.FloatTensor(pit) + + len_pit = pit.size()[0] + len_mel = mel.size()[1] + len_min = min(len_pit, len_mel) + pit = pit[:len_min] + mel = mel[:, :len_min] + + with torch.no_grad(): + mel = mel.unsqueeze(0).to(device) + pit = pit.unsqueeze(0).to(device) + audio = model.inference(mel, pit) + audio = audio.cpu().detach().numpy() + + pitwav = model.pitch2wav(pit) + pitwav = pitwav.cpu().detach().numpy() + + write("gvc_out.wav", hp.audio.sampling_rate, audio) + write("gvc_pitch.wav", hp.audio.sampling_rate, pitwav) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--mel', type=str, + help="Path of content vector.") + parser.add_argument('--pit', type=str, + help="Path of pitch csv file.") + args = parser.parse_args() + + args.config = "./bigvgan/configs/nsf_bigvgan.yaml" + args.model = "./bigvgan_pretrain/nsf_bigvgan_pretrain_32K.pth" + + main(args) diff --git a/gvc_trainer.py b/gvc_trainer.py new file mode 100644 index 0000000..4802ada --- /dev/null +++ b/gvc_trainer.py @@ -0,0 +1,30 @@ +import sys,os +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +import argparse +import torch +import numpy as np + +from omegaconf import OmegaConf +from grad_extend.train import train + +torch.backends.cudnn.benchmark = True + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--config', type=str, default='./configs/base.yaml', + help="yaml file for configuration") + parser.add_argument('-p', '--checkpoint_path', type=str, default=None, + help="path of checkpoint pt file to resume training") + args = parser.parse_args() + + assert torch.cuda.is_available() + print('Numbers of GPU :', torch.cuda.device_count()) + + hps = OmegaConf.load(args.config) + + np.random.seed(hps.train.seed) + torch.manual_seed(hps.train.seed) + torch.cuda.manual_seed(hps.train.seed) + + train(hps, args.checkpoint_path) diff --git a/hubert/__init__.py b/hubert/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hubert/hubert_model.py b/hubert/hubert_model.py new file mode 100644 index 0000000..09df44c --- /dev/null +++ b/hubert/hubert_model.py @@ -0,0 +1,229 @@ +import copy +import random +from typing import Optional, Tuple + +import torch +import torch.nn as nn +import torch.nn.functional as t_func + + +class Hubert(nn.Module): + def __init__(self, num_label_embeddings: int = 100, mask: bool = True): + super().__init__() + self._mask = mask + self.feature_extractor = FeatureExtractor() + self.feature_projection = FeatureProjection() + self.positional_embedding = PositionalConvEmbedding() + self.norm = nn.LayerNorm(768) + self.dropout = nn.Dropout(0.1) + self.encoder = TransformerEncoder( + nn.TransformerEncoderLayer( + 768, 12, 3072, activation="gelu", batch_first=True + ), + 12, + ) + self.proj = nn.Linear(768, 256) + + self.masked_spec_embed = nn.Parameter(torch.FloatTensor(768).uniform_()) + self.label_embedding = nn.Embedding(num_label_embeddings, 256) + + def mask(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + mask = None + if self.training and self._mask: + mask = _compute_mask((x.size(0), x.size(1)), 0.8, 10, x.device, 2) + x[mask] = self.masked_spec_embed.to(x.dtype) + return x, mask + + def encode( + self, x: torch.Tensor, layer: Optional[int] = None + ) -> Tuple[torch.Tensor, torch.Tensor]: + x = self.feature_extractor(x) + x = self.feature_projection(x.transpose(1, 2)) + x, mask = self.mask(x) + x = x + self.positional_embedding(x) + x = self.dropout(self.norm(x)) + x = self.encoder(x, output_layer=layer) + return x, mask + + def logits(self, x: torch.Tensor) -> torch.Tensor: + logits = torch.cosine_similarity( + x.unsqueeze(2), + self.label_embedding.weight.unsqueeze(0).unsqueeze(0), + dim=-1, + ) + return logits / 0.1 + + def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + x, mask = self.encode(x) + x = self.proj(x) + logits = self.logits(x) + return logits, mask + + +class HubertSoft(Hubert): + def __init__(self): + super().__init__() + + @torch.inference_mode() + def units(self, wav: torch.Tensor) -> torch.Tensor: + wav = t_func.pad(wav, ((400 - 320) // 2, (400 - 320) // 2)) + x, _ = self.encode(wav) + return self.proj(x) + + +class FeatureExtractor(nn.Module): + def __init__(self): + super().__init__() + self.conv0 = nn.Conv1d(1, 512, 10, 5, bias=False) + self.norm0 = nn.GroupNorm(512, 512) + self.conv1 = nn.Conv1d(512, 512, 3, 2, bias=False) + self.conv2 = nn.Conv1d(512, 512, 3, 2, bias=False) + self.conv3 = nn.Conv1d(512, 512, 3, 2, bias=False) + self.conv4 = nn.Conv1d(512, 512, 3, 2, bias=False) + self.conv5 = nn.Conv1d(512, 512, 2, 2, bias=False) + self.conv6 = nn.Conv1d(512, 512, 2, 2, bias=False) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = t_func.gelu(self.norm0(self.conv0(x))) + x = t_func.gelu(self.conv1(x)) + x = t_func.gelu(self.conv2(x)) + x = t_func.gelu(self.conv3(x)) + x = t_func.gelu(self.conv4(x)) + x = t_func.gelu(self.conv5(x)) + x = t_func.gelu(self.conv6(x)) + return x + + +class FeatureProjection(nn.Module): + def __init__(self): + super().__init__() + self.norm = nn.LayerNorm(512) + self.projection = nn.Linear(512, 768) + self.dropout = nn.Dropout(0.1) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.norm(x) + x = self.projection(x) + x = self.dropout(x) + return x + + +class PositionalConvEmbedding(nn.Module): + def __init__(self): + super().__init__() + self.conv = nn.Conv1d( + 768, + 768, + kernel_size=128, + padding=128 // 2, + groups=16, + ) + self.conv = nn.utils.weight_norm(self.conv, name="weight", dim=2) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.conv(x.transpose(1, 2)) + x = t_func.gelu(x[:, :, :-1]) + return x.transpose(1, 2) + + +class TransformerEncoder(nn.Module): + def __init__( + self, encoder_layer: nn.TransformerEncoderLayer, num_layers: int + ) -> None: + super(TransformerEncoder, self).__init__() + self.layers = nn.ModuleList( + [copy.deepcopy(encoder_layer) for _ in range(num_layers)] + ) + self.num_layers = num_layers + + def forward( + self, + src: torch.Tensor, + mask: torch.Tensor = None, + src_key_padding_mask: torch.Tensor = None, + output_layer: Optional[int] = None, + ) -> torch.Tensor: + output = src + for layer in self.layers[:output_layer]: + output = layer( + output, src_mask=mask, src_key_padding_mask=src_key_padding_mask + ) + return output + + +def _compute_mask( + shape: Tuple[int, int], + mask_prob: float, + mask_length: int, + device: torch.device, + min_masks: int = 0, +) -> torch.Tensor: + batch_size, sequence_length = shape + + if mask_length < 1: + raise ValueError("`mask_length` has to be bigger than 0.") + + if mask_length > sequence_length: + raise ValueError( + f"`mask_length` has to be smaller than `sequence_length`, but got `mask_length`: {mask_length} and `sequence_length`: {sequence_length}`" + ) + + # compute number of masked spans in batch + num_masked_spans = int(mask_prob * sequence_length / mask_length + random.random()) + num_masked_spans = max(num_masked_spans, min_masks) + + # make sure num masked indices <= sequence_length + if num_masked_spans * mask_length > sequence_length: + num_masked_spans = sequence_length // mask_length + + # SpecAugment mask to fill + mask = torch.zeros((batch_size, sequence_length), device=device, dtype=torch.bool) + + # uniform distribution to sample from, make sure that offset samples are < sequence_length + uniform_dist = torch.ones( + (batch_size, sequence_length - (mask_length - 1)), device=device + ) + + # get random indices to mask + mask_indices = torch.multinomial(uniform_dist, num_masked_spans) + + # expand masked indices to masked spans + mask_indices = ( + mask_indices.unsqueeze(dim=-1) + .expand((batch_size, num_masked_spans, mask_length)) + .reshape(batch_size, num_masked_spans * mask_length) + ) + offsets = ( + torch.arange(mask_length, device=device)[None, None, :] + .expand((batch_size, num_masked_spans, mask_length)) + .reshape(batch_size, num_masked_spans * mask_length) + ) + mask_idxs = mask_indices + offsets + + # scatter indices to mask + mask = mask.scatter(1, mask_idxs, True) + + return mask + + +def consume_prefix(state_dict, prefix: str) -> None: + keys = sorted(state_dict.keys()) + for key in keys: + if key.startswith(prefix): + newkey = key[len(prefix):] + state_dict[newkey] = state_dict.pop(key) + + +def hubert_soft( + path: str, +) -> HubertSoft: + r"""HuBERT-Soft from `"A Comparison of Discrete and Soft Speech Units for Improved Voice Conversion"`. + Args: + path (str): path of a pretrained model + """ + hubert = HubertSoft() + checkpoint = torch.load(path) + consume_prefix(checkpoint, "module.") + hubert.load_state_dict(checkpoint) + hubert.eval() + return hubert diff --git a/hubert/inference.py b/hubert/inference.py new file mode 100644 index 0000000..a40bdeb --- /dev/null +++ b/hubert/inference.py @@ -0,0 +1,67 @@ +import sys,os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import numpy as np +import argparse +import torch +import librosa + +from hubert import hubert_model + + +def load_audio(file: str, sr: int = 16000): + x, sr = librosa.load(file, sr=sr) + return x + + +def load_model(path, device): + model = hubert_model.hubert_soft(path) + model.eval() + if not (device == "cpu"): + model.half() + model.to(device) + return model + + +def pred_vec(model, wavPath, vecPath, device): + audio = load_audio(wavPath) + audln = audio.shape[0] + vec_a = [] + idx_s = 0 + while (idx_s + 20 * 16000 < audln): + feats = audio[idx_s:idx_s + 20 * 16000] + feats = torch.from_numpy(feats).to(device) + feats = feats[None, None, :] + if not (device == "cpu"): + feats = feats.half() + with torch.no_grad(): + vec = model.units(feats).squeeze().data.cpu().float().numpy() + vec_a.extend(vec) + idx_s = idx_s + 20 * 16000 + if (idx_s < audln): + feats = audio[idx_s:audln] + feats = torch.from_numpy(feats).to(device) + feats = feats[None, None, :] + if not (device == "cpu"): + feats = feats.half() + with torch.no_grad(): + vec = model.units(feats).squeeze().data.cpu().float().numpy() + # print(vec.shape) # [length, dim=256] hop=320 + vec_a.extend(vec) + np.save(vecPath, vec_a, allow_pickle=False) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-w", "--wav", help="wav", dest="wav") + parser.add_argument("-v", "--vec", help="vec", dest="vec") + args = parser.parse_args() + print(args.wav) + print(args.vec) + + wavPath = args.wav + vecPath = args.vec + + device = "cuda" if torch.cuda.is_available() else "cpu" + hubert = load_model(os.path.join( + "hubert_pretrain", "hubert-soft-0d54a1f4.pt"), device) + pred_vec(hubert, wavPath, vecPath, device) diff --git a/hubert_pretrain/README.md b/hubert_pretrain/README.md new file mode 100644 index 0000000..dbecfeb --- /dev/null +++ b/hubert_pretrain/README.md @@ -0,0 +1,3 @@ +Path for: + + hubert-soft-0d54a1f4.pt \ No newline at end of file diff --git a/pitch/__init__.py b/pitch/__init__.py new file mode 100644 index 0000000..bc41814 --- /dev/null +++ b/pitch/__init__.py @@ -0,0 +1 @@ +from .inference import load_csv_pitch \ No newline at end of file diff --git a/pitch/inference.py b/pitch/inference.py new file mode 100644 index 0000000..f5c6309 --- /dev/null +++ b/pitch/inference.py @@ -0,0 +1,54 @@ +import sys,os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import librosa +import argparse +import numpy as np +import parselmouth +# pip install praat-parselmouth + +def compute_f0_mouth(path): + x, sr = librosa.load(path, sr=16000) + assert sr == 16000 + lpad = 1024 // 160 + rpad = lpad + f0 = parselmouth.Sound(x, sr).to_pitch_ac( + time_step=160 / sr, + voicing_threshold=0.5, + pitch_floor=30, + pitch_ceiling=1000).selected_array['frequency'] + f0 = np.pad(f0, [[lpad, rpad]], mode='constant') + return f0 + + +def save_csv_pitch(pitch, path): + with open(path, "w", encoding='utf-8') as pitch_file: + for i in range(len(pitch)): + t = i * 10 + minute = t // 60000 + seconds = (t - minute * 60000) // 1000 + millisecond = t % 1000 + print( + f"{minute}m {seconds}s {millisecond:3d},{int(pitch[i])}", file=pitch_file) + + +def load_csv_pitch(path): + pitch = [] + with open(path, "r", encoding='utf-8') as pitch_file: + for line in pitch_file.readlines(): + pit = line.strip().split(",")[-1] + pitch.append(int(pit)) + return pitch + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-w", "--wav", help="wav", dest="wav") + parser.add_argument("-p", "--pit", help="pit", dest="pit") # csv for excel + args = parser.parse_args() + print(args.wav) + print(args.pit) + + pitch = compute_f0_mouth(args.wav) + save_csv_pitch(pitch, args.pit) + #tmp = load_csv_pitch(args.pit) + #save_csv_pitch(tmp, "tmp.csv") diff --git a/prepare/preprocess_a.py b/prepare/preprocess_a.py new file mode 100644 index 0000000..87d03b5 --- /dev/null +++ b/prepare/preprocess_a.py @@ -0,0 +1,58 @@ +import os +import librosa +import argparse +import numpy as np +from tqdm import tqdm +from concurrent.futures import ThreadPoolExecutor, as_completed +from scipy.io import wavfile + + +def resample_wave(wav_in, wav_out, sample_rate): + wav, _ = librosa.load(wav_in, sr=sample_rate) + wav = wav / np.abs(wav).max() * 0.6 + wav = wav / max(0.01, np.max(np.abs(wav))) * 32767 * 0.6 + wavfile.write(wav_out, sample_rate, wav.astype(np.int16)) + + +def process_file(file, wavPath, spks, outPath, sr): + if file.endswith(".wav"): + file = file[:-4] + resample_wave(f"{wavPath}/{spks}/{file}.wav", f"{outPath}/{spks}/{file}.wav", sr) + + +def process_files_with_thread_pool(wavPath, spks, outPath, sr, thread_num=None): + files = [f for f in os.listdir(f"./{wavPath}/{spks}") if f.endswith(".wav")] + + with ThreadPoolExecutor(max_workers=thread_num) as executor: + futures = {executor.submit(process_file, file, wavPath, spks, outPath, sr): file for file in files} + + for future in tqdm(as_completed(futures), total=len(futures), desc=f'Processing {sr} {spks}'): + future.result() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-w", "--wav", help="wav", dest="wav", required=True) + parser.add_argument("-o", "--out", help="out", dest="out", required=True) + parser.add_argument("-s", "--sr", help="sample rate", dest="sr", type=int, required=True) + parser.add_argument("-t", "--thread_count", help="thread count to process, set 0 to use all cpu cores", dest="thread_count", type=int, default=1) + + args = parser.parse_args() + print(args.wav) + print(args.out) + print(args.sr) + + os.makedirs(args.out, exist_ok=True) + wavPath = args.wav + outPath = args.out + + assert args.sr == 16000 or args.sr == 32000 + + for spks in os.listdir(wavPath): + if os.path.isdir(f"./{wavPath}/{spks}"): + os.makedirs(f"./{outPath}/{spks}", exist_ok=True) + if args.thread_count == 0: + process_num = os.cpu_count() // 2 + 1 + else: + process_num = args.thread_count + process_files_with_thread_pool(wavPath, spks, outPath, args.sr, process_num) diff --git a/prepare/preprocess_f0.py b/prepare/preprocess_f0.py new file mode 100644 index 0000000..bdae0a0 --- /dev/null +++ b/prepare/preprocess_f0.py @@ -0,0 +1,64 @@ +import os +import numpy as np +import librosa +import argparse +import parselmouth +# pip install praat-parselmouth +from tqdm import tqdm +from concurrent.futures import ProcessPoolExecutor, as_completed + + +def compute_f0(path, save): + x, sr = librosa.load(path, sr=16000) + assert sr == 16000 + lpad = 1024 // 160 + rpad = lpad + f0 = parselmouth.Sound(x, sr).to_pitch_ac( + time_step=160 / sr, + voicing_threshold=0.5, + pitch_floor=30, + pitch_ceiling=1000).selected_array['frequency'] + f0 = np.pad(f0, [[lpad, rpad]], mode='constant') + for index, pitch in enumerate(f0): + f0[index] = round(pitch, 1) + np.save(save, f0, allow_pickle=False) + + +def process_file(file, wavPath, spks, pitPath): + if file.endswith(".wav"): + file = file[:-4] + compute_f0(f"{wavPath}/{spks}/{file}.wav", f"{pitPath}/{spks}/{file}.pit") + + +def process_files_with_process_pool(wavPath, spks, pitPath, process_num=None): + files = [f for f in os.listdir(f"./{wavPath}/{spks}") if f.endswith(".wav")] + + with ProcessPoolExecutor(max_workers=process_num) as executor: + futures = {executor.submit(process_file, file, wavPath, spks, pitPath): file for file in files} + + for future in tqdm(as_completed(futures), total=len(futures), desc=f'Processing f0 {spks}'): + future.result() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-w", "--wav", help="wav", dest="wav", required=True) + parser.add_argument("-p", "--pit", help="pit", dest="pit", required=True) + parser.add_argument("-t", "--thread_count", help="thread count to process, set 0 to use all cpu cores", dest="thread_count", type=int, default=1) + + args = parser.parse_args() + print(args.wav) + print(args.pit) + + os.makedirs(args.pit, exist_ok=True) + wavPath = args.wav + pitPath = args.pit + + for spks in os.listdir(wavPath): + if os.path.isdir(f"./{wavPath}/{spks}"): + os.makedirs(f"./{pitPath}/{spks}", exist_ok=True) + if args.thread_count == 0: + process_num = os.cpu_count() // 2 + 1 + else: + process_num = args.thread_count + process_files_with_process_pool(wavPath, spks, pitPath, process_num) diff --git a/prepare/preprocess_hubert.py b/prepare/preprocess_hubert.py new file mode 100644 index 0000000..dd4265b --- /dev/null +++ b/prepare/preprocess_hubert.py @@ -0,0 +1,58 @@ +import sys,os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import numpy as np +import argparse +import torch +import librosa + +from tqdm import tqdm +from hubert import hubert_model + + +def load_audio(file: str, sr: int = 16000): + x, sr = librosa.load(file, sr=sr) + return x + + +def load_model(path, device): + model = hubert_model.hubert_soft(path) + model.eval() + model.half() + model.to(device) + return model + + +def pred_vec(model, wavPath, vecPath, device): + feats = load_audio(wavPath) + feats = torch.from_numpy(feats).to(device) + feats = feats[None, None, :].half() + with torch.no_grad(): + vec = model.units(feats).squeeze().data.cpu().float().numpy() + # print(vec.shape) # [length, dim=256] hop=320 + np.save(vecPath, vec, allow_pickle=False) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-w", "--wav", help="wav", dest="wav", required=True) + parser.add_argument("-v", "--vec", help="vec", dest="vec", required=True) + + args = parser.parse_args() + print(args.wav) + print(args.vec) + os.makedirs(args.vec, exist_ok=True) + + wavPath = args.wav + vecPath = args.vec + + device = "cuda" if torch.cuda.is_available() else "cpu" + hubert = load_model(os.path.join("hubert_pretrain", "hubert-soft-0d54a1f4.pt"), device) + + for spks in os.listdir(wavPath): + if os.path.isdir(f"./{wavPath}/{spks}"): + os.makedirs(f"./{vecPath}/{spks}", exist_ok=True) + + files = [f for f in os.listdir(f"./{wavPath}/{spks}") if f.endswith(".wav")] + for file in tqdm(files, desc=f'Processing vec {spks}'): + file = file[:-4] + pred_vec(hubert, f"{wavPath}/{spks}/{file}.wav", f"{vecPath}/{spks}/{file}.vec", device) diff --git a/prepare/preprocess_random.py b/prepare/preprocess_random.py new file mode 100644 index 0000000..f84977b --- /dev/null +++ b/prepare/preprocess_random.py @@ -0,0 +1,23 @@ +import random + + +if __name__ == "__main__": + all_items = [] + fo = open("./files/train_all.txt", "r+", encoding='utf-8') + while (True): + try: + item = fo.readline().strip() + except Exception as e: + print('nothing of except:', e) + break + if (item == None or item == ""): + break + all_items.append(item) + fo.close() + + random.shuffle(all_items) + + fw = open("./files/train_all.txt", "w", encoding="utf-8") + for strs in all_items: + print(strs, file=fw) + fw.close() diff --git a/prepare/preprocess_speaker.py b/prepare/preprocess_speaker.py new file mode 100644 index 0000000..797b60e --- /dev/null +++ b/prepare/preprocess_speaker.py @@ -0,0 +1,103 @@ +import sys,os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import torch +import numpy as np +import argparse + +from tqdm import tqdm +from functools import partial +from argparse import RawTextHelpFormatter +from multiprocessing.pool import ThreadPool + +from speaker.models.lstm import LSTMSpeakerEncoder +from speaker.config import SpeakerEncoderConfig +from speaker.utils.audio import AudioProcessor +from speaker.infer import read_json + + +def get_spk_wavs(dataset_path, output_path): + wav_files = [] + os.makedirs(f"./{output_path}", exist_ok=True) + for spks in os.listdir(dataset_path): + if os.path.isdir(f"./{dataset_path}/{spks}"): + os.makedirs(f"./{output_path}/{spks}", exist_ok=True) + for file in os.listdir(f"./{dataset_path}/{spks}"): + if file.endswith(".wav"): + wav_files.append(f"./{dataset_path}/{spks}/{file}") + elif spks.endswith(".wav"): + wav_files.append(f"./{dataset_path}/{spks}") + return wav_files + + +def process_wav(wav_file, dataset_path, output_path, args, speaker_encoder_ap, speaker_encoder): + waveform = speaker_encoder_ap.load_wav( + wav_file, sr=speaker_encoder_ap.sample_rate + ) + spec = speaker_encoder_ap.melspectrogram(waveform) + spec = torch.from_numpy(spec.T) + if args.use_cuda: + spec = spec.cuda() + spec = spec.unsqueeze(0) + embed = speaker_encoder.compute_embedding(spec).detach().cpu().numpy() + embed = embed.squeeze() + embed_path = wav_file.replace(dataset_path, output_path) + embed_path = embed_path.replace(".wav", ".spk") + np.save(embed_path, embed, allow_pickle=False) + + +def extract_speaker_embeddings(wav_files, dataset_path, output_path, args, speaker_encoder_ap, speaker_encoder, concurrency): + bound_process_wav = partial(process_wav, dataset_path=dataset_path, output_path=output_path, args=args, speaker_encoder_ap=speaker_encoder_ap, speaker_encoder=speaker_encoder) + + with ThreadPool(concurrency) as pool: + list(tqdm(pool.imap(bound_process_wav, wav_files), total=len(wav_files))) + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + description="""Compute embedding vectors for each wav file in a dataset.""", + formatter_class=RawTextHelpFormatter, + ) + parser.add_argument("dataset_path", type=str, help="Path to dataset waves.") + parser.add_argument( + "output_path", type=str, help="path for output speaker/speaker_wavs.npy." + ) + parser.add_argument("--use_cuda", type=bool, help="flag to set cuda.", default=True) + parser.add_argument("-t", "--thread_count", help="thread count to process, set 0 to use all cpu cores", dest="thread_count", type=int, default=1) + args = parser.parse_args() + dataset_path = args.dataset_path + output_path = args.output_path + thread_count = args.thread_count + # model + args.model_path = os.path.join("speaker_pretrain", "best_model.pth.tar") + args.config_path = os.path.join("speaker_pretrain", "config.json") + # config + config_dict = read_json(args.config_path) + + # model + config = SpeakerEncoderConfig(config_dict) + config.from_dict(config_dict) + + speaker_encoder = LSTMSpeakerEncoder( + config.model_params["input_dim"], + config.model_params["proj_dim"], + config.model_params["lstm_dim"], + config.model_params["num_lstm_layers"], + ) + + speaker_encoder.load_checkpoint(args.model_path, eval=True, use_cuda=args.use_cuda) + + # preprocess + speaker_encoder_ap = AudioProcessor(**config.audio) + # normalize the input audio level and trim silences + speaker_encoder_ap.do_sound_norm = True + speaker_encoder_ap.do_trim_silence = True + + wav_files = get_spk_wavs(dataset_path, output_path) + + if thread_count == 0: + process_num = os.cpu_count() + else: + process_num = thread_count + + extract_speaker_embeddings(wav_files, dataset_path, output_path, args, speaker_encoder_ap, speaker_encoder, process_num) \ No newline at end of file diff --git a/prepare/preprocess_speaker_ave.py b/prepare/preprocess_speaker_ave.py new file mode 100644 index 0000000..9423f61 --- /dev/null +++ b/prepare/preprocess_speaker_ave.py @@ -0,0 +1,54 @@ +import os +import torch +import argparse +import numpy as np +from tqdm import tqdm + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("dataset_speaker", type=str) + parser.add_argument("dataset_singer", type=str) + + data_speaker = parser.parse_args().dataset_speaker + data_singer = parser.parse_args().dataset_singer + + os.makedirs(data_singer, exist_ok=True) + + for speaker in os.listdir(data_speaker): + subfile_num = 0 + speaker_ave = 0 + + for file in tqdm(os.listdir(os.path.join(data_speaker, speaker)), desc=f"average {speaker}"): + if not file.endswith(".npy"): + continue + source_embed = np.load(os.path.join(data_speaker, speaker, file)) + source_embed = source_embed.astype(np.float32) + speaker_ave = speaker_ave + source_embed + subfile_num = subfile_num + 1 + if subfile_num == 0: + continue + speaker_ave = speaker_ave / subfile_num + + np.save(os.path.join(data_singer, f"{speaker}.spk.npy"), + speaker_ave, allow_pickle=False) + + # rewrite timbre code by average, if similarity is larger than cmp_val + rewrite_timbre_code = False + if not rewrite_timbre_code: + continue + cmp_src = torch.FloatTensor(speaker_ave) + cmp_num = 0 + cmp_val = 0.85 + for file in tqdm(os.listdir(os.path.join(data_speaker, speaker)), desc=f"rewrite {speaker}"): + if not file.endswith(".npy"): + continue + cmp_tmp = np.load(os.path.join(data_speaker, speaker, file)) + cmp_tmp = cmp_tmp.astype(np.float32) + cmp_tmp = torch.FloatTensor(cmp_tmp) + cmp_cos = torch.cosine_similarity(cmp_src, cmp_tmp, dim=0) + if (cmp_cos > cmp_val): + cmp_num += 1 + np.save(os.path.join(data_speaker, speaker, file), + speaker_ave, allow_pickle=False) + print(f"rewrite timbre for {speaker} with :", cmp_num) diff --git a/prepare/preprocess_spec.py b/prepare/preprocess_spec.py new file mode 100644 index 0000000..8304f15 --- /dev/null +++ b/prepare/preprocess_spec.py @@ -0,0 +1,52 @@ +import sys,os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import torch +import argparse +from concurrent.futures import ThreadPoolExecutor +from spec.inference import mel_spectrogram_file +from tqdm import tqdm +from omegaconf import OmegaConf + + +def compute_spec(hps, filename, specname): + spec = mel_spectrogram_file(filename, hps) + spec = torch.squeeze(spec, 0) + # print(spec.shape) + torch.save(spec, specname) + + +def process_file(file): + if file.endswith(".wav"): + file = file[:-4] + compute_spec(hps, f"{wavPath}/{spks}/{file}.wav", f"{spePath}/{spks}/{file}.mel.pt") + + +def process_files_with_thread_pool(wavPath, spks, thread_num): + files = os.listdir(f"./{wavPath}/{spks}") + with ThreadPoolExecutor(max_workers=thread_num) as executor: + list(tqdm(executor.map(process_file, files), total=len(files), desc=f'Processing spec {spks}')) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-w", "--wav", help="wav", dest="wav", required=True) + parser.add_argument("-s", "--spe", help="spe", dest="spe", required=True) + parser.add_argument("-t", "--thread_count", help="thread count to process, set 0 to use all cpu cores", dest="thread_count", type=int, default=1) + + args = parser.parse_args() + print(args.wav) + print(args.spe) + + os.makedirs(args.spe, exist_ok=True) + wavPath = args.wav + spePath = args.spe + hps = OmegaConf.load("./configs/base.yaml") + + for spks in os.listdir(wavPath): + if os.path.isdir(f"./{wavPath}/{spks}"): + os.makedirs(f"./{spePath}/{spks}", exist_ok=True) + if args.thread_count == 0: + process_num = os.cpu_count() // 2 + 1 + else: + process_num = args.thread_count + process_files_with_thread_pool(wavPath, spks, process_num) diff --git a/prepare/preprocess_train.py b/prepare/preprocess_train.py new file mode 100644 index 0000000..6410a0b --- /dev/null +++ b/prepare/preprocess_train.py @@ -0,0 +1,56 @@ +import os +import random + + +def print_error(info): + print(f"\033[31m File isn't existed: {info}\033[0m") + + +if __name__ == "__main__": + os.makedirs("./files/", exist_ok=True) + + rootPath = "./data_svc/waves-32k/" + all_items = [] + for spks in os.listdir(f"./{rootPath}"): + if not os.path.isdir(f"./{rootPath}/{spks}"): + continue + print(f"./{rootPath}/{spks}") + for file in os.listdir(f"./{rootPath}/{spks}"): + if file.endswith(".wav"): + file = file[:-4] + + path_mel = f"./data_svc/mel/{spks}/{file}.mel.pt" + path_vec = f"./data_svc/hubert/{spks}/{file}.vec.npy" + path_pit = f"./data_svc/pitch/{spks}/{file}.pit.npy" + path_spk = f"./data_svc/speaker/{spks}/{file}.spk.npy" + + has_error = 0 + if not os.path.isfile(path_mel): + print_error(path_mel) + has_error = 1 + if not os.path.isfile(path_vec): + print_error(path_vec) + has_error = 1 + if not os.path.isfile(path_pit): + print_error(path_pit) + has_error = 1 + if not os.path.isfile(path_spk): + print_error(path_spk) + has_error = 1 + if has_error == 0: + all_items.append( + f"{path_mel}|{path_vec}|{path_pit}|{path_spk}") + + random.shuffle(all_items) + valids = all_items[:10] + valids.sort() + trains = all_items[10:] + # trains.sort() + fw = open("./files/valid.txt", "w", encoding="utf-8") + for strs in valids: + print(strs, file=fw) + fw.close() + fw = open("./files/train.txt", "w", encoding="utf-8") + for strs in trains: + print(strs, file=fw) + fw.close() diff --git a/prepare/preprocess_zzz.py b/prepare/preprocess_zzz.py new file mode 100644 index 0000000..03659e2 --- /dev/null +++ b/prepare/preprocess_zzz.py @@ -0,0 +1,30 @@ +import sys,os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from tqdm import tqdm +from torch.utils.data import DataLoader +from grad_extend.data import TextMelSpeakerDataset, TextMelSpeakerBatchCollate + + +filelist_path = "files/valid.txt" + +dataset = TextMelSpeakerDataset(filelist_path) +collate = TextMelSpeakerBatchCollate() +loader = DataLoader(dataset=dataset, + batch_size=2, + collate_fn=collate, + drop_last=True, + num_workers=1, + shuffle=True) + +for batch in tqdm(loader): + lengths = batch['lengths'].cuda() + vec = batch['vec'].cuda() + pit = batch['pit'].cuda() + spk = batch['spk'].cuda() + mel = batch['mel'].cuda() + + print('len', lengths.shape) + print('vec', vec.shape) + print('pit', pit.shape) + print('spk', spk.shape) + print('mel', mel.shape) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..908feb5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +librosa +soundfile +matplotlib +tensorboard +transformers +tqdm +einops +fsspec +omegaconf +praat-parselmouth \ No newline at end of file diff --git a/speaker/__init__.py b/speaker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/speaker/config.py b/speaker/config.py new file mode 100644 index 0000000..7172ee2 --- /dev/null +++ b/speaker/config.py @@ -0,0 +1,64 @@ +from dataclasses import asdict, dataclass, field +from typing import Dict, List + +from .utils.coqpit import MISSING +from .utils.shared_configs import BaseAudioConfig, BaseDatasetConfig, BaseTrainingConfig + + +@dataclass +class SpeakerEncoderConfig(BaseTrainingConfig): + """Defines parameters for Speaker Encoder model.""" + + model: str = "speaker_encoder" + audio: BaseAudioConfig = field(default_factory=BaseAudioConfig) + datasets: List[BaseDatasetConfig] = field(default_factory=lambda: [BaseDatasetConfig()]) + # model params + model_params: Dict = field( + default_factory=lambda: { + "model_name": "lstm", + "input_dim": 80, + "proj_dim": 256, + "lstm_dim": 768, + "num_lstm_layers": 3, + "use_lstm_with_projection": True, + } + ) + + audio_augmentation: Dict = field(default_factory=lambda: {}) + + storage: Dict = field( + default_factory=lambda: { + "sample_from_storage_p": 0.66, # the probability with which we'll sample from the DataSet in-memory storage + "storage_size": 15, # the size of the in-memory storage with respect to a single batch + } + ) + + # training params + max_train_step: int = 1000000 # end training when number of training steps reaches this value. + loss: str = "angleproto" + grad_clip: float = 3.0 + lr: float = 0.0001 + lr_decay: bool = False + warmup_steps: int = 4000 + wd: float = 1e-6 + + # logging params + tb_model_param_stats: bool = False + steps_plot_stats: int = 10 + checkpoint: bool = True + save_step: int = 1000 + print_step: int = 20 + + # data loader + num_speakers_in_batch: int = MISSING + num_utters_per_speaker: int = MISSING + num_loader_workers: int = MISSING + skip_speakers: bool = False + voice_len: float = 1.6 + + def check_values(self): + super().check_values() + c = asdict(self) + assert ( + c["model_params"]["input_dim"] == self.audio.num_mels + ), " [!] model input dimendion must be equal to melspectrogram dimension." diff --git a/speaker/infer.py b/speaker/infer.py new file mode 100644 index 0000000..b69b2ee --- /dev/null +++ b/speaker/infer.py @@ -0,0 +1,108 @@ +import re +import json +import fsspec +import torch +import numpy as np +import argparse + +from argparse import RawTextHelpFormatter +from .models.lstm import LSTMSpeakerEncoder +from .config import SpeakerEncoderConfig +from .utils.audio import AudioProcessor + + +def read_json(json_path): + config_dict = {} + try: + with fsspec.open(json_path, "r", encoding="utf-8") as f: + data = json.load(f) + except json.decoder.JSONDecodeError: + # backwards compat. + data = read_json_with_comments(json_path) + config_dict.update(data) + return config_dict + + +def read_json_with_comments(json_path): + """for backward compat.""" + # fallback to json + with fsspec.open(json_path, "r", encoding="utf-8") as f: + input_str = f.read() + # handle comments + input_str = re.sub(r"\\\n", "", input_str) + input_str = re.sub(r"//.*\n", "\n", input_str) + data = json.loads(input_str) + return data + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + description="""Compute embedding vectors for each wav file in a dataset.""", + formatter_class=RawTextHelpFormatter, + ) + parser.add_argument("model_path", type=str, help="Path to model checkpoint file.") + parser.add_argument( + "config_path", + type=str, + help="Path to model config file.", + ) + + parser.add_argument("-s", "--source", help="input wave", dest="source") + parser.add_argument( + "-t", "--target", help="output 256d speaker embeddimg", dest="target" + ) + + parser.add_argument("--use_cuda", type=bool, help="flag to set cuda.", default=True) + parser.add_argument("--eval", type=bool, help="compute eval.", default=True) + + args = parser.parse_args() + source_file = args.source + target_file = args.target + + # config + config_dict = read_json(args.config_path) + # print(config_dict) + + # model + config = SpeakerEncoderConfig(config_dict) + config.from_dict(config_dict) + + speaker_encoder = LSTMSpeakerEncoder( + config.model_params["input_dim"], + config.model_params["proj_dim"], + config.model_params["lstm_dim"], + config.model_params["num_lstm_layers"], + ) + + speaker_encoder.load_checkpoint(args.model_path, eval=True, use_cuda=args.use_cuda) + + # preprocess + speaker_encoder_ap = AudioProcessor(**config.audio) + # normalize the input audio level and trim silences + speaker_encoder_ap.do_sound_norm = True + speaker_encoder_ap.do_trim_silence = True + + # compute speaker embeddings + + # extract the embedding + waveform = speaker_encoder_ap.load_wav( + source_file, sr=speaker_encoder_ap.sample_rate + ) + spec = speaker_encoder_ap.melspectrogram(waveform) + spec = torch.from_numpy(spec.T) + if args.use_cuda: + spec = spec.cuda() + spec = spec.unsqueeze(0) + embed = speaker_encoder.compute_embedding(spec).detach().cpu().numpy() + embed = embed.squeeze() + # print(embed) + # print(embed.size) + np.save(target_file, embed, allow_pickle=False) + + + if hasattr(speaker_encoder, 'module'): + state_dict = speaker_encoder.module.state_dict() + else: + state_dict = speaker_encoder.state_dict() + torch.save({'model': state_dict}, "model_small.pth") diff --git a/speaker/models/__init__.py b/speaker/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/speaker/models/lstm.py b/speaker/models/lstm.py new file mode 100644 index 0000000..45e8cce --- /dev/null +++ b/speaker/models/lstm.py @@ -0,0 +1,131 @@ +import numpy as np +import torch +from torch import nn + +from ..utils.io import load_fsspec + + +class LSTMWithProjection(nn.Module): + def __init__(self, input_size, hidden_size, proj_size): + super().__init__() + self.input_size = input_size + self.hidden_size = hidden_size + self.proj_size = proj_size + self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True) + self.linear = nn.Linear(hidden_size, proj_size, bias=False) + + def forward(self, x): + self.lstm.flatten_parameters() + o, (_, _) = self.lstm(x) + return self.linear(o) + + +class LSTMWithoutProjection(nn.Module): + def __init__(self, input_dim, lstm_dim, proj_dim, num_lstm_layers): + super().__init__() + self.lstm = nn.LSTM(input_size=input_dim, hidden_size=lstm_dim, num_layers=num_lstm_layers, batch_first=True) + self.linear = nn.Linear(lstm_dim, proj_dim, bias=True) + self.relu = nn.ReLU() + + def forward(self, x): + _, (hidden, _) = self.lstm(x) + return self.relu(self.linear(hidden[-1])) + + +class LSTMSpeakerEncoder(nn.Module): + def __init__(self, input_dim, proj_dim=256, lstm_dim=768, num_lstm_layers=3, use_lstm_with_projection=True): + super().__init__() + self.use_lstm_with_projection = use_lstm_with_projection + layers = [] + # choise LSTM layer + if use_lstm_with_projection: + layers.append(LSTMWithProjection(input_dim, lstm_dim, proj_dim)) + for _ in range(num_lstm_layers - 1): + layers.append(LSTMWithProjection(proj_dim, lstm_dim, proj_dim)) + self.layers = nn.Sequential(*layers) + else: + self.layers = LSTMWithoutProjection(input_dim, lstm_dim, proj_dim, num_lstm_layers) + + self._init_layers() + + def _init_layers(self): + for name, param in self.layers.named_parameters(): + if "bias" in name: + nn.init.constant_(param, 0.0) + elif "weight" in name: + nn.init.xavier_normal_(param) + + def forward(self, x): + # TODO: implement state passing for lstms + d = self.layers(x) + if self.use_lstm_with_projection: + d = torch.nn.functional.normalize(d[:, -1], p=2, dim=1) + else: + d = torch.nn.functional.normalize(d, p=2, dim=1) + return d + + @torch.no_grad() + def inference(self, x): + d = self.layers.forward(x) + if self.use_lstm_with_projection: + d = torch.nn.functional.normalize(d[:, -1], p=2, dim=1) + else: + d = torch.nn.functional.normalize(d, p=2, dim=1) + return d + + def compute_embedding(self, x, num_frames=250, num_eval=10, return_mean=True): + """ + Generate embeddings for a batch of utterances + x: 1xTxD + """ + max_len = x.shape[1] + + if max_len < num_frames: + num_frames = max_len + + offsets = np.linspace(0, max_len - num_frames, num=num_eval) + + frames_batch = [] + for offset in offsets: + offset = int(offset) + end_offset = int(offset + num_frames) + frames = x[:, offset:end_offset] + frames_batch.append(frames) + + frames_batch = torch.cat(frames_batch, dim=0) + embeddings = self.inference(frames_batch) + + if return_mean: + embeddings = torch.mean(embeddings, dim=0, keepdim=True) + + return embeddings + + def batch_compute_embedding(self, x, seq_lens, num_frames=160, overlap=0.5): + """ + Generate embeddings for a batch of utterances + x: BxTxD + """ + num_overlap = num_frames * overlap + max_len = x.shape[1] + embed = None + num_iters = seq_lens / (num_frames - num_overlap) + cur_iter = 0 + for offset in range(0, max_len, num_frames - num_overlap): + cur_iter += 1 + end_offset = min(x.shape[1], offset + num_frames) + frames = x[:, offset:end_offset] + if embed is None: + embed = self.inference(frames) + else: + embed[cur_iter <= num_iters, :] += self.inference(frames[cur_iter <= num_iters, :, :]) + return embed / num_iters + + # pylint: disable=unused-argument, redefined-builtin + def load_checkpoint(self, checkpoint_path: str, eval: bool = False, use_cuda: bool = False): + state = load_fsspec(checkpoint_path, map_location=torch.device("cpu")) + self.load_state_dict(state["model"]) + if use_cuda: + self.cuda() + if eval: + self.eval() + assert not self.training diff --git a/speaker/models/resnet.py b/speaker/models/resnet.py new file mode 100644 index 0000000..fcc850d --- /dev/null +++ b/speaker/models/resnet.py @@ -0,0 +1,212 @@ +import numpy as np +import torch +from torch import nn + +from TTS.utils.io import load_fsspec + + +class SELayer(nn.Module): + def __init__(self, channel, reduction=8): + super(SELayer, self).__init__() + self.avg_pool = nn.AdaptiveAvgPool2d(1) + self.fc = nn.Sequential( + nn.Linear(channel, channel // reduction), + nn.ReLU(inplace=True), + nn.Linear(channel // reduction, channel), + nn.Sigmoid(), + ) + + def forward(self, x): + b, c, _, _ = x.size() + y = self.avg_pool(x).view(b, c) + y = self.fc(y).view(b, c, 1, 1) + return x * y + + +class SEBasicBlock(nn.Module): + expansion = 1 + + def __init__(self, inplanes, planes, stride=1, downsample=None, reduction=8): + super(SEBasicBlock, self).__init__() + self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=3, stride=stride, padding=1, bias=False) + self.bn1 = nn.BatchNorm2d(planes) + self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, padding=1, bias=False) + self.bn2 = nn.BatchNorm2d(planes) + self.relu = nn.ReLU(inplace=True) + self.se = SELayer(planes, reduction) + self.downsample = downsample + self.stride = stride + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.relu(out) + out = self.bn1(out) + + out = self.conv2(out) + out = self.bn2(out) + out = self.se(out) + + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + return out + + +class ResNetSpeakerEncoder(nn.Module): + """Implementation of the model H/ASP without batch normalization in speaker embedding. This model was proposed in: https://arxiv.org/abs/2009.14153 + Adapted from: https://github.com/clovaai/voxceleb_trainer + """ + + # pylint: disable=W0102 + def __init__( + self, + input_dim=64, + proj_dim=512, + layers=[3, 4, 6, 3], + num_filters=[32, 64, 128, 256], + encoder_type="ASP", + log_input=False, + ): + super(ResNetSpeakerEncoder, self).__init__() + + self.encoder_type = encoder_type + self.input_dim = input_dim + self.log_input = log_input + self.conv1 = nn.Conv2d(1, num_filters[0], kernel_size=3, stride=1, padding=1) + self.relu = nn.ReLU(inplace=True) + self.bn1 = nn.BatchNorm2d(num_filters[0]) + + self.inplanes = num_filters[0] + self.layer1 = self.create_layer(SEBasicBlock, num_filters[0], layers[0]) + self.layer2 = self.create_layer(SEBasicBlock, num_filters[1], layers[1], stride=(2, 2)) + self.layer3 = self.create_layer(SEBasicBlock, num_filters[2], layers[2], stride=(2, 2)) + self.layer4 = self.create_layer(SEBasicBlock, num_filters[3], layers[3], stride=(2, 2)) + + self.instancenorm = nn.InstanceNorm1d(input_dim) + + outmap_size = int(self.input_dim / 8) + + self.attention = nn.Sequential( + nn.Conv1d(num_filters[3] * outmap_size, 128, kernel_size=1), + nn.ReLU(), + nn.BatchNorm1d(128), + nn.Conv1d(128, num_filters[3] * outmap_size, kernel_size=1), + nn.Softmax(dim=2), + ) + + if self.encoder_type == "SAP": + out_dim = num_filters[3] * outmap_size + elif self.encoder_type == "ASP": + out_dim = num_filters[3] * outmap_size * 2 + else: + raise ValueError("Undefined encoder") + + self.fc = nn.Linear(out_dim, proj_dim) + + self._init_layers() + + def _init_layers(self): + for m in self.modules(): + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") + elif isinstance(m, nn.BatchNorm2d): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) + + def create_layer(self, block, planes, blocks, stride=1): + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2d(self.inplanes, planes * block.expansion, kernel_size=1, stride=stride, bias=False), + nn.BatchNorm2d(planes * block.expansion), + ) + + layers = [] + layers.append(block(self.inplanes, planes, stride, downsample)) + self.inplanes = planes * block.expansion + for _ in range(1, blocks): + layers.append(block(self.inplanes, planes)) + + return nn.Sequential(*layers) + + # pylint: disable=R0201 + def new_parameter(self, *size): + out = nn.Parameter(torch.FloatTensor(*size)) + nn.init.xavier_normal_(out) + return out + + def forward(self, x, l2_norm=False): + x = x.transpose(1, 2) + with torch.no_grad(): + with torch.cuda.amp.autocast(enabled=False): + if self.log_input: + x = (x + 1e-6).log() + x = self.instancenorm(x).unsqueeze(1) + + x = self.conv1(x) + x = self.relu(x) + x = self.bn1(x) + + x = self.layer1(x) + x = self.layer2(x) + x = self.layer3(x) + x = self.layer4(x) + + x = x.reshape(x.size()[0], -1, x.size()[-1]) + + w = self.attention(x) + + if self.encoder_type == "SAP": + x = torch.sum(x * w, dim=2) + elif self.encoder_type == "ASP": + mu = torch.sum(x * w, dim=2) + sg = torch.sqrt((torch.sum((x ** 2) * w, dim=2) - mu ** 2).clamp(min=1e-5)) + x = torch.cat((mu, sg), 1) + + x = x.view(x.size()[0], -1) + x = self.fc(x) + + if l2_norm: + x = torch.nn.functional.normalize(x, p=2, dim=1) + return x + + @torch.no_grad() + def compute_embedding(self, x, num_frames=250, num_eval=10, return_mean=True): + """ + Generate embeddings for a batch of utterances + x: 1xTxD + """ + max_len = x.shape[1] + + if max_len < num_frames: + num_frames = max_len + + offsets = np.linspace(0, max_len - num_frames, num=num_eval) + + frames_batch = [] + for offset in offsets: + offset = int(offset) + end_offset = int(offset + num_frames) + frames = x[:, offset:end_offset] + frames_batch.append(frames) + + frames_batch = torch.cat(frames_batch, dim=0) + embeddings = self.forward(frames_batch, l2_norm=True) + + if return_mean: + embeddings = torch.mean(embeddings, dim=0, keepdim=True) + + return embeddings + + def load_checkpoint(self, config: dict, checkpoint_path: str, eval: bool = False, use_cuda: bool = False): + state = load_fsspec(checkpoint_path, map_location=torch.device("cpu")) + self.load_state_dict(state["model"]) + if use_cuda: + self.cuda() + if eval: + self.eval() + assert not self.training diff --git a/speaker/utils/__init__.py b/speaker/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/speaker/utils/audio.py b/speaker/utils/audio.py new file mode 100644 index 0000000..e2c9627 --- /dev/null +++ b/speaker/utils/audio.py @@ -0,0 +1,822 @@ +from typing import Dict, Tuple + +import librosa +import numpy as np +import pyworld as pw +import scipy.io.wavfile +import scipy.signal +import soundfile as sf +import torch +from torch import nn + +class StandardScaler: + """StandardScaler for mean-scale normalization with the given mean and scale values.""" + + def __init__(self, mean: np.ndarray = None, scale: np.ndarray = None) -> None: + self.mean_ = mean + self.scale_ = scale + + def set_stats(self, mean, scale): + self.mean_ = mean + self.scale_ = scale + + def reset_stats(self): + delattr(self, "mean_") + delattr(self, "scale_") + + def transform(self, X): + X = np.asarray(X) + X -= self.mean_ + X /= self.scale_ + return X + + def inverse_transform(self, X): + X = np.asarray(X) + X *= self.scale_ + X += self.mean_ + return X + +class TorchSTFT(nn.Module): # pylint: disable=abstract-method + """Some of the audio processing funtions using Torch for faster batch processing. + + TODO: Merge this with audio.py + """ + + def __init__( + self, + n_fft, + hop_length, + win_length, + pad_wav=False, + window="hann_window", + sample_rate=None, + mel_fmin=0, + mel_fmax=None, + n_mels=80, + use_mel=False, + do_amp_to_db=False, + spec_gain=1.0, + ): + super().__init__() + self.n_fft = n_fft + self.hop_length = hop_length + self.win_length = win_length + self.pad_wav = pad_wav + self.sample_rate = sample_rate + self.mel_fmin = mel_fmin + self.mel_fmax = mel_fmax + self.n_mels = n_mels + self.use_mel = use_mel + self.do_amp_to_db = do_amp_to_db + self.spec_gain = spec_gain + self.window = nn.Parameter(getattr(torch, window)(win_length), requires_grad=False) + self.mel_basis = None + if use_mel: + self._build_mel_basis() + + def __call__(self, x): + """Compute spectrogram frames by torch based stft. + + Args: + x (Tensor): input waveform + + Returns: + Tensor: spectrogram frames. + + Shapes: + x: [B x T] or [:math:`[B, 1, T]`] + """ + if x.ndim == 2: + x = x.unsqueeze(1) + if self.pad_wav: + padding = int((self.n_fft - self.hop_length) / 2) + x = torch.nn.functional.pad(x, (padding, padding), mode="reflect") + # B x D x T x 2 + o = torch.stft( + x.squeeze(1), + self.n_fft, + self.hop_length, + self.win_length, + self.window, + center=True, + pad_mode="reflect", # compatible with audio.py + normalized=False, + onesided=True, + return_complex=False, + ) + M = o[:, :, :, 0] + P = o[:, :, :, 1] + S = torch.sqrt(torch.clamp(M ** 2 + P ** 2, min=1e-8)) + if self.use_mel: + S = torch.matmul(self.mel_basis.to(x), S) + if self.do_amp_to_db: + S = self._amp_to_db(S, spec_gain=self.spec_gain) + return S + + def _build_mel_basis(self): + mel_basis = librosa.filters.mel( + sr=self.sample_rate, n_fft=self.n_fft, n_mels=self.n_mels, fmin=self.mel_fmin, fmax=self.mel_fmax + ) + self.mel_basis = torch.from_numpy(mel_basis).float() + + @staticmethod + def _amp_to_db(x, spec_gain=1.0): + return torch.log(torch.clamp(x, min=1e-5) * spec_gain) + + @staticmethod + def _db_to_amp(x, spec_gain=1.0): + return torch.exp(x) / spec_gain + + +# pylint: disable=too-many-public-methods +class AudioProcessor(object): + """Audio Processor for TTS used by all the data pipelines. + + Note: + All the class arguments are set to default values to enable a flexible initialization + of the class with the model config. They are not meaningful for all the arguments. + + Args: + sample_rate (int, optional): + target audio sampling rate. Defaults to None. + + resample (bool, optional): + enable/disable resampling of the audio clips when the target sampling rate does not match the original sampling rate. Defaults to False. + + num_mels (int, optional): + number of melspectrogram dimensions. Defaults to None. + + log_func (int, optional): + log exponent used for converting spectrogram aplitude to DB. + + min_level_db (int, optional): + minimum db threshold for the computed melspectrograms. Defaults to None. + + frame_shift_ms (int, optional): + milliseconds of frames between STFT columns. Defaults to None. + + frame_length_ms (int, optional): + milliseconds of STFT window length. Defaults to None. + + hop_length (int, optional): + number of frames between STFT columns. Used if ```frame_shift_ms``` is None. Defaults to None. + + win_length (int, optional): + STFT window length. Used if ```frame_length_ms``` is None. Defaults to None. + + ref_level_db (int, optional): + reference DB level to avoid background noise. In general <20DB corresponds to the air noise. Defaults to None. + + fft_size (int, optional): + FFT window size for STFT. Defaults to 1024. + + power (int, optional): + Exponent value applied to the spectrogram before GriffinLim. Defaults to None. + + preemphasis (float, optional): + Preemphasis coefficient. Preemphasis is disabled if == 0.0. Defaults to 0.0. + + signal_norm (bool, optional): + enable/disable signal normalization. Defaults to None. + + symmetric_norm (bool, optional): + enable/disable symmetric normalization. If set True normalization is performed in the range [-k, k] else [0, k], Defaults to None. + + max_norm (float, optional): + ```k``` defining the normalization range. Defaults to None. + + mel_fmin (int, optional): + minimum filter frequency for computing melspectrograms. Defaults to None. + + mel_fmax (int, optional): + maximum filter frequency for computing melspectrograms.. Defaults to None. + + spec_gain (int, optional): + gain applied when converting amplitude to DB. Defaults to 20. + + stft_pad_mode (str, optional): + Padding mode for STFT. Defaults to 'reflect'. + + clip_norm (bool, optional): + enable/disable clipping the our of range values in the normalized audio signal. Defaults to True. + + griffin_lim_iters (int, optional): + Number of GriffinLim iterations. Defaults to None. + + do_trim_silence (bool, optional): + enable/disable silence trimming when loading the audio signal. Defaults to False. + + trim_db (int, optional): + DB threshold used for silence trimming. Defaults to 60. + + do_sound_norm (bool, optional): + enable/disable signal normalization. Defaults to False. + + do_amp_to_db_linear (bool, optional): + enable/disable amplitude to dB conversion of linear spectrograms. Defaults to True. + + do_amp_to_db_mel (bool, optional): + enable/disable amplitude to dB conversion of mel spectrograms. Defaults to True. + + stats_path (str, optional): + Path to the computed stats file. Defaults to None. + + verbose (bool, optional): + enable/disable logging. Defaults to True. + + """ + + def __init__( + self, + sample_rate=None, + resample=False, + num_mels=None, + log_func="np.log10", + min_level_db=None, + frame_shift_ms=None, + frame_length_ms=None, + hop_length=None, + win_length=None, + ref_level_db=None, + fft_size=1024, + power=None, + preemphasis=0.0, + signal_norm=None, + symmetric_norm=None, + max_norm=None, + mel_fmin=None, + mel_fmax=None, + spec_gain=20, + stft_pad_mode="reflect", + clip_norm=True, + griffin_lim_iters=None, + do_trim_silence=False, + trim_db=60, + do_sound_norm=False, + do_amp_to_db_linear=True, + do_amp_to_db_mel=True, + stats_path=None, + verbose=True, + **_, + ): + + # setup class attributed + self.sample_rate = sample_rate + self.resample = resample + self.num_mels = num_mels + self.log_func = log_func + self.min_level_db = min_level_db or 0 + self.frame_shift_ms = frame_shift_ms + self.frame_length_ms = frame_length_ms + self.ref_level_db = ref_level_db + self.fft_size = fft_size + self.power = power + self.preemphasis = preemphasis + self.griffin_lim_iters = griffin_lim_iters + self.signal_norm = signal_norm + self.symmetric_norm = symmetric_norm + self.mel_fmin = mel_fmin or 0 + self.mel_fmax = mel_fmax + self.spec_gain = float(spec_gain) + self.stft_pad_mode = stft_pad_mode + self.max_norm = 1.0 if max_norm is None else float(max_norm) + self.clip_norm = clip_norm + self.do_trim_silence = do_trim_silence + self.trim_db = trim_db + self.do_sound_norm = do_sound_norm + self.do_amp_to_db_linear = do_amp_to_db_linear + self.do_amp_to_db_mel = do_amp_to_db_mel + self.stats_path = stats_path + # setup exp_func for db to amp conversion + if log_func == "np.log": + self.base = np.e + elif log_func == "np.log10": + self.base = 10 + else: + raise ValueError(" [!] unknown `log_func` value.") + # setup stft parameters + if hop_length is None: + # compute stft parameters from given time values + self.hop_length, self.win_length = self._stft_parameters() + else: + # use stft parameters from config file + self.hop_length = hop_length + self.win_length = win_length + assert min_level_db != 0.0, " [!] min_level_db is 0" + assert self.win_length <= self.fft_size, " [!] win_length cannot be larger than fft_size" + members = vars(self) + if verbose: + print(" > Setting up Audio Processor...") + for key, value in members.items(): + print(" | > {}:{}".format(key, value)) + # create spectrogram utils + self.mel_basis = self._build_mel_basis() + self.inv_mel_basis = np.linalg.pinv(self._build_mel_basis()) + # setup scaler + if stats_path and signal_norm: + mel_mean, mel_std, linear_mean, linear_std, _ = self.load_stats(stats_path) + self.setup_scaler(mel_mean, mel_std, linear_mean, linear_std) + self.signal_norm = True + self.max_norm = None + self.clip_norm = None + self.symmetric_norm = None + + ### setting up the parameters ### + def _build_mel_basis( + self, + ) -> np.ndarray: + """Build melspectrogram basis. + + Returns: + np.ndarray: melspectrogram basis. + """ + if self.mel_fmax is not None: + assert self.mel_fmax <= self.sample_rate // 2 + return librosa.filters.mel( + sr=self.sample_rate, n_fft=self.fft_size, n_mels=self.num_mels, fmin=self.mel_fmin, fmax=self.mel_fmax + ) + + def _stft_parameters( + self, + ) -> Tuple[int, int]: + """Compute the real STFT parameters from the time values. + + Returns: + Tuple[int, int]: hop length and window length for STFT. + """ + factor = self.frame_length_ms / self.frame_shift_ms + assert (factor).is_integer(), " [!] frame_shift_ms should divide frame_length_ms" + hop_length = int(self.frame_shift_ms / 1000.0 * self.sample_rate) + win_length = int(hop_length * factor) + return hop_length, win_length + + ### normalization ### + def normalize(self, S: np.ndarray) -> np.ndarray: + """Normalize values into `[0, self.max_norm]` or `[-self.max_norm, self.max_norm]` + + Args: + S (np.ndarray): Spectrogram to normalize. + + Raises: + RuntimeError: Mean and variance is computed from incompatible parameters. + + Returns: + np.ndarray: Normalized spectrogram. + """ + # pylint: disable=no-else-return + S = S.copy() + if self.signal_norm: + # mean-var scaling + if hasattr(self, "mel_scaler"): + if S.shape[0] == self.num_mels: + return self.mel_scaler.transform(S.T).T + elif S.shape[0] == self.fft_size / 2: + return self.linear_scaler.transform(S.T).T + else: + raise RuntimeError(" [!] Mean-Var stats does not match the given feature dimensions.") + # range normalization + S -= self.ref_level_db # discard certain range of DB assuming it is air noise + S_norm = (S - self.min_level_db) / (-self.min_level_db) + if self.symmetric_norm: + S_norm = ((2 * self.max_norm) * S_norm) - self.max_norm + if self.clip_norm: + S_norm = np.clip( + S_norm, -self.max_norm, self.max_norm # pylint: disable=invalid-unary-operand-type + ) + return S_norm + else: + S_norm = self.max_norm * S_norm + if self.clip_norm: + S_norm = np.clip(S_norm, 0, self.max_norm) + return S_norm + else: + return S + + def denormalize(self, S: np.ndarray) -> np.ndarray: + """Denormalize spectrogram values. + + Args: + S (np.ndarray): Spectrogram to denormalize. + + Raises: + RuntimeError: Mean and variance are incompatible. + + Returns: + np.ndarray: Denormalized spectrogram. + """ + # pylint: disable=no-else-return + S_denorm = S.copy() + if self.signal_norm: + # mean-var scaling + if hasattr(self, "mel_scaler"): + if S_denorm.shape[0] == self.num_mels: + return self.mel_scaler.inverse_transform(S_denorm.T).T + elif S_denorm.shape[0] == self.fft_size / 2: + return self.linear_scaler.inverse_transform(S_denorm.T).T + else: + raise RuntimeError(" [!] Mean-Var stats does not match the given feature dimensions.") + if self.symmetric_norm: + if self.clip_norm: + S_denorm = np.clip( + S_denorm, -self.max_norm, self.max_norm # pylint: disable=invalid-unary-operand-type + ) + S_denorm = ((S_denorm + self.max_norm) * -self.min_level_db / (2 * self.max_norm)) + self.min_level_db + return S_denorm + self.ref_level_db + else: + if self.clip_norm: + S_denorm = np.clip(S_denorm, 0, self.max_norm) + S_denorm = (S_denorm * -self.min_level_db / self.max_norm) + self.min_level_db + return S_denorm + self.ref_level_db + else: + return S_denorm + + ### Mean-STD scaling ### + def load_stats(self, stats_path: str) -> Tuple[np.array, np.array, np.array, np.array, Dict]: + """Loading mean and variance statistics from a `npy` file. + + Args: + stats_path (str): Path to the `npy` file containing + + Returns: + Tuple[np.array, np.array, np.array, np.array, Dict]: loaded statistics and the config used to + compute them. + """ + stats = np.load(stats_path, allow_pickle=True).item() # pylint: disable=unexpected-keyword-arg + mel_mean = stats["mel_mean"] + mel_std = stats["mel_std"] + linear_mean = stats["linear_mean"] + linear_std = stats["linear_std"] + stats_config = stats["audio_config"] + # check all audio parameters used for computing stats + skip_parameters = ["griffin_lim_iters", "stats_path", "do_trim_silence", "ref_level_db", "power"] + for key in stats_config.keys(): + if key in skip_parameters: + continue + if key not in ["sample_rate", "trim_db"]: + assert ( + stats_config[key] == self.__dict__[key] + ), f" [!] Audio param {key} does not match the value used for computing mean-var stats. {stats_config[key]} vs {self.__dict__[key]}" + return mel_mean, mel_std, linear_mean, linear_std, stats_config + + # pylint: disable=attribute-defined-outside-init + def setup_scaler( + self, mel_mean: np.ndarray, mel_std: np.ndarray, linear_mean: np.ndarray, linear_std: np.ndarray + ) -> None: + """Initialize scaler objects used in mean-std normalization. + + Args: + mel_mean (np.ndarray): Mean for melspectrograms. + mel_std (np.ndarray): STD for melspectrograms. + linear_mean (np.ndarray): Mean for full scale spectrograms. + linear_std (np.ndarray): STD for full scale spectrograms. + """ + self.mel_scaler = StandardScaler() + self.mel_scaler.set_stats(mel_mean, mel_std) + self.linear_scaler = StandardScaler() + self.linear_scaler.set_stats(linear_mean, linear_std) + + ### DB and AMP conversion ### + # pylint: disable=no-self-use + def _amp_to_db(self, x: np.ndarray) -> np.ndarray: + """Convert amplitude values to decibels. + + Args: + x (np.ndarray): Amplitude spectrogram. + + Returns: + np.ndarray: Decibels spectrogram. + """ + return self.spec_gain * _log(np.maximum(1e-5, x), self.base) + + # pylint: disable=no-self-use + def _db_to_amp(self, x: np.ndarray) -> np.ndarray: + """Convert decibels spectrogram to amplitude spectrogram. + + Args: + x (np.ndarray): Decibels spectrogram. + + Returns: + np.ndarray: Amplitude spectrogram. + """ + return _exp(x / self.spec_gain, self.base) + + ### Preemphasis ### + def apply_preemphasis(self, x: np.ndarray) -> np.ndarray: + """Apply pre-emphasis to the audio signal. Useful to reduce the correlation between neighbouring signal values. + + Args: + x (np.ndarray): Audio signal. + + Raises: + RuntimeError: Preemphasis coeff is set to 0. + + Returns: + np.ndarray: Decorrelated audio signal. + """ + if self.preemphasis == 0: + raise RuntimeError(" [!] Preemphasis is set 0.0.") + return scipy.signal.lfilter([1, -self.preemphasis], [1], x) + + def apply_inv_preemphasis(self, x: np.ndarray) -> np.ndarray: + """Reverse pre-emphasis.""" + if self.preemphasis == 0: + raise RuntimeError(" [!] Preemphasis is set 0.0.") + return scipy.signal.lfilter([1], [1, -self.preemphasis], x) + + ### SPECTROGRAMs ### + def _linear_to_mel(self, spectrogram: np.ndarray) -> np.ndarray: + """Project a full scale spectrogram to a melspectrogram. + + Args: + spectrogram (np.ndarray): Full scale spectrogram. + + Returns: + np.ndarray: Melspectrogram + """ + return np.dot(self.mel_basis, spectrogram) + + def _mel_to_linear(self, mel_spec: np.ndarray) -> np.ndarray: + """Convert a melspectrogram to full scale spectrogram.""" + return np.maximum(1e-10, np.dot(self.inv_mel_basis, mel_spec)) + + def spectrogram(self, y: np.ndarray) -> np.ndarray: + """Compute a spectrogram from a waveform. + + Args: + y (np.ndarray): Waveform. + + Returns: + np.ndarray: Spectrogram. + """ + if self.preemphasis != 0: + D = self._stft(self.apply_preemphasis(y)) + else: + D = self._stft(y) + if self.do_amp_to_db_linear: + S = self._amp_to_db(np.abs(D)) + else: + S = np.abs(D) + return self.normalize(S).astype(np.float32) + + def melspectrogram(self, y: np.ndarray) -> np.ndarray: + """Compute a melspectrogram from a waveform.""" + if self.preemphasis != 0: + D = self._stft(self.apply_preemphasis(y)) + else: + D = self._stft(y) + if self.do_amp_to_db_mel: + S = self._amp_to_db(self._linear_to_mel(np.abs(D))) + else: + S = self._linear_to_mel(np.abs(D)) + return self.normalize(S).astype(np.float32) + + def inv_spectrogram(self, spectrogram: np.ndarray) -> np.ndarray: + """Convert a spectrogram to a waveform using Griffi-Lim vocoder.""" + S = self.denormalize(spectrogram) + S = self._db_to_amp(S) + # Reconstruct phase + if self.preemphasis != 0: + return self.apply_inv_preemphasis(self._griffin_lim(S ** self.power)) + return self._griffin_lim(S ** self.power) + + def inv_melspectrogram(self, mel_spectrogram: np.ndarray) -> np.ndarray: + """Convert a melspectrogram to a waveform using Griffi-Lim vocoder.""" + D = self.denormalize(mel_spectrogram) + S = self._db_to_amp(D) + S = self._mel_to_linear(S) # Convert back to linear + if self.preemphasis != 0: + return self.apply_inv_preemphasis(self._griffin_lim(S ** self.power)) + return self._griffin_lim(S ** self.power) + + def out_linear_to_mel(self, linear_spec: np.ndarray) -> np.ndarray: + """Convert a full scale linear spectrogram output of a network to a melspectrogram. + + Args: + linear_spec (np.ndarray): Normalized full scale linear spectrogram. + + Returns: + np.ndarray: Normalized melspectrogram. + """ + S = self.denormalize(linear_spec) + S = self._db_to_amp(S) + S = self._linear_to_mel(np.abs(S)) + S = self._amp_to_db(S) + mel = self.normalize(S) + return mel + + ### STFT and ISTFT ### + def _stft(self, y: np.ndarray) -> np.ndarray: + """Librosa STFT wrapper. + + Args: + y (np.ndarray): Audio signal. + + Returns: + np.ndarray: Complex number array. + """ + return librosa.stft( + y=y, + n_fft=self.fft_size, + hop_length=self.hop_length, + win_length=self.win_length, + pad_mode=self.stft_pad_mode, + window="hann", + center=True, + ) + + def _istft(self, y: np.ndarray) -> np.ndarray: + """Librosa iSTFT wrapper.""" + return librosa.istft(y, hop_length=self.hop_length, win_length=self.win_length) + + def _griffin_lim(self, S): + angles = np.exp(2j * np.pi * np.random.rand(*S.shape)) + S_complex = np.abs(S).astype(np.complex) + y = self._istft(S_complex * angles) + if not np.isfinite(y).all(): + print(" [!] Waveform is not finite everywhere. Skipping the GL.") + return np.array([0.0]) + for _ in range(self.griffin_lim_iters): + angles = np.exp(1j * np.angle(self._stft(y))) + y = self._istft(S_complex * angles) + return y + + def compute_stft_paddings(self, x, pad_sides=1): + """Compute paddings used by Librosa's STFT. Compute right padding (final frame) or both sides padding + (first and final frames)""" + assert pad_sides in (1, 2) + pad = (x.shape[0] // self.hop_length + 1) * self.hop_length - x.shape[0] + if pad_sides == 1: + return 0, pad + return pad // 2, pad // 2 + pad % 2 + + def compute_f0(self, x: np.ndarray) -> np.ndarray: + """Compute pitch (f0) of a waveform using the same parameters used for computing melspectrogram. + + Args: + x (np.ndarray): Waveform. + + Returns: + np.ndarray: Pitch. + + Examples: + >>> WAV_FILE = filename = librosa.util.example_audio_file() + >>> from TTS.config import BaseAudioConfig + >>> from TTS.utils.audio import AudioProcessor + >>> conf = BaseAudioConfig(mel_fmax=8000) + >>> ap = AudioProcessor(**conf) + >>> wav = ap.load_wav(WAV_FILE, sr=22050)[:5 * 22050] + >>> pitch = ap.compute_f0(wav) + """ + f0, t = pw.dio( + x.astype(np.double), + fs=self.sample_rate, + f0_ceil=self.mel_fmax, + frame_period=1000 * self.hop_length / self.sample_rate, + ) + f0 = pw.stonemask(x.astype(np.double), f0, t, self.sample_rate) + # pad = int((self.win_length / self.hop_length) / 2) + # f0 = [0.0] * pad + f0 + [0.0] * pad + # f0 = np.pad(f0, (pad, pad), mode="constant", constant_values=0) + # f0 = np.array(f0, dtype=np.float32) + + # f01, _, _ = librosa.pyin( + # x, + # fmin=65 if self.mel_fmin == 0 else self.mel_fmin, + # fmax=self.mel_fmax, + # frame_length=self.win_length, + # sr=self.sample_rate, + # fill_na=0.0, + # ) + + # spec = self.melspectrogram(x) + return f0 + + ### Audio Processing ### + def find_endpoint(self, wav: np.ndarray, threshold_db=-40, min_silence_sec=0.8) -> int: + """Find the last point without silence at the end of a audio signal. + + Args: + wav (np.ndarray): Audio signal. + threshold_db (int, optional): Silence threshold in decibels. Defaults to -40. + min_silence_sec (float, optional): Ignore silences that are shorter then this in secs. Defaults to 0.8. + + Returns: + int: Last point without silence. + """ + window_length = int(self.sample_rate * min_silence_sec) + hop_length = int(window_length / 4) + threshold = self._db_to_amp(threshold_db) + for x in range(hop_length, len(wav) - window_length, hop_length): + if np.max(wav[x : x + window_length]) < threshold: + return x + hop_length + return len(wav) + + def trim_silence(self, wav): + """Trim silent parts with a threshold and 0.01 sec margin""" + margin = int(self.sample_rate * 0.01) + wav = wav[margin:-margin] + return librosa.effects.trim(wav, top_db=self.trim_db, frame_length=self.win_length, hop_length=self.hop_length)[ + 0 + ] + + @staticmethod + def sound_norm(x: np.ndarray) -> np.ndarray: + """Normalize the volume of an audio signal. + + Args: + x (np.ndarray): Raw waveform. + + Returns: + np.ndarray: Volume normalized waveform. + """ + return x / abs(x).max() * 0.95 + + ### save and load ### + def load_wav(self, filename: str, sr: int = None) -> np.ndarray: + """Read a wav file using Librosa and optionally resample, silence trim, volume normalize. + + Args: + filename (str): Path to the wav file. + sr (int, optional): Sampling rate for resampling. Defaults to None. + + Returns: + np.ndarray: Loaded waveform. + """ + if self.resample: + x, sr = librosa.load(filename, sr=self.sample_rate) + elif sr is None: + x, sr = sf.read(filename) + assert self.sample_rate == sr, "%s vs %s" % (self.sample_rate, sr) + else: + x, sr = librosa.load(filename, sr=sr) + if self.do_trim_silence: + try: + x = self.trim_silence(x) + except ValueError: + print(f" [!] File cannot be trimmed for silence - {filename}") + if self.do_sound_norm: + x = self.sound_norm(x) + return x + + def save_wav(self, wav: np.ndarray, path: str, sr: int = None) -> None: + """Save a waveform to a file using Scipy. + + Args: + wav (np.ndarray): Waveform to save. + path (str): Path to a output file. + sr (int, optional): Sampling rate used for saving to the file. Defaults to None. + """ + wav_norm = wav * (32767 / max(0.01, np.max(np.abs(wav)))) + scipy.io.wavfile.write(path, sr if sr else self.sample_rate, wav_norm.astype(np.int16)) + + @staticmethod + def mulaw_encode(wav: np.ndarray, qc: int) -> np.ndarray: + mu = 2 ** qc - 1 + # wav_abs = np.minimum(np.abs(wav), 1.0) + signal = np.sign(wav) * np.log(1 + mu * np.abs(wav)) / np.log(1.0 + mu) + # Quantize signal to the specified number of levels. + signal = (signal + 1) / 2 * mu + 0.5 + return np.floor( + signal, + ) + + @staticmethod + def mulaw_decode(wav, qc): + """Recovers waveform from quantized values.""" + mu = 2 ** qc - 1 + x = np.sign(wav) / mu * ((1 + mu) ** np.abs(wav) - 1) + return x + + @staticmethod + def encode_16bits(x): + return np.clip(x * 2 ** 15, -(2 ** 15), 2 ** 15 - 1).astype(np.int16) + + @staticmethod + def quantize(x: np.ndarray, bits: int) -> np.ndarray: + """Quantize a waveform to a given number of bits. + + Args: + x (np.ndarray): Waveform to quantize. Must be normalized into the range `[-1, 1]`. + bits (int): Number of quantization bits. + + Returns: + np.ndarray: Quantized waveform. + """ + return (x + 1.0) * (2 ** bits - 1) / 2 + + @staticmethod + def dequantize(x, bits): + """Dequantize a waveform from the given number of bits.""" + return 2 * x / (2 ** bits - 1) - 1 + + +def _log(x, base): + if base == 10: + return np.log10(x) + return np.log(x) + + +def _exp(x, base): + if base == 10: + return np.power(10, x) + return np.exp(x) diff --git a/speaker/utils/coqpit.py b/speaker/utils/coqpit.py new file mode 100644 index 0000000..e214c8b --- /dev/null +++ b/speaker/utils/coqpit.py @@ -0,0 +1,954 @@ +import argparse +import functools +import json +import operator +import os +from collections.abc import MutableMapping +from dataclasses import MISSING as _MISSING +from dataclasses import Field, asdict, dataclass, fields, is_dataclass, replace +from pathlib import Path +from pprint import pprint +from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union, get_type_hints + +T = TypeVar("T") +MISSING: Any = "???" + + +class _NoDefault(Generic[T]): + pass + + +NoDefaultVar = Union[_NoDefault[T], T] +no_default: NoDefaultVar = _NoDefault() + + +def is_primitive_type(arg_type: Any) -> bool: + """Check if the input type is one of `int, float, str, bool`. + + Args: + arg_type (typing.Any): input type to check. + + Returns: + bool: True if input type is one of `int, float, str, bool`. + """ + try: + return isinstance(arg_type(), (int, float, str, bool)) + except (AttributeError, TypeError): + return False + + +def is_list(arg_type: Any) -> bool: + """Check if the input type is `list` + + Args: + arg_type (typing.Any): input type. + + Returns: + bool: True if input type is `list` + """ + try: + return arg_type is list or arg_type is List or arg_type.__origin__ is list or arg_type.__origin__ is List + except AttributeError: + return False + + +def is_dict(arg_type: Any) -> bool: + """Check if the input type is `dict` + + Args: + arg_type (typing.Any): input type. + + Returns: + bool: True if input type is `dict` + """ + try: + return arg_type is dict or arg_type is Dict or arg_type.__origin__ is dict + except AttributeError: + return False + + +def is_union(arg_type: Any) -> bool: + """Check if the input type is `Union`. + + Args: + arg_type (typing.Any): input type. + + Returns: + bool: True if input type is `Union` + """ + try: + return safe_issubclass(arg_type.__origin__, Union) + except AttributeError: + return False + + +def safe_issubclass(cls, classinfo) -> bool: + """Check if the input type is a subclass of the given class. + + Args: + cls (type): input type. + classinfo (type): parent class. + + Returns: + bool: True if the input type is a subclass of the given class + """ + try: + r = issubclass(cls, classinfo) + except Exception: # pylint: disable=broad-except + return cls is classinfo + else: + return r + + +def _coqpit_json_default(obj: Any) -> Any: + if isinstance(obj, Path): + return str(obj) + raise TypeError(f"Can't encode object of type {type(obj).__name__}") + + +def _default_value(x: Field): + """Return the default value of the input Field. + + Args: + x (Field): input Field. + + Returns: + object: default value of the input Field. + """ + if x.default not in (MISSING, _MISSING): + return x.default + if x.default_factory not in (MISSING, _MISSING): + return x.default_factory() + return x.default + + +def _is_optional_field(field) -> bool: + """Check if the input field is optional. + + Args: + field (Field): input Field to check. + + Returns: + bool: True if the input field is optional. + """ + # return isinstance(field.type, _GenericAlias) and type(None) in getattr(field.type, "__args__") + return type(None) in getattr(field.type, "__args__") + + +def my_get_type_hints( + cls, +): + """Custom `get_type_hints` dealing with https://github.com/python/typing/issues/737 + + Returns: + [dataclass]: dataclass to get the type hints of its fields. + """ + r_dict = {} + for base in cls.__class__.__bases__: + if base == object: + break + r_dict.update(my_get_type_hints(base)) + r_dict.update(get_type_hints(cls)) + return r_dict + + +def _serialize(x): + """Pick the right serialization for the datatype of the given input. + + Args: + x (object): input object. + + Returns: + object: serialized object. + """ + if isinstance(x, Path): + return str(x) + if isinstance(x, dict): + return {k: _serialize(v) for k, v in x.items()} + if isinstance(x, list): + return [_serialize(xi) for xi in x] + if isinstance(x, Serializable) or issubclass(type(x), Serializable): + return x.serialize() + if isinstance(x, type) and issubclass(x, Serializable): + return x.serialize(x) + return x + + +def _deserialize_dict(x: Dict) -> Dict: + """Deserialize dict. + + Args: + x (Dict): value to deserialized. + + Returns: + Dict: deserialized dictionary. + """ + out_dict = {} + for k, v in x.items(): + if v is None: # if {'key':None} + out_dict[k] = None + else: + out_dict[k] = _deserialize(v, type(v)) + return out_dict + + +def _deserialize_list(x: List, field_type: Type) -> List: + """Deserialize values for List typed fields. + + Args: + x (List): value to be deserialized + field_type (Type): field type. + + Raises: + ValueError: Coqpit does not support multi type-hinted lists. + + Returns: + [List]: deserialized list. + """ + field_args = None + if hasattr(field_type, "__args__") and field_type.__args__: + field_args = field_type.__args__ + elif hasattr(field_type, "__parameters__") and field_type.__parameters__: + # bandaid for python 3.6 + field_args = field_type.__parameters__ + if field_args: + if len(field_args) > 1: + raise ValueError(" [!] Coqpit does not support multi-type hinted 'List'") + field_arg = field_args[0] + # if field type is TypeVar set the current type by the value's type. + if isinstance(field_arg, TypeVar): + field_arg = type(x) + return [_deserialize(xi, field_arg) for xi in x] + return x + + +def _deserialize_union(x: Any, field_type: Type) -> Any: + """Deserialize values for Union typed fields + + Args: + x (Any): value to be deserialized. + field_type (Type): field type. + + Returns: + [Any]: desrialized value. + """ + for arg in field_type.__args__: + # stop after first matching type in Union + try: + x = _deserialize(x, arg) + break + except ValueError: + pass + return x + + +def _deserialize_primitive_types(x: Union[int, float, str, bool], field_type: Type) -> Union[int, float, str, bool]: + """Deserialize python primitive types (float, int, str, bool). + It handles `inf` values exclusively and keeps them float against int fields since int does not support inf values. + + Args: + x (Union[int, float, str, bool]): value to be deserialized. + field_type (Type): field type. + + Returns: + Union[int, float, str, bool]: deserialized value. + """ + + if isinstance(x, (str, bool)): + return x + if isinstance(x, (int, float)): + if x == float("inf") or x == float("-inf"): + # if value type is inf return regardless. + return x + x = field_type(x) + return x + # TODO: Raise an error when x does not match the types. + return None + + +def _deserialize(x: Any, field_type: Any) -> Any: + """Pick the right desrialization for the given object and the corresponding field type. + + Args: + x (object): object to be deserialized. + field_type (type): expected type after deserialization. + + Returns: + object: deserialized object + + """ + # pylint: disable=too-many-return-statements + if is_dict(field_type): + return _deserialize_dict(x) + if is_list(field_type): + return _deserialize_list(x, field_type) + if is_union(field_type): + return _deserialize_union(x, field_type) + if issubclass(field_type, Serializable): + return field_type.deserialize_immutable(x) + if is_primitive_type(field_type): + return _deserialize_primitive_types(x, field_type) + raise ValueError(f" [!] '{type(x)}' value type of '{x}' does not match '{field_type}' field type.") + + +# Recursive setattr (supports dotted attr names) +def rsetattr(obj, attr, val): + def _setitem(obj, attr, val): + return operator.setitem(obj, int(attr), val) + + pre, _, post = attr.rpartition(".") + setfunc = _setitem if post.isnumeric() else setattr + + return setfunc(rgetattr(obj, pre) if pre else obj, post, val) + + +# Recursive getattr (supports dotted attr names) +def rgetattr(obj, attr, *args): + def _getitem(obj, attr): + return operator.getitem(obj, int(attr), *args) + + def _getattr(obj, attr): + getfunc = _getitem if attr.isnumeric() else getattr + return getfunc(obj, attr, *args) + + return functools.reduce(_getattr, [obj] + attr.split(".")) + + +# Recursive setitem (supports dotted attr names) +def rsetitem(obj, attr, val): + pre, _, post = attr.rpartition(".") + return operator.setitem(rgetitem(obj, pre) if pre else obj, post, val) + + +# Recursive getitem (supports dotted attr names) +def rgetitem(obj, attr, *args): + def _getitem(obj, attr): + return operator.getitem(obj, int(attr) if attr.isnumeric() else attr, *args) + + return functools.reduce(_getitem, [obj] + attr.split(".")) + + +@dataclass +class Serializable: + """Gives serialization ability to any inheriting dataclass.""" + + def __post_init__(self): + self._validate_contracts() + for key, value in self.__dict__.items(): + if value is no_default: + raise TypeError(f"__init__ missing 1 required argument: '{key}'") + + def _validate_contracts(self): + dataclass_fields = fields(self) + + for field in dataclass_fields: + + value = getattr(self, field.name) + + if value is None: + if not _is_optional_field(field): + raise TypeError(f"{field.name} is not optional") + + contract = field.metadata.get("contract", None) + + if contract is not None: + if value is not None and not contract(value): + raise ValueError(f"break the contract for {field.name}, {self.__class__.__name__}") + + def validate(self): + """validate if object can serialize / deserialize correctly.""" + self._validate_contracts() + if self != self.__class__.deserialize( # pylint: disable=no-value-for-parameter + json.loads(json.dumps(self.serialize())) + ): + raise ValueError("could not be deserialized with same value") + + def to_dict(self) -> dict: + """Transform serializable object to dict.""" + cls_fields = fields(self) + o = {} + for cls_field in cls_fields: + o[cls_field.name] = getattr(self, cls_field.name) + return o + + def serialize(self) -> dict: + """Serialize object to be json serializable representation.""" + if not is_dataclass(self): + raise TypeError("need to be decorated as dataclass") + + dataclass_fields = fields(self) + + o = {} + + for field in dataclass_fields: + value = getattr(self, field.name) + value = _serialize(value) + o[field.name] = value + return o + + def deserialize(self, data: dict) -> "Serializable": + """Parse input dictionary and desrialize its fields to a dataclass. + + Returns: + self: deserialized `self`. + """ + if not isinstance(data, dict): + raise ValueError() + data = data.copy() + init_kwargs = {} + for field in fields(self): + # if field.name == 'dataset_config': + if field.name not in data: + if field.name in vars(self): + init_kwargs[field.name] = vars(self)[field.name] + continue + raise ValueError(f' [!] Missing required field "{field.name}"') + value = data.get(field.name, _default_value(field)) + if value is None: + init_kwargs[field.name] = value + continue + if value == MISSING: + raise ValueError(f"deserialized with unknown value for {field.name} in {self.__name__}") + value = _deserialize(value, field.type) + init_kwargs[field.name] = value + for k, v in init_kwargs.items(): + setattr(self, k, v) + return self + + @classmethod + def deserialize_immutable(cls, data: dict) -> "Serializable": + """Parse input dictionary and desrialize its fields to a dataclass. + + Returns: + Newly created deserialized object. + """ + if not isinstance(data, dict): + raise ValueError() + data = data.copy() + init_kwargs = {} + for field in fields(cls): + # if field.name == 'dataset_config': + if field.name not in data: + if field.name in vars(cls): + init_kwargs[field.name] = vars(cls)[field.name] + continue + # if not in cls and the default value is not Missing use it + default_value = _default_value(field) + if default_value not in (MISSING, _MISSING): + init_kwargs[field.name] = default_value + continue + raise ValueError(f' [!] Missing required field "{field.name}"') + value = data.get(field.name, _default_value(field)) + if value is None: + init_kwargs[field.name] = value + continue + if value == MISSING: + raise ValueError(f"Deserialized with unknown value for {field.name} in {cls.__name__}") + value = _deserialize(value, field.type) + init_kwargs[field.name] = value + return cls(**init_kwargs) + + +# ---------------------------------------------------------------------------- # +# Argument Parsing from `argparse` # +# ---------------------------------------------------------------------------- # + + +def _get_help(field): + try: + field_help = field.metadata["help"] + except KeyError: + field_help = "" + return field_help + + +def _init_argparse( + parser, + field_name, + field_type, + field_default, + field_default_factory, + field_help, + arg_prefix="", + help_prefix="", + relaxed_parser=False, +): + has_default = False + default = None + if field_default: + has_default = True + default = field_default + elif field_default_factory not in (None, _MISSING): + has_default = True + default = field_default_factory() + + if not has_default and not is_primitive_type(field_type) and not is_list(field_type): + # aggregate types (fields with a Coqpit subclass as type) are not supported without None + return parser + arg_prefix = field_name if arg_prefix == "" else f"{arg_prefix}.{field_name}" + help_prefix = field_help if help_prefix == "" else f"{help_prefix} - {field_help}" + if is_dict(field_type): # pylint: disable=no-else-raise + # NOTE: accept any string in json format as input to dict field. + parser.add_argument( + f"--{arg_prefix}", + dest=arg_prefix, + default=json.dumps(field_default) if field_default else None, + type=json.loads, + ) + elif is_list(field_type): + # TODO: We need a more clear help msg for lists. + if hasattr(field_type, "__args__"): # if the list is hinted + if len(field_type.__args__) > 1 and not relaxed_parser: + raise ValueError(" [!] Coqpit does not support multi-type hinted 'List'") + list_field_type = field_type.__args__[0] + else: + raise ValueError(" [!] Coqpit does not support un-hinted 'List'") + + # TODO: handle list of lists + if is_list(list_field_type) and relaxed_parser: + return parser + + if not has_default or field_default_factory is list: + if not is_primitive_type(list_field_type) and not relaxed_parser: + raise NotImplementedError(" [!] Empty list with non primitive inner type is currently not supported.") + + # If the list's default value is None, the user can specify the entire list by passing multiple parameters + parser.add_argument( + f"--{arg_prefix}", + nargs="*", + type=list_field_type, + help=f"Coqpit Field: {help_prefix}", + ) + else: + # If a default value is defined, just enable editing the values from argparse + # TODO: allow inserting a new value/obj to the end of the list. + for idx, fv in enumerate(default): + parser = _init_argparse( + parser, + str(idx), + list_field_type, + fv, + field_default_factory, + field_help="", + help_prefix=f"{help_prefix} - ", + arg_prefix=f"{arg_prefix}", + relaxed_parser=relaxed_parser, + ) + elif is_union(field_type): + # TODO: currently I don't know how to handle Union type on argparse + if not relaxed_parser: + raise NotImplementedError( + " [!] Parsing `Union` field from argparse is not yet implemented. Please create an issue." + ) + elif issubclass(field_type, Serializable): + return default.init_argparse( + parser, arg_prefix=arg_prefix, help_prefix=help_prefix, relaxed_parser=relaxed_parser + ) + elif isinstance(field_type(), bool): + + def parse_bool(x): + if x not in ("true", "false"): + raise ValueError(f' [!] Value for boolean field must be either "true" or "false". Got "{x}".') + return x == "true" + + parser.add_argument( + f"--{arg_prefix}", + type=parse_bool, + default=field_default, + help=f"Coqpit Field: {help_prefix}", + metavar="true/false", + ) + elif is_primitive_type(field_type): + parser.add_argument( + f"--{arg_prefix}", + default=field_default, + type=field_type, + help=f"Coqpit Field: {help_prefix}", + ) + else: + if not relaxed_parser: + raise NotImplementedError(f" [!] '{field_type}' is not supported by arg_parser. Please file a bug report.") + return parser + + +# ---------------------------------------------------------------------------- # +# Main Coqpit Class # +# ---------------------------------------------------------------------------- # + + +@dataclass +class Coqpit(Serializable, MutableMapping): + """Coqpit base class to be inherited by any Coqpit dataclasses. + It overrides Python `dict` interface and provides `dict` compatible API. + It also enables serializing/deserializing a dataclass to/from a json file, plus some semi-dynamic type and value check. + Note that it does not support all datatypes and likely to fail in some cases. + """ + + _initialized = False + + def _is_initialized(self): + """Check if Coqpit is initialized. Useful to prevent running some aux functions + at the initialization when no attribute has been defined.""" + return "_initialized" in vars(self) and self._initialized + + def __post_init__(self): + self._initialized = True + try: + self.check_values() + except AttributeError: + pass + + ## `dict` API functions + + def __iter__(self): + return iter(asdict(self)) + + def __len__(self): + return len(fields(self)) + + def __setitem__(self, arg: str, value: Any): + setattr(self, arg, value) + + def __getitem__(self, arg: str): + """Access class attributes with ``[arg]``.""" + return self.__dict__[arg] + + def __delitem__(self, arg: str): + delattr(self, arg) + + def _keytransform(self, key): # pylint: disable=no-self-use + return key + + ## end `dict` API functions + + def __getattribute__(self, arg: str): # pylint: disable=no-self-use + """Check if the mandatory field is defined when accessing it.""" + value = super().__getattribute__(arg) + if isinstance(value, str) and value == "???": + raise AttributeError(f" [!] MISSING field {arg} must be defined.") + return value + + def __contains__(self, arg: str): + return arg in self.to_dict() + + def get(self, key: str, default: Any = None): + if self.has(key): + return asdict(self)[key] + return default + + def items(self): + return asdict(self).items() + + def merge(self, coqpits: Union["Coqpit", List["Coqpit"]]): + """Merge a coqpit instance or a list of coqpit instances to self. + Note that it does not pass the fields and overrides attributes with + the last Coqpit instance in the given List. + TODO: find a way to merge instances with all the class internals. + + Args: + coqpits (Union[Coqpit, List[Coqpit]]): coqpit instance or list of instances to be merged. + """ + + def _merge(coqpit): + self.__dict__.update(coqpit.__dict__) + self.__annotations__.update(coqpit.__annotations__) + self.__dataclass_fields__.update(coqpit.__dataclass_fields__) + + if isinstance(coqpits, list): + for coqpit in coqpits: + _merge(coqpit) + else: + _merge(coqpits) + + def check_values(self): + pass + + def has(self, arg: str) -> bool: + return arg in vars(self) + + def copy(self): + return replace(self) + + def update(self, new: dict, allow_new=False) -> None: + """Update Coqpit fields by the input ```dict```. + + Args: + new (dict): dictionary with new values. + allow_new (bool, optional): allow new fields to add. Defaults to False. + """ + for key, value in new.items(): + if allow_new: + setattr(self, key, value) + else: + if hasattr(self, key): + setattr(self, key, value) + else: + raise KeyError(f" [!] No key - {key}") + + def pprint(self) -> None: + """Print Coqpit fields in a format.""" + pprint(asdict(self)) + + def to_dict(self) -> dict: + # return asdict(self) + return self.serialize() + + def from_dict(self, data: dict) -> None: + self = self.deserialize(data) # pylint: disable=self-cls-assignment + + @classmethod + def new_from_dict(cls: Serializable, data: dict) -> "Coqpit": + return cls.deserialize_immutable(data) + + def to_json(self) -> str: + """Returns a JSON string representation.""" + return json.dumps(asdict(self), indent=4, default=_coqpit_json_default) + + def save_json(self, file_name: str) -> None: + """Save Coqpit to a json file. + + Args: + file_name (str): path to the output json file. + """ + with open(file_name, "w", encoding="utf8") as f: + json.dump(asdict(self), f, indent=4) + + def load_json(self, file_name: str) -> None: + """Load a json file and update matching config fields with type checking. + Non-matching parameters in the json file are ignored. + + Args: + file_name (str): path to the json file. + + Returns: + Coqpit: new Coqpit with updated config fields. + """ + with open(file_name, "r", encoding="utf8") as f: + input_str = f.read() + dump_dict = json.loads(input_str) + # TODO: this looks stupid 💆 + self = self.deserialize(dump_dict) # pylint: disable=self-cls-assignment + self.check_values() + + @classmethod + def init_from_argparse( + cls, args: Optional[Union[argparse.Namespace, List[str]]] = None, arg_prefix: str = "coqpit" + ) -> "Coqpit": + """Create a new Coqpit instance from argparse input. + + Args: + args (namespace or list of str, optional): parsed argparse.Namespace or list of command line parameters. If unspecified will use a newly created parser with ```init_argparse()```. + arg_prefix: prefix to add to CLI parameters. Gets forwarded to ```init_argparse``` when ```args``` is not passed. + """ + if not args: + # If args was not specified, parse from sys.argv + parser = cls.init_argparse(cls, arg_prefix=arg_prefix) + args = parser.parse_args() # pylint: disable=E1120, E1111 + if isinstance(args, list): + # If a list was passed in (eg. the second result of `parse_known_args`, run that through argparse first to get a parsed Namespace + parser = cls.init_argparse(cls, arg_prefix=arg_prefix) + args = parser.parse_args(args) # pylint: disable=E1120, E1111 + + # Handle list and object attributes with defaults, which can be modified + # directly (eg. --coqpit.list.0.val_a 1), by constructing real objects + # from defaults and passing those to `cls.__init__` + args_with_lists_processed = {} + class_fields = fields(cls) + for field in class_fields: + has_default = False + default = None + field_default = field.default if field.default is not _MISSING else None + field_default_factory = field.default_factory if field.default_factory is not _MISSING else None + if field_default: + has_default = True + default = field_default + elif field_default_factory: + has_default = True + default = field_default_factory() + + if has_default and (not is_primitive_type(field.type) or is_list(field.type)): + args_with_lists_processed[field.name] = default + + args_dict = vars(args) + for k, v in args_dict.items(): + # Remove argparse prefix (eg. "--coqpit." if present) + if k.startswith(f"{arg_prefix}."): + k = k[len(f"{arg_prefix}.") :] + + rsetitem(args_with_lists_processed, k, v) + + return cls(**args_with_lists_processed) + + def parse_args( + self, args: Optional[Union[argparse.Namespace, List[str]]] = None, arg_prefix: str = "coqpit" + ) -> None: + """Update config values from argparse arguments with some meta-programming ✨. + + Args: + args (namespace or list of str, optional): parsed argparse.Namespace or list of command line parameters. If unspecified will use a newly created parser with ```init_argparse()```. + arg_prefix: prefix to add to CLI parameters. Gets forwarded to ```init_argparse``` when ```args``` is not passed. + """ + if not args: + # If args was not specified, parse from sys.argv + parser = self.init_argparse(arg_prefix=arg_prefix) + args = parser.parse_args() + if isinstance(args, list): + # If a list was passed in (eg. the second result of `parse_known_args`, run that through argparse first to get a parsed Namespace + parser = self.init_argparse(arg_prefix=arg_prefix) + args = parser.parse_args(args) + + args_dict = vars(args) + + for k, v in args_dict.items(): + if k.startswith(f"{arg_prefix}."): + k = k[len(f"{arg_prefix}.") :] + try: + rgetattr(self, k) + except (TypeError, AttributeError) as e: + raise Exception(f" [!] '{k}' not exist to override from argparse.") from e + + rsetattr(self, k, v) + + self.check_values() + + def parse_known_args( + self, + args: Optional[Union[argparse.Namespace, List[str]]] = None, + arg_prefix: str = "coqpit", + relaxed_parser=False, + ) -> List[str]: + """Update config values from argparse arguments. Ignore unknown arguments. + This is analog to argparse.ArgumentParser.parse_known_args (vs parse_args). + + Args: + args (namespace or list of str, optional): parsed argparse.Namespace or list of command line parameters. If unspecified will use a newly created parser with ```init_argparse()```. + arg_prefix: prefix to add to CLI parameters. Gets forwarded to ```init_argparse``` when ```args``` is not passed. + relaxed_parser (bool, optional): If True, do not force all the fields to have compatible types with the argparser. Defaults to False. + + Returns: + List of unknown parameters. + """ + if not args: + # If args was not specified, parse from sys.argv + parser = self.init_argparse(arg_prefix=arg_prefix, relaxed_parser=relaxed_parser) + args, unknown = parser.parse_known_args() + if isinstance(args, list): + # If a list was passed in (eg. the second result of `parse_known_args`, run that through argparse first to get a parsed Namespace + parser = self.init_argparse(arg_prefix=arg_prefix, relaxed_parser=relaxed_parser) + args, unknown = parser.parse_known_args(args) + + self.parse_args(args) + return unknown + + def init_argparse( + self, + parser: Optional[argparse.ArgumentParser] = None, + arg_prefix="coqpit", + help_prefix="", + relaxed_parser=False, + ) -> argparse.ArgumentParser: + """Pass Coqpit fields as argparse arguments. This allows to edit values through command-line. + + Args: + parser (argparse.ArgumentParser, optional): argparse.ArgumentParser instance. If unspecified a new one will be created. + arg_prefix (str, optional): Prefix to be used for the argument name. Defaults to 'coqpit'. + help_prefix (str, optional): Prefix to be used for the argument description. Defaults to ''. + relaxed_parser (bool, optional): If True, do not force all the fields to have compatible types with the argparser. Defaults to False. + + Returns: + argparse.ArgumentParser: parser instance with the new arguments. + """ + if not parser: + parser = argparse.ArgumentParser() + class_fields = fields(self) + for field in class_fields: + if field.name in vars(self): + # use the current value of the field + # prevent dropping the current value + field_default = vars(self)[field.name] + else: + # use the default value of the field + field_default = field.default if field.default is not _MISSING else None + field_type = field.type + field_default_factory = field.default_factory + field_help = _get_help(field) + _init_argparse( + parser, + field.name, + field_type, + field_default, + field_default_factory, + field_help, + arg_prefix, + help_prefix, + relaxed_parser, + ) + return parser + + +def check_argument( + name, + c, + is_path: bool = False, + prerequest: str = None, + enum_list: list = None, + max_val: float = None, + min_val: float = None, + restricted: bool = False, + alternative: str = None, + allow_none: bool = True, +) -> None: + """Simple type and value checking for Coqpit. + It is intended to be used under ```__post_init__()``` of config dataclasses. + + Args: + name (str): name of the field to be checked. + c (dict): config dictionary. + is_path (bool, optional): if ```True``` check if the path is exist. Defaults to False. + prerequest (list or str, optional): a list of field name that are prerequestedby the target field name. + Defaults to ```[]```. + enum_list (list, optional): list of possible values for the target field. Defaults to None. + max_val (float, optional): maximum possible value for the target field. Defaults to None. + min_val (float, optional): minimum possible value for the target field. Defaults to None. + restricted (bool, optional): if ```True``` the target field has to be defined. Defaults to False. + alternative (str, optional): a field name superceding the target field. Defaults to None. + allow_none (bool, optional): if ```True``` allow the target field to be ```None```. Defaults to False. + + + Example: + >>> num_mels = 5 + >>> check_argument('num_mels', c, restricted=True, min_val=10, max_val=2056) + >>> fft_size = 128 + >>> check_argument('fft_size', c, restricted=True, min_val=128, max_val=4058) + """ + # check if None allowed + if allow_none and c[name] is None: + return + if not allow_none: + assert c[name] is not None, f" [!] None value is not allowed for {name}." + # check if restricted and it it is check if it exists + if isinstance(restricted, bool) and restricted: + assert name in c.keys(), f" [!] {name} not defined in config.json" + # check prerequest fields are defined + if isinstance(prerequest, list): + assert any( + f not in c.keys() for f in prerequest + ), f" [!] prequested fields {prerequest} for {name} are not defined." + else: + assert ( + prerequest is None or prerequest in c.keys() + ), f" [!] prequested fields {prerequest} for {name} are not defined." + # check if the path exists + if is_path: + assert os.path.exists(c[name]), f' [!] path for {name} ("{c[name]}") does not exist.' + # skip the rest if the alternative field is defined. + if alternative in c.keys() and c[alternative] is not None: + return + # check value constraints + if name in c.keys(): + if max_val is not None: + assert c[name] <= max_val, f" [!] {name} is larger than max value {max_val}" + if min_val is not None: + assert c[name] >= min_val, f" [!] {name} is smaller than min value {min_val}" + if enum_list is not None: + assert c[name].lower() in enum_list, f" [!] {name} is not a valid value" diff --git a/speaker/utils/io.py b/speaker/utils/io.py new file mode 100644 index 0000000..1d4c079 --- /dev/null +++ b/speaker/utils/io.py @@ -0,0 +1,198 @@ +import datetime +import json +import os +import pickle as pickle_tts +import shutil +from typing import Any, Callable, Dict, Union + +import fsspec +import torch +from .coqpit import Coqpit + + +class RenamingUnpickler(pickle_tts.Unpickler): + """Overload default pickler to solve module renaming problem""" + + def find_class(self, module, name): + return super().find_class(module.replace("mozilla_voice_tts", "TTS"), name) + + +class AttrDict(dict): + """A custom dict which converts dict keys + to class attributes""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__dict__ = self + + +def copy_model_files(config: Coqpit, out_path, new_fields): + """Copy config.json and other model files to training folder and add + new fields. + + Args: + config (Coqpit): Coqpit config defining the training run. + out_path (str): output path to copy the file. + new_fields (dict): new fileds to be added or edited + in the config file. + """ + copy_config_path = os.path.join(out_path, "config.json") + # add extra information fields + config.update(new_fields, allow_new=True) + # TODO: Revert to config.save_json() once Coqpit supports arbitrary paths. + with fsspec.open(copy_config_path, "w", encoding="utf8") as f: + json.dump(config.to_dict(), f, indent=4) + + # copy model stats file if available + if config.audio.stats_path is not None: + copy_stats_path = os.path.join(out_path, "scale_stats.npy") + filesystem = fsspec.get_mapper(copy_stats_path).fs + if not filesystem.exists(copy_stats_path): + with fsspec.open(config.audio.stats_path, "rb") as source_file: + with fsspec.open(copy_stats_path, "wb") as target_file: + shutil.copyfileobj(source_file, target_file) + + +def load_fsspec( + path: str, + map_location: Union[str, Callable, torch.device, Dict[Union[str, torch.device], Union[str, torch.device]]] = None, + **kwargs, +) -> Any: + """Like torch.load but can load from other locations (e.g. s3:// , gs://). + + Args: + path: Any path or url supported by fsspec. + map_location: torch.device or str. + **kwargs: Keyword arguments forwarded to torch.load. + + Returns: + Object stored in path. + """ + with fsspec.open(path, "rb") as f: + return torch.load(f, map_location=map_location, **kwargs) + + +def load_checkpoint(model, checkpoint_path, use_cuda=False, eval=False): # pylint: disable=redefined-builtin + try: + state = load_fsspec(checkpoint_path, map_location=torch.device("cpu")) + except ModuleNotFoundError: + pickle_tts.Unpickler = RenamingUnpickler + state = load_fsspec(checkpoint_path, map_location=torch.device("cpu"), pickle_module=pickle_tts) + model.load_state_dict(state["model"]) + if use_cuda: + model.cuda() + if eval: + model.eval() + return model, state + + +def save_fsspec(state: Any, path: str, **kwargs): + """Like torch.save but can save to other locations (e.g. s3:// , gs://). + + Args: + state: State object to save + path: Any path or url supported by fsspec. + **kwargs: Keyword arguments forwarded to torch.save. + """ + with fsspec.open(path, "wb") as f: + torch.save(state, f, **kwargs) + + +def save_model(config, model, optimizer, scaler, current_step, epoch, output_path, **kwargs): + if hasattr(model, "module"): + model_state = model.module.state_dict() + else: + model_state = model.state_dict() + if isinstance(optimizer, list): + optimizer_state = [optim.state_dict() for optim in optimizer] + else: + optimizer_state = optimizer.state_dict() if optimizer is not None else None + + if isinstance(scaler, list): + scaler_state = [s.state_dict() for s in scaler] + else: + scaler_state = scaler.state_dict() if scaler is not None else None + + if isinstance(config, Coqpit): + config = config.to_dict() + + state = { + "config": config, + "model": model_state, + "optimizer": optimizer_state, + "scaler": scaler_state, + "step": current_step, + "epoch": epoch, + "date": datetime.date.today().strftime("%B %d, %Y"), + } + state.update(kwargs) + save_fsspec(state, output_path) + + +def save_checkpoint( + config, + model, + optimizer, + scaler, + current_step, + epoch, + output_folder, + **kwargs, +): + file_name = "checkpoint_{}.pth.tar".format(current_step) + checkpoint_path = os.path.join(output_folder, file_name) + print("\n > CHECKPOINT : {}".format(checkpoint_path)) + save_model( + config, + model, + optimizer, + scaler, + current_step, + epoch, + checkpoint_path, + **kwargs, + ) + + +def save_best_model( + current_loss, + best_loss, + config, + model, + optimizer, + scaler, + current_step, + epoch, + out_path, + keep_all_best=False, + keep_after=10000, + **kwargs, +): + if current_loss < best_loss: + best_model_name = f"best_model_{current_step}.pth.tar" + checkpoint_path = os.path.join(out_path, best_model_name) + print(" > BEST MODEL : {}".format(checkpoint_path)) + save_model( + config, + model, + optimizer, + scaler, + current_step, + epoch, + checkpoint_path, + model_loss=current_loss, + **kwargs, + ) + fs = fsspec.get_mapper(out_path).fs + # only delete previous if current is saved successfully + if not keep_all_best or (current_step < keep_after): + model_names = fs.glob(os.path.join(out_path, "best_model*.pth.tar")) + for model_name in model_names: + if os.path.basename(model_name) != best_model_name: + fs.rm(model_name) + # create a shortcut which always points to the currently best model + shortcut_name = "best_model.pth.tar" + shortcut_path = os.path.join(out_path, shortcut_name) + fs.copy(checkpoint_path, shortcut_path) + best_loss = current_loss + return best_loss diff --git a/speaker/utils/shared_configs.py b/speaker/utils/shared_configs.py new file mode 100644 index 0000000..a89d3a9 --- /dev/null +++ b/speaker/utils/shared_configs.py @@ -0,0 +1,342 @@ +from dataclasses import asdict, dataclass +from typing import List + +from .coqpit import Coqpit, check_argument + + +@dataclass +class BaseAudioConfig(Coqpit): + """Base config to definge audio processing parameters. It is used to initialize + ```TTS.utils.audio.AudioProcessor.``` + + Args: + fft_size (int): + Number of STFT frequency levels aka.size of the linear spectogram frame. Defaults to 1024. + + win_length (int): + Each frame of audio is windowed by window of length ```win_length``` and then padded with zeros to match + ```fft_size```. Defaults to 1024. + + hop_length (int): + Number of audio samples between adjacent STFT columns. Defaults to 1024. + + frame_shift_ms (int): + Set ```hop_length``` based on milliseconds and sampling rate. + + frame_length_ms (int): + Set ```win_length``` based on milliseconds and sampling rate. + + stft_pad_mode (str): + Padding method used in STFT. 'reflect' or 'center'. Defaults to 'reflect'. + + sample_rate (int): + Audio sampling rate. Defaults to 22050. + + resample (bool): + Enable / Disable resampling audio to ```sample_rate```. Defaults to ```False```. + + preemphasis (float): + Preemphasis coefficient. Defaults to 0.0. + + ref_level_db (int): 20 + Reference Db level to rebase the audio signal and ignore the level below. 20Db is assumed the sound of air. + Defaults to 20. + + do_sound_norm (bool): + Enable / Disable sound normalization to reconcile the volume differences among samples. Defaults to False. + + log_func (str): + Numpy log function used for amplitude to DB conversion. Defaults to 'np.log10'. + + do_trim_silence (bool): + Enable / Disable trimming silences at the beginning and the end of the audio clip. Defaults to ```True```. + + do_amp_to_db_linear (bool, optional): + enable/disable amplitude to dB conversion of linear spectrograms. Defaults to True. + + do_amp_to_db_mel (bool, optional): + enable/disable amplitude to dB conversion of mel spectrograms. Defaults to True. + + trim_db (int): + Silence threshold used for silence trimming. Defaults to 45. + + power (float): + Exponent used for expanding spectrogra levels before running Griffin Lim. It helps to reduce the + artifacts in the synthesized voice. Defaults to 1.5. + + griffin_lim_iters (int): + Number of Griffing Lim iterations. Defaults to 60. + + num_mels (int): + Number of mel-basis frames that defines the frame lengths of each mel-spectrogram frame. Defaults to 80. + + mel_fmin (float): Min frequency level used for the mel-basis filters. ~50 for male and ~95 for female voices. + It needs to be adjusted for a dataset. Defaults to 0. + + mel_fmax (float): + Max frequency level used for the mel-basis filters. It needs to be adjusted for a dataset. + + spec_gain (int): + Gain applied when converting amplitude to DB. Defaults to 20. + + signal_norm (bool): + enable/disable signal normalization. Defaults to True. + + min_level_db (int): + minimum db threshold for the computed melspectrograms. Defaults to -100. + + symmetric_norm (bool): + enable/disable symmetric normalization. If set True normalization is performed in the range [-k, k] else + [0, k], Defaults to True. + + max_norm (float): + ```k``` defining the normalization range. Defaults to 4.0. + + clip_norm (bool): + enable/disable clipping the our of range values in the normalized audio signal. Defaults to True. + + stats_path (str): + Path to the computed stats file. Defaults to None. + """ + + # stft parameters + fft_size: int = 1024 + win_length: int = 1024 + hop_length: int = 256 + frame_shift_ms: int = None + frame_length_ms: int = None + stft_pad_mode: str = "reflect" + # audio processing parameters + sample_rate: int = 22050 + resample: bool = False + preemphasis: float = 0.0 + ref_level_db: int = 20 + do_sound_norm: bool = False + log_func: str = "np.log10" + # silence trimming + do_trim_silence: bool = True + trim_db: int = 45 + # griffin-lim params + power: float = 1.5 + griffin_lim_iters: int = 60 + # mel-spec params + num_mels: int = 80 + mel_fmin: float = 0.0 + mel_fmax: float = None + spec_gain: int = 20 + do_amp_to_db_linear: bool = True + do_amp_to_db_mel: bool = True + # normalization params + signal_norm: bool = True + min_level_db: int = -100 + symmetric_norm: bool = True + max_norm: float = 4.0 + clip_norm: bool = True + stats_path: str = None + + def check_values( + self, + ): + """Check config fields""" + c = asdict(self) + check_argument("num_mels", c, restricted=True, min_val=10, max_val=2056) + check_argument("fft_size", c, restricted=True, min_val=128, max_val=4058) + check_argument("sample_rate", c, restricted=True, min_val=512, max_val=100000) + check_argument( + "frame_length_ms", + c, + restricted=True, + min_val=10, + max_val=1000, + alternative="win_length", + ) + check_argument("frame_shift_ms", c, restricted=True, min_val=1, max_val=1000, alternative="hop_length") + check_argument("preemphasis", c, restricted=True, min_val=0, max_val=1) + check_argument("min_level_db", c, restricted=True, min_val=-1000, max_val=10) + check_argument("ref_level_db", c, restricted=True, min_val=0, max_val=1000) + check_argument("power", c, restricted=True, min_val=1, max_val=5) + check_argument("griffin_lim_iters", c, restricted=True, min_val=10, max_val=1000) + + # normalization parameters + check_argument("signal_norm", c, restricted=True) + check_argument("symmetric_norm", c, restricted=True) + check_argument("max_norm", c, restricted=True, min_val=0.1, max_val=1000) + check_argument("clip_norm", c, restricted=True) + check_argument("mel_fmin", c, restricted=True, min_val=0.0, max_val=1000) + check_argument("mel_fmax", c, restricted=True, min_val=500.0, allow_none=True) + check_argument("spec_gain", c, restricted=True, min_val=1, max_val=100) + check_argument("do_trim_silence", c, restricted=True) + check_argument("trim_db", c, restricted=True) + + +@dataclass +class BaseDatasetConfig(Coqpit): + """Base config for TTS datasets. + + Args: + name (str): + Dataset name that defines the preprocessor in use. Defaults to None. + + path (str): + Root path to the dataset files. Defaults to None. + + meta_file_train (str): + Name of the dataset meta file. Or a list of speakers to be ignored at training for multi-speaker datasets. + Defaults to None. + + unused_speakers (List): + List of speakers IDs that are not used at the training. Default None. + + meta_file_val (str): + Name of the dataset meta file that defines the instances used at validation. + + meta_file_attn_mask (str): + Path to the file that lists the attention mask files used with models that require attention masks to + train the duration predictor. + """ + + name: str = "" + path: str = "" + meta_file_train: str = "" + ununsed_speakers: List[str] = None + meta_file_val: str = "" + meta_file_attn_mask: str = "" + + def check_values( + self, + ): + """Check config fields""" + c = asdict(self) + check_argument("name", c, restricted=True) + check_argument("path", c, restricted=True) + check_argument("meta_file_train", c, restricted=True) + check_argument("meta_file_val", c, restricted=False) + check_argument("meta_file_attn_mask", c, restricted=False) + + +@dataclass +class BaseTrainingConfig(Coqpit): + """Base config to define the basic training parameters that are shared + among all the models. + + Args: + model (str): + Name of the model that is used in the training. + + run_name (str): + Name of the experiment. This prefixes the output folder name. Defaults to `coqui_tts`. + + run_description (str): + Short description of the experiment. + + epochs (int): + Number training epochs. Defaults to 10000. + + batch_size (int): + Training batch size. + + eval_batch_size (int): + Validation batch size. + + mixed_precision (bool): + Enable / Disable mixed precision training. It reduces the VRAM use and allows larger batch sizes, however + it may also cause numerical unstability in some cases. + + scheduler_after_epoch (bool): + If true, run the scheduler step after each epoch else run it after each model step. + + run_eval (bool): + Enable / Disable evaluation (validation) run. Defaults to True. + + test_delay_epochs (int): + Number of epochs before starting to use evaluation runs. Initially, models do not generate meaningful + results, hence waiting for a couple of epochs might save some time. + + print_eval (bool): + Enable / Disable console logging for evalutaion steps. If disabled then it only shows the final values at + the end of the evaluation. Default to ```False```. + + print_step (int): + Number of steps required to print the next training log. + + log_dashboard (str): "tensorboard" or "wandb" + Set the experiment tracking tool + + plot_step (int): + Number of steps required to log training on Tensorboard. + + model_param_stats (bool): + Enable / Disable logging internal model stats for model diagnostic. It might be useful for model debugging. + Defaults to ```False```. + + project_name (str): + Name of the project. Defaults to config.model + + wandb_entity (str): + Name of W&B entity/team. Enables collaboration across a team or org. + + log_model_step (int): + Number of steps required to log a checkpoint as W&B artifact + + save_step (int):ipt + Number of steps required to save the next checkpoint. + + checkpoint (bool): + Enable / Disable checkpointing. + + keep_all_best (bool): + Enable / Disable keeping all the saved best models instead of overwriting the previous one. Defaults + to ```False```. + + keep_after (int): + Number of steps to wait before saving all the best models. In use if ```keep_all_best == True```. Defaults + to 10000. + + num_loader_workers (int): + Number of workers for training time dataloader. + + num_eval_loader_workers (int): + Number of workers for evaluation time dataloader. + + output_path (str): + Path for training output folder, either a local file path or other + URLs supported by both fsspec and tensorboardX, e.g. GCS (gs://) or + S3 (s3://) paths. The nonexist part of the given path is created + automatically. All training artefacts are saved there. + """ + + model: str = None + run_name: str = "coqui_tts" + run_description: str = "" + # training params + epochs: int = 10000 + batch_size: int = None + eval_batch_size: int = None + mixed_precision: bool = False + scheduler_after_epoch: bool = False + # eval params + run_eval: bool = True + test_delay_epochs: int = 0 + print_eval: bool = False + # logging + dashboard_logger: str = "tensorboard" + print_step: int = 25 + plot_step: int = 100 + model_param_stats: bool = False + project_name: str = None + log_model_step: int = None + wandb_entity: str = None + # checkpointing + save_step: int = 10000 + checkpoint: bool = True + keep_all_best: bool = False + keep_after: int = 10000 + # dataloading + num_loader_workers: int = 0 + num_eval_loader_workers: int = 0 + use_noise_augment: bool = False + # paths + output_path: str = None + # distributed + distributed_backend: str = "nccl" + distributed_url: str = "tcp://localhost:54321" diff --git a/speaker_pretrain/README.md b/speaker_pretrain/README.md new file mode 100644 index 0000000..1cc5960 --- /dev/null +++ b/speaker_pretrain/README.md @@ -0,0 +1,5 @@ +Path for: + + best_model.pth.tar + + config.json \ No newline at end of file diff --git a/speaker_pretrain/config.json b/speaker_pretrain/config.json new file mode 100644 index 0000000..e330aab --- /dev/null +++ b/speaker_pretrain/config.json @@ -0,0 +1,104 @@ +{ + "model_name": "lstm", + "run_name": "mueller91", + "run_description": "train speaker encoder with voxceleb1, voxceleb2 and libriSpeech ", + "audio":{ + // Audio processing parameters + "num_mels": 80, // size of the mel spec frame. + "fft_size": 1024, // number of stft frequency levels. Size of the linear spectogram frame. + "sample_rate": 16000, // DATASET-RELATED: wav sample-rate. If different than the original data, it is resampled. + "win_length": 1024, // stft window length in ms. + "hop_length": 256, // stft window hop-lengh in ms. + "frame_length_ms": null, // stft window length in ms.If null, 'win_length' is used. + "frame_shift_ms": null, // stft window hop-lengh in ms. If null, 'hop_length' is used. + "preemphasis": 0.98, // pre-emphasis to reduce spec noise and make it more structured. If 0.0, no -pre-emphasis. + "min_level_db": -100, // normalization range + "ref_level_db": 20, // reference level db, theoretically 20db is the sound of air. + "power": 1.5, // value to sharpen wav signals after GL algorithm. + "griffin_lim_iters": 60,// #griffin-lim iterations. 30-60 is a good range. Larger the value, slower the generation. + // Normalization parameters + "signal_norm": true, // normalize the spec values in range [0, 1] + "symmetric_norm": true, // move normalization to range [-1, 1] + "max_norm": 4.0, // scale normalization to range [-max_norm, max_norm] or [0, max_norm] + "clip_norm": true, // clip normalized values into the range. + "mel_fmin": 0.0, // minimum freq level for mel-spec. ~50 for male and ~95 for female voices. Tune for dataset!! + "mel_fmax": 8000.0, // maximum freq level for mel-spec. Tune for dataset!! + "do_trim_silence": true, // enable trimming of slience of audio as you load it. LJspeech (false), TWEB (false), Nancy (true) + "trim_db": 60 // threshold for timming silence. Set this according to your dataset. + }, + "reinit_layers": [], + "loss": "angleproto", // "ge2e" to use Generalized End-to-End loss and "angleproto" to use Angular Prototypical loss (new SOTA) + "grad_clip": 3.0, // upper limit for gradients for clipping. + "epochs": 1000, // total number of epochs to train. + "lr": 0.0001, // Initial learning rate. If Noam decay is active, maximum learning rate. + "lr_decay": false, // if true, Noam learning rate decaying is applied through training. + "warmup_steps": 4000, // Noam decay steps to increase the learning rate from 0 to "lr" + "tb_model_param_stats": false, // true, plots param stats per layer on tensorboard. Might be memory consuming, but good for debugging. + "steps_plot_stats": 10, // number of steps to plot embeddings. + "num_speakers_in_batch": 64, // Batch size for training. Lower values than 32 might cause hard to learn attention. It is overwritten by 'gradual_training'. + "voice_len": 2.0, // size of the voice + "num_utters_per_speaker": 10, // + "num_loader_workers": 8, // number of training data loader processes. Don't set it too big. 4-8 are good values. + "wd": 0.000001, // Weight decay weight. + "checkpoint": true, // If true, it saves checkpoints per "save_step" + "save_step": 1000, // Number of training steps expected to save traning stats and checkpoints. + "print_step": 20, // Number of steps to log traning on console. + "output_path": "../../OutputsMozilla/checkpoints/speaker_encoder/", // DATASET-RELATED: output path for all training outputs. + "model": { + "input_dim": 80, + "proj_dim": 256, + "lstm_dim": 768, + "num_lstm_layers": 3, + "use_lstm_with_projection": true + }, + "storage": { + "sample_from_storage_p": 0.9, // the probability with which we'll sample from the DataSet in-memory storage + "storage_size": 25, // the size of the in-memory storage with respect to a single batch + "additive_noise": 1e-5 // add very small gaussian noise to the data in order to increase robustness + }, + "datasets": + [ + { + "name": "vctk_slim", + "path": "../../../audio-datasets/en/VCTK-Corpus/", + "meta_file_train": null, + "meta_file_val": null + }, + { + "name": "libri_tts", + "path": "../../../audio-datasets/en/LibriTTS/train-clean-100", + "meta_file_train": null, + "meta_file_val": null + }, + { + "name": "libri_tts", + "path": "../../../audio-datasets/en/LibriTTS/train-clean-360", + "meta_file_train": null, + "meta_file_val": null + }, + { + "name": "libri_tts", + "path": "../../../audio-datasets/en/LibriTTS/train-other-500", + "meta_file_train": null, + "meta_file_val": null + }, + { + "name": "voxceleb1", + "path": "../../../audio-datasets/en/voxceleb1/", + "meta_file_train": null, + "meta_file_val": null + }, + { + "name": "voxceleb2", + "path": "../../../audio-datasets/en/voxceleb2/", + "meta_file_train": null, + "meta_file_val": null + }, + { + "name": "common_voice", + "path": "../../../audio-datasets/en/MozillaCommonVoice", + "meta_file_train": "train.tsv", + "meta_file_val": "test.tsv" + } + ] +} \ No newline at end of file diff --git a/spec/inference.py b/spec/inference.py new file mode 100644 index 0000000..6cc4042 --- /dev/null +++ b/spec/inference.py @@ -0,0 +1,113 @@ +import argparse +import torch +import torch.utils.data +import numpy as np +import librosa +from omegaconf import OmegaConf +from librosa.filters import mel as librosa_mel_fn + + +MAX_WAV_VALUE = 32768.0 + + +def load_wav_to_torch(full_path, sample_rate): + wav, _ = librosa.load(full_path, sr=sample_rate) + wav = wav / np.abs(wav).max() * 0.6 + return torch.FloatTensor(wav) + + +def dynamic_range_compression(x, C=1, clip_val=1e-5): + return np.log(np.clip(x, a_min=clip_val, a_max=None) * C) + + +def dynamic_range_decompression(x, C=1): + return np.exp(x) / C + + +def dynamic_range_compression_torch(x, C=1, clip_val=1e-5): + return torch.log(torch.clamp(x, min=clip_val) * C) + + +def dynamic_range_decompression_torch(x, C=1): + return torch.exp(x) / C + + +def spectral_normalize_torch(magnitudes): + output = dynamic_range_compression_torch(magnitudes) + return output + + +def spectral_de_normalize_torch(magnitudes): + output = dynamic_range_decompression_torch(magnitudes) + return output + + +mel_basis = {} +hann_window = {} + + +def mel_spectrogram(y, n_fft, num_mels, sampling_rate, hop_size, win_size, fmin, fmax, center=False): + if torch.min(y) < -1.: + print('min value is ', torch.min(y)) + if torch.max(y) > 1.: + print('max value is ', torch.max(y)) + + global mel_basis, hann_window + if fmax not in mel_basis: + mel = librosa_mel_fn(sr=sampling_rate, n_fft=n_fft, n_mels=num_mels, fmin=fmin, fmax=fmax) + mel_basis[str(fmax)+'_'+str(y.device)] = torch.from_numpy(mel).float().to(y.device) + hann_window[str(y.device)] = torch.hann_window(win_size).to(y.device) + + y = torch.nn.functional.pad(y.unsqueeze(1), (int((n_fft-hop_size)/2), int((n_fft-hop_size)/2)), mode='reflect') + y = y.squeeze(1) + + # complex tensor as default, then use view_as_real for future pytorch compatibility + spec = torch.stft(y, n_fft, hop_length=hop_size, win_length=win_size, window=hann_window[str(y.device)], + center=center, pad_mode='reflect', normalized=False, onesided=True, return_complex=True) + spec = torch.view_as_real(spec) + spec = torch.sqrt(spec.pow(2).sum(-1)+(1e-9)) + + spec = torch.matmul(mel_basis[str(fmax)+'_'+str(y.device)], spec) + spec = spectral_normalize_torch(spec) + + return spec + + +def mel_spectrogram_file(path, hps): + audio = load_wav_to_torch(path, hps.data.sampling_rate) + audio = audio.unsqueeze(0) + + # match audio length to self.hop_length * n for evaluation + if (audio.size(1) % hps.data.hop_length) != 0: + audio = audio[:, :-(audio.size(1) % hps.data.hop_length)] + mel = mel_spectrogram(audio, hps.data.filter_length, hps.data.mel_channels, hps.data.sampling_rate, + hps.data.hop_length, hps.data.win_length, hps.data.mel_fmin, hps.data.mel_fmax, center=False) + return mel + + +def print_mel(mel, path="mel.png"): + import matplotlib.pyplot as plt + fig = plt.figure(figsize=(12, 4)) + if isinstance(mel, torch.Tensor): + mel = mel.cpu().numpy() + plt.pcolor(mel) + plt.savefig(path, format="png") + plt.close(fig) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-w", "--wav", help="wav", dest="wav") + parser.add_argument("-m", "--mel", help="mel", dest="mel") # csv for excel + args = parser.parse_args() + print(args.wav) + print(args.mel) + + hps = OmegaConf.load(f"./configs/base.yaml") + + mel = mel_spectrogram_file(args.wav, hps) + # TODO + mel = torch.squeeze(mel, 0) + # [100, length] + torch.save(mel, args.mel) + print_mel(mel, "debug.mel.png")