diff --git a/.cardboardlint.yml b/.cardboardlint.yml
index 464ea733..4a115a37 100644
--- a/.cardboardlint.yml
+++ b/.cardboardlint.yml
@@ -1,2 +1,5 @@
linters:
- pylint:
+ # pylintrc: pylintrc
+ filefilter: ['- test_*.py', '+ *.py', '- *.npy']
+ # exclude:
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index ff377f69..694bb36f 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -6,8 +6,9 @@ labels: ''
assignees: ''
---
+Questions will not be answered here!!
-Please consider posting on [TTS Discourse](https://discourse.mozilla.org/c/tts) page, if your issue is not directly related to TTS development process.
+Please consider posting on [TTS Discourse](https://discourse.mozilla.org/c/tts) page if your issue is not directly related to TTS development (Bugs, code updates etc.).
You can also check https://github.com/mozilla/TTS/wiki/FAQ for common questions and answers.
diff --git a/.pylintrc b/.pylintrc
index b6e04944..a78b521e 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -157,7 +157,8 @@ disable=missing-docstring,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
- comprehension-escape
+ comprehension-escape,
+ duplicate-code
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
diff --git a/.travis.yml b/.travis.yml
index 645f9861..83ba25a3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,6 +3,10 @@ language: python
git:
quiet: true
+before_install:
+ - sudo apt-get update
+ - sudo apt-get -y install espeak
+
matrix:
include:
- name: "Lint check"
diff --git a/.travis/script b/.travis/script
index ca6f4cd3..ce886fc2 100755
--- a/.travis/script
+++ b/.travis/script
@@ -12,7 +12,9 @@ fi
if [[ "$TEST_SUITE" == "unittest" ]]; then
# Run tests on all pushes
pushd tts_namespace
- python -m unittest
+ nosetests TTS.speaker_encoder.tests --nocapture
+ nosetests TTS.vocoder.tests --nocapture
+ nosetests TTS.tests --nocapture
popd
# Test server package
./tests/test_server_package.sh
diff --git a/README.md b/README.md
index 4adae507..13d6bcfc 100644
--- a/README.md
+++ b/README.md
@@ -3,25 +3,49 @@
-This project is a part of [Mozilla Common Voice](https://voice.mozilla.org/en). TTS aims a deep learning based Text2Speech engine, low in cost and high in quality. To begin with, you can hear a sample generated voice from [here](https://soundcloud.com/user-565970875/commonvoice-loc-sens-attn).
-
-TTS includes two different model implementations which are based on [Tacotron](https://arxiv.org/abs/1703.10135) and [Tacotron2](https://arxiv.org/abs/1712.05884). Tacotron is smaller, efficient and easier to train but Tacotron2 provides better results, especially when it is combined with a Neural vocoder. Therefore, choose depending on your project requirements.
+This project is a part of [Mozilla Common Voice](https://voice.mozilla.org/en). TTS aims a deep learning based Text2Speech engine, low in cost and high in quality. To begin with, you can hear a sample synthesized voice from [here](https://soundcloud.com/user-565970875/commonvoice-loc-sens-attn).
If you are new, you can also find [here](http://www.erogol.com/text-speech-deep-learning-architectures/) a brief post about TTS architectures and their comparisons.
-## TTS Performance
+[](https://sourcerer.io/fame/erogol/erogol/TTS/links/0)[](https://sourcerer.io/fame/erogol/erogol/TTS/links/1)[](https://sourcerer.io/fame/erogol/erogol/TTS/links/2)[](https://sourcerer.io/fame/erogol/erogol/TTS/links/3)[](https://sourcerer.io/fame/erogol/erogol/TTS/links/4)[](https://sourcerer.io/fame/erogol/erogol/TTS/links/5)[](https://sourcerer.io/fame/erogol/erogol/TTS/links/6)[](https://sourcerer.io/fame/erogol/erogol/TTS/links/7)
+
+## TTS Performance
[Details...](https://github.com/mozilla/TTS/wiki/Mean-Opinion-Score-Results)
+## Provided Models and Methods
+Text-to-Spectrogram:
+- Tacotron: [paper](https://arxiv.org/abs/1703.10135)
+- Tacotron2: [paper](https://arxiv.org/abs/1712.05884)
+
+Attention Methods:
+- Guided Attention: [paper](https://arxiv.org/abs/1710.08969)
+- Forward Backward Decoding: [paper](https://arxiv.org/abs/1907.09006)
+- Graves Attention: [paper](https://arxiv.org/abs/1907.09006)
+- Double Decoder Consistency: [blog](https://erogol.com/solving-attention-problems-of-tts-models-with-double-decoder-consistency/)
+
+Speaker Encoder:
+- GE2E: [paper](https://arxiv.org/abs/1710.10467)
+
+Vocoders:
+- MelGAN: [paper](https://arxiv.org/abs/1710.10467)
+- MultiBandMelGAN: [paper](https://arxiv.org/abs/2005.05106)
+- GAN-TTS discriminators: [paper](https://arxiv.org/abs/1909.11646)
+
+You can also help us implement more models. Some TTS related work can be found [here](https://github.com/erogol/TTS-papers).
+
## Features
-- High performance Text2Speech models on Torch and Tensorflow 2.0.
-- High performance Speaker Encoder to compute speaker embeddings efficiently.
-- Integration with various Neural Vocoders (PWGAN, MelGAN, WaveRNN)
-- Released trained models.
-- Efficient training codes for PyTorch. (soon for Tensorflow 2.0)
-- Codes to convert Torch models to Tensorflow 2.0.
-- Detailed training anlaysis on console and Tensorboard.
+- High performance Deep Learning models for Text2Speech related tasks.
+ - Text2Speech models (Tacotron, Tacotron2).
+ - Speaker Encoder to compute speaker embeddings efficiently.
+ - Vocoder models (MelGAN, Multiband-MelGAN, GAN-TTS)
+- Support for multi-speaker TTS training.
+- Support for Multi-GPUs training.
+- Ability to convert Torch models to Tensorflow 2.0 for inference.
+- Released pre-trained models.
+- Fast and efficient model training.
+- Detailed training logs on console and Tensorboard.
- Tools to curate Text2Speech datasets under```dataset_analysis```.
- Demo server for model testing.
- Notebooks for extensive model benchmarking.
@@ -45,6 +69,22 @@ Or you can use ```requirements.txt``` to install the requirements only.
```pip install -r requirements.txt```
+### Directory Structure
+```
+|- TTS/
+| |- train.py (train your TTS model.)
+| |- distribute.py (train your TTS model using Multiple GPUs)
+| |- config.json (TTS model configuration file)
+| |- tf/ (Tensorflow 2 utilities and model implementations)
+| |- layers/ (model layer definitions)
+| |- models/ (model definitions)
+| |- notebooks/ (Jupyter Notebooks for model evaluation and parameter selection)
+| |- data_analysis/ (TTS Dataset analysis tools and notebooks.)
+| |- utils/ (TTS utilities -io, visualization, data processing etc.-)
+| |- speaker_encoder/ (Speaker Encoder implementation with the same folder structure.)
+| |- vocoder/ (Vocoder implementations with the same folder structure.)
+```
+
### Docker
A barebone `Dockerfile` exists at the root of the project, which should let you quickly setup the environment. By default, it will start the server and let you query it. Make sure to use `nvidia-docker` to use your GPUs. Make sure you follow the instructions in the [`server README`](server/README.md) before you build your image so that the server can find the model within the image.
@@ -61,7 +101,7 @@ Below you see Tacotron model state after 16K iterations with batch-size 32 with
> "Recent research at Harvard has shown meditating for as little as 8 weeks can actually increase the grey matter in the parts of the brain responsible for emotional regulation and learning."
-Audio examples: [https://soundcloud.com/user-565970875](https://soundcloud.com/user-565970875)
+Audio examples: [soundcloud](https://soundcloud.com/user-565970875/pocket-article-wavernn-and-tacotron2)
@@ -82,7 +122,7 @@ Audio length is approximately 6 secs.
## Datasets and Data-Loading
-TTS provides a generic dataloder easy to use for new datasets. You need to write an preprocessor function to integrate your own dataset.Check ```datasets/preprocess.py``` to see some examples. After the function, you need to set ```dataset``` field in ```config.json```. Do not forget other data related fields too.
+TTS provides a generic dataloader easy to use for new datasets. You need to write an preprocessor function to integrate your own dataset.Check ```datasets/preprocess.py``` to see some examples. After the function, you need to set ```dataset``` field in ```config.json```. Do not forget other data related fields too.
Some of the open-sourced datasets that we successfully applied TTS, are linked below.
@@ -94,9 +134,9 @@ Some of the open-sourced datasets that we successfully applied TTS, are linked b
- [Spanish](https://drive.google.com/file/d/1Sm_zyBo67XHkiFhcRSQ4YaHPYM0slO_e/view?usp=sharing) - thx! @carlfm01
## Training and Fine-tuning LJ-Speech
-Here you can find a [CoLab](https://gist.github.com/erogol/97516ad65b44dbddb8cd694953187c5b) notebook for a hands-on example, training LJSpeech. Or you can manually follow the guideline below.
+Here you can find a [CoLab](https://gist.github.com/erogol/97516ad65b44dbddb8cd694953187c5b) notebook for a hands-on example, training LJSpeech. Or you can manually follow the guideline below.
-To start with, split ```metadata.csv``` into train and validation subsets respectively ```metadata_train.csv``` and ```metadata_val.csv```. Note that for text-to-speech, validation performance might be misleading since the loss value does not directly measure the voice quality to the human ear and it also does not measure the attention module performance. Therefore, running the model with new sentences and listening to the results is the best way to go.
+To start with, split ```metadata.csv``` into train and validation subsets respectively ```metadata_train.csv``` and ```metadata_val.csv```. Note that for text-to-speech, validation performance might be misleading since the loss value does not directly measure the voice quality to the human ear and it also does not measure the attention module performance. Therefore, running the model with new sentences and listening to the results is the best way to go.
```
shuf metadata.csv > metadata_shuf.csv
@@ -135,10 +175,10 @@ cardboardlinter --refspec master
```
## Collaborative Experimentation Guide
-If you like to use TTS to try a new idea and like to share your experiments with the community, we urge you to use the following guideline for a better collaboration.
+If you like to use TTS to try a new idea and like to share your experiments with the community, we urge you to use the following guideline for a better collaboration.
(If you have an idea for better collaboration, let us know)
- Create a new branch.
-- Open an issue pointing your branch.
+- Open an issue pointing your branch.
- Explain your experiment.
- Share your results as you proceed. (Tensorboard log files, audio results, visuals etc.)
- Use LJSpeech dataset (for English) if you like to compare results with the released models. (It is the most open scalable dataset for quick experimentation)
diff --git a/compute_statistics.py b/compute_statistics.py
index bbedf7af..f718cde3 100755
--- a/compute_statistics.py
+++ b/compute_statistics.py
@@ -8,7 +8,7 @@ import numpy as np
from tqdm import tqdm
from TTS.datasets.preprocess import load_meta_data
-from TTS.utils.generic_utils import load_config
+from TTS.utils.io import load_config
from TTS.utils.audio import AudioProcessor
def main():
@@ -63,6 +63,11 @@ def main():
stats['linear_mean'] = linear_mean
stats['linear_std'] = linear_scale
+ print(f' > Avg mel spec mean: {mel_mean.mean()}')
+ print(f' > Avg mel spec scale: {mel_scale.mean()}')
+ print(f' > Avg linear spec mean: {linear_mean.mean()}')
+ print(f' > Avg lienar spec scale: {linear_scale.mean()}')
+
# set default config values for mean-var scaling
CONFIG.audio['stats_path'] = output_file_path
CONFIG.audio['signal_norm'] = True
@@ -73,6 +78,7 @@ def main():
del CONFIG.audio['clip_norm']
stats['audio_config'] = CONFIG.audio
np.save(output_file_path, stats, allow_pickle=True)
+ print(f' > scale_stats.npy is saved to {output_file_path}')
if __name__ == "__main__":
diff --git a/config.json b/config.json
index 03907fb0..77a223dc 100644
--- a/config.json
+++ b/config.json
@@ -1,24 +1,24 @@
{
"model": "Tacotron2",
- "run_name": "ljspeech",
- "run_description": "tacotron2",
+ "run_name": "ljspeech-ddc-bn",
+ "run_description": "tacotron2 with ddc and batch-normalization",
// AUDIO PARAMETERS
"audio":{
// stft parameters
- "num_freq": 513, // number of stft frequency levels. Size of the linear spectogram frame.
+ "fft_size": 1024, // number of stft frequency levels. Size of the linear spectogram frame.
"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.
// Audio processing parameters
- "sample_rate": 22050, // DATASET-RELATED: wav sample-rate. If different than the original data, it is resampled.
+ "sample_rate": 22050, // DATASET-RELATED: wav sample-rate.
"preemphasis": 0.0, // pre-emphasis to reduce spec noise and make it more structured. If 0.0, no -pre-emphasis.
"ref_level_db": 20, // reference level db, theoretically 20db is the sound of air.
// Silence trimming
- "do_trim_silence": true,// enable trimming of slience of audio as you load it. LJspeech (false), TWEB (false), Nancy (true)
+ "do_trim_silence": true,// enable trimming of slience of audio as you load it. LJspeech (true), TWEB (false), Nancy (true)
"trim_db": 60, // threshold for timming silence. Set this according to your dataset.
// Griffin-Lim
@@ -83,27 +83,30 @@
// TACOTRON PRENET
"memory_size": -1, // ONLY TACOTRON - size of the memory queue used fro storing last decoder predictions for auto-regression. If < 0, memory queue is disabled and decoder only uses the last prediction frame.
- "prenet_type": "original", // "original" or "bn".
- "prenet_dropout": true, // enable/disable dropout at prenet.
+ "prenet_type": "bn", // "original" or "bn".
+ "prenet_dropout": false, // enable/disable dropout at prenet.
// ATTENTION
"attention_type": "original", // 'original' or 'graves'
"attention_heads": 4, // number of attention heads (only for 'graves')
- "attention_norm": "sigmoid", // softmax or sigmoid. Suggested to use softmax for Tacotron2 and sigmoid for Tacotron.
+ "attention_norm": "sigmoid", // softmax or sigmoid.
"windowing": false, // Enables attention windowing. Used only in eval mode.
"use_forward_attn": false, // if it uses forward attention. In general, it aligns faster.
"forward_attn_mask": false, // Additional masking forcing monotonicity only in eval mode.
"transition_agent": false, // enable/disable transition agent of forward attention.
"location_attn": true, // enable_disable location sensitive attention. It is enabled for TACOTRON by default.
"bidirectional_decoder": false, // use https://arxiv.org/abs/1907.09006. Use it, if attention does not work well with your dataset.
+ "double_decoder_consistency": true, // use DDC explained here https://erogol.com/solving-attention-problems-of-tts-models-with-double-decoder-consistency-draft/
+ "ddc_r": 7, // reduction rate for coarse decoder.
// STOPNET
"stopnet": true, // Train stopnet predicting the end of synthesis.
"separate_stopnet": true, // Train stopnet seperately if 'stopnet==true'. It prevents stopnet loss to influence the rest of the model. It causes a better model, but it trains SLOWER.
// TENSORBOARD and LOGGING
- "print_step": 25, // Number of steps to log traning on console.
- "print_eval": false, // If True, it prints loss values in evalulation.
+ "print_step": 25, // Number of steps to log training on console.
+ "tb_plot_step:": 100, // Number of steps to plot TB training figures.
+ "print_eval": false, // If True, it prints intermediate loss values in evalulation.
"save_step": 10000, // Number of training steps expected to save traninpg stats and checkpoints.
"checkpoint": true, // If true, it saves checkpoints per "save_step"
"tb_model_param_stats": false, // true, plots param stats per layer on tensorboard. Might be memory consuming, but good for debugging.
@@ -122,7 +125,7 @@
// PHONEMES
"phoneme_cache_path": "/media/erogol/data_ssd2/mozilla_us_phonemes_3", // phoneme computation is slow, therefore, it caches results in the given folder.
- "use_phonemes": false, // use phonemes instead of raw characters. It is suggested for better pronounciation.
+ "use_phonemes": true, // use phonemes instead of raw characters. It is suggested for better pronounciation.
"phoneme_language": "en-us", // depending on your target language, pick one from https://github.com/bootphon/phonemizer#languages
// MULTI-SPEAKER and GST
diff --git a/config_template.json b/config_template.json
deleted file mode 100644
index e525ec31..00000000
--- a/config_template.json
+++ /dev/null
@@ -1,134 +0,0 @@
-{
- "model": "Tacotron2", // one of the model in models/
- "run_name": "ljspeech-stft_params",
- "run_description": "tacotron2 cosntant stf parameters",
-
- // AUDIO PARAMETERS
- "audio":{
- // Audio processing parameters
- "num_mels": 80, // size of the mel spec frame.
- "num_freq": 513, // number of stft frequency levels. Size of the linear spectogram frame.
- "sample_rate": 22050, // 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.
- "preemphasis": 0.0, // pre-emphasis to reduce spec noise and make it more structured. If 0.0, no -pre-emphasis.
- "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.
- "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": 1.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.
- },
-
- // VOCABULARY PARAMETERS
- // if custom character set is not defined,
- // default set in symbols.py is used
- "characters":{
- "pad": "_",
- "eos": "~",
- "bos": "^",
- "characters": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!'(),-.:;? ",
- "punctuations":"!'(),-.:;? ",
- "phonemes":"iyɨʉɯuɪʏʊeøɘəɵɤoɛœɜɞʌɔæɐaɶɑɒᵻʘɓǀɗǃʄǂɠǁʛpbtdʈɖcɟkɡqɢʔɴŋɲɳnɱmʙrʀⱱɾɽɸβfvθðszʃʒʂʐçʝxɣχʁħʕhɦɬɮʋɹɻjɰlɭʎʟˈˌːˑʍwɥʜʢʡɕʑɺɧɚ˞ɫ"
- },
-
- // DISTRIBUTED TRAINING
- "distributed":{
- "backend": "nccl",
- "url": "tcp:\/\/localhost:54321"
- },
-
- "reinit_layers": [], // give a list of layer names to restore from the given checkpoint. If not defined, it reloads all heuristically matching layers.
-
- // TRAINING
- "batch_size": 32, // Batch size for training. Lower values than 32 might cause hard to learn attention. It is overwritten by 'gradual_training'.
- "eval_batch_size":16,
- "r": 7, // Number of decoder frames to predict per iteration. Set the initial values if gradual training is enabled.
- "gradual_training": [[0, 7, 64], [1, 5, 64], [50000, 3, 32], [130000, 2, 32], [290000, 1, 32]], //set gradual training steps [first_step, r, batch_size]. If it is null, gradual training is disabled. For Tacotron, you might need to reduce the 'batch_size' as you proceeed.
- "loss_masking": true, // enable / disable loss masking against the sequence padding.
- "ga_alpha": 10.0, // weight for guided attention loss. If > 0, guided attention is enabled.
-
- // VALIDATION
- "run_eval": true,
- "test_delay_epochs": 10, //Until attention is aligned, testing only wastes computation time.
- "test_sentences_file": null, // set a file to load sentences to be used for testing. If it is null then we use default english sentences.
-
- // OPTIMIZER
- "noam_schedule": false, // use noam warmup and lr schedule.
- "grad_clip": 1.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.
- "wd": 0.000001, // Weight decay weight.
- "warmup_steps": 4000, // Noam decay steps to increase the learning rate from 0 to "lr"
- "seq_len_norm": false, // Normalize eash sample loss with its length to alleviate imbalanced datasets. Use it if your dataset is small or has skewed distribution of sequence lengths.
-
- // TACOTRON PRENET
- "memory_size": -1, // ONLY TACOTRON - size of the memory queue used fro storing last decoder predictions for auto-regression. If < 0, memory queue is disabled and decoder only uses the last prediction frame.
- "prenet_type": "original", // "original" or "bn".
- "prenet_dropout": true, // enable/disable dropout at prenet.
-
- // ATTENTION
- "attention_type": "original", // 'original' or 'graves'
- "attention_heads": 4, // number of attention heads (only for 'graves')
- "attention_norm": "sigmoid", // softmax or sigmoid. Suggested to use softmax for Tacotron2 and sigmoid for Tacotron.
- "windowing": false, // Enables attention windowing. Used only in eval mode.
- "use_forward_attn": false, // if it uses forward attention. In general, it aligns faster.
- "forward_attn_mask": false, // Additional masking forcing monotonicity only in eval mode.
- "transition_agent": false, // enable/disable transition agent of forward attention.
- "location_attn": true, // enable_disable location sensitive attention. It is enabled for TACOTRON by default.
- "bidirectional_decoder": false, // use https://arxiv.org/abs/1907.09006. Use it, if attention does not work well with your dataset.
-
- // STOPNET
- "stopnet": true, // Train stopnet predicting the end of synthesis.
- "separate_stopnet": true, // Train stopnet seperately if 'stopnet==true'. It prevents stopnet loss to influence the rest of the model. It causes a better model, but it trains SLOWER.
-
- // TENSORBOARD and LOGGING
- "print_step": 25, // Number of steps to log traning on console.
- "save_step": 10000, // Number of training steps expected to save traninpg stats and checkpoints.
- "checkpoint": true, // If true, it saves checkpoints per "save_step"
- "tb_model_param_stats": false, // true, plots param stats per layer on tensorboard. Might be memory consuming, but good for debugging.
-
- // DATA LOADING
- "text_cleaner": "phoneme_cleaners",
- "enable_eos_bos_chars": false, // enable/disable beginning of sentence and end of sentence chars.
- "num_loader_workers": 4, // number of training data loader processes. Don't set it too big. 4-8 are good values.
- "num_val_loader_workers": 4, // number of evaluation data loader processes.
- "batch_group_size": 0, //Number of batches to shuffle after bucketing.
- "min_seq_len": 6, // DATASET-RELATED: minimum text length to use in training
- "max_seq_len": 153, // DATASET-RELATED: maximum text length
-
- // PATHS
- "output_path": "/data4/rw/home/Trainings/",
-
- // PHONEMES
- "phoneme_cache_path": "mozilla_us_phonemes_3", // phoneme computation is slow, therefore, it caches results in the given folder.
- "use_phonemes": true, // use phonemes instead of raw characters. It is suggested for better pronounciation.
- "phoneme_language": "en-us", // depending on your target language, pick one from https://github.com/bootphon/phonemizer#languages
-
- // MULTI-SPEAKER and GST
- "use_speaker_embedding": false, // use speaker embedding to enable multi-speaker learning.
- "style_wav_for_test": null, // path to style wav file to be used in TacotronGST inference.
- "use_gst": false, // TACOTRON ONLY: use global style tokens
-
- // DATASETS
- "datasets": // List of datasets. They all merged and they get different speaker_ids.
- [
- {
- "name": "ljspeech",
- "path": "/root/LJSpeech-1.1/",
- "meta_file_train": "metadata.csv",
- "meta_file_val": null
- }
- ]
-
-}
-
diff --git a/datasets/TTSDataset.py b/datasets/TTSDataset.py
index 0d884c00..7fe966d7 100644
--- a/datasets/TTSDataset.py
+++ b/datasets/TTSDataset.py
@@ -92,7 +92,7 @@ class MyDataset(Dataset):
return phonemes
def _load_or_generate_phoneme_sequence(self, wav_file, text):
- file_name = os.path.basename(wav_file).split('.')[0]
+ file_name = os.path.splitext(os.path.basename(wav_file))[0]
cache_path = os.path.join(self.phoneme_cache_path,
file_name + '_phoneme.npy')
try:
diff --git a/datasets/preprocess.py b/datasets/preprocess.py
index ce876edc..e8700c6b 100644
--- a/datasets/preprocess.py
+++ b/datasets/preprocess.py
@@ -120,7 +120,7 @@ def mailabs(root_path, meta_files=None):
text = cols[1].strip()
items.append([text, wav_file, speaker_name])
else:
- raise RuntimeError("> File %s is not exist!"%(wav_file))
+ raise RuntimeError("> File %s does not exist!"%(wav_file))
return items
@@ -185,7 +185,7 @@ def libri_tts(root_path, meta_files=None):
text = cols[1]
items.append([text, wav_file, speaker_name])
for item in items:
- assert os.path.exists(item[1]), f" [!] wav file is not exist - {item[1]}"
+ assert os.path.exists(item[1]), f" [!] wav files don't exist - {item[1]}"
return items
@@ -203,5 +203,5 @@ def custom_turkish(root_path, meta_file):
continue
text = cols[1].strip()
items.append([text, wav_file, speaker_name])
- print(f" [!] {len(skipped_files)} files skipped. They are not exist...")
+ print(f" [!] {len(skipped_files)} files skipped. They don't exist...")
return items
diff --git a/layers/custom_layers.py b/layers/custom_layers.py
deleted file mode 100644
index 72668c97..00000000
--- a/layers/custom_layers.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# coding: utf-8
-# import torch
-# from torch import nn
-
-# class StopProjection(nn.Module):
-# r""" Simple projection layer to predict the "stop token"
-
-# Args:
-# in_features (int): size of the input vector
-# out_features (int or list): size of each output vector. aka number
-# of predicted frames.
-# """
-
-# def __init__(self, in_features, out_features):
-# super(StopProjection, self).__init__()
-# self.linear = nn.Linear(in_features, out_features)
-# self.dropout = nn.Dropout(0.5)
-# self.sigmoid = nn.Sigmoid()
-
-# def forward(self, inputs):
-# out = self.dropout(inputs)
-# out = self.linear(out)
-# out = self.sigmoid(out)
-# return out
diff --git a/layers/losses.py b/layers/losses.py
index 608e247d..f7745b6e 100644
--- a/layers/losses.py
+++ b/layers/losses.py
@@ -184,7 +184,7 @@ class TacotronLoss(torch.nn.Module):
def forward(self, postnet_output, decoder_output, mel_input, linear_input,
stopnet_output, stopnet_target, output_lens, decoder_b_output,
- alignments, alignment_lens, input_lens):
+ alignments, alignment_lens, alignments_backwards, input_lens):
return_dict = {}
# decoder and postnet losses
@@ -226,6 +226,15 @@ class TacotronLoss(torch.nn.Module):
return_dict['decoder_b_loss'] = decoder_b_loss
return_dict['decoder_c_loss'] = decoder_c_loss
+ # double decoder consistency loss (if enabled)
+ if self.config.double_decoder_consistency:
+ decoder_b_loss = self.criterion(decoder_b_output, mel_input, output_lens)
+ # decoder_c_loss = torch.nn.functional.l1_loss(decoder_b_output, decoder_output)
+ attention_c_loss = torch.nn.functional.l1_loss(alignments, alignments_backwards)
+ loss += decoder_b_loss + attention_c_loss
+ return_dict['decoder_coarse_loss'] = decoder_b_loss
+ return_dict['decoder_ddc_loss'] = attention_c_loss
+
# guided attention loss (if enabled)
if self.config.ga_alpha > 0:
ga_loss = self.criterion_ga(alignments, input_lens, alignment_lens)
diff --git a/models/tacotron.py b/models/tacotron.py
index fba82b1b..ba42610c 100644
--- a/models/tacotron.py
+++ b/models/tacotron.py
@@ -1,23 +1,21 @@
# coding: utf-8
import torch
-import copy
from torch import nn
-from TTS.layers.tacotron import Encoder, Decoder, PostCBHG
-from TTS.utils.generic_utils import sequence_mask
+
from TTS.layers.gst_layers import GST
+from TTS.layers.tacotron import Decoder, Encoder, PostCBHG
+from TTS.models.tacotron_abstract import TacotronAbstract
-class Tacotron(nn.Module):
+class Tacotron(TacotronAbstract):
def __init__(self,
num_chars,
num_speakers,
r=5,
postnet_output_dim=1025,
decoder_output_dim=80,
- memory_size=5,
attn_type='original',
attn_win=False,
- gst=False,
attn_norm="sigmoid",
prenet_type="original",
prenet_dropout=True,
@@ -27,38 +25,41 @@ class Tacotron(nn.Module):
location_attn=True,
attn_K=5,
separate_stopnet=True,
- bidirectional_decoder=False):
- super(Tacotron, self).__init__()
- self.r = r
- self.decoder_output_dim = decoder_output_dim
- self.postnet_output_dim = postnet_output_dim
- self.gst = gst
- self.num_speakers = num_speakers
- self.bidirectional_decoder = bidirectional_decoder
- decoder_dim = 512 if num_speakers > 1 else 256
- encoder_dim = 512 if num_speakers > 1 else 256
+ bidirectional_decoder=False,
+ double_decoder_consistency=False,
+ ddc_r=None,
+ gst=False,
+ memory_size=5):
+ super(Tacotron,
+ self).__init__(num_chars, num_speakers, r, postnet_output_dim,
+ decoder_output_dim, attn_type, attn_win,
+ attn_norm, prenet_type, prenet_dropout,
+ forward_attn, trans_agent, forward_attn_mask,
+ location_attn, attn_K, separate_stopnet,
+ bidirectional_decoder, double_decoder_consistency,
+ ddc_r, gst)
+ decoder_in_features = 512 if num_speakers > 1 else 256
+ encoder_in_features = 512 if num_speakers > 1 else 256
+ speaker_embedding_dim = 256
proj_speaker_dim = 80 if num_speakers > 1 else 0
- # embedding layer
+ # base model layers
self.embedding = nn.Embedding(num_chars, 256, padding_idx=0)
self.embedding.weight.data.normal_(0, 0.3)
- # boilerplate model
- self.encoder = Encoder(encoder_dim)
- self.decoder = Decoder(decoder_dim, decoder_output_dim, r, memory_size, attn_type, attn_win,
- attn_norm, prenet_type, prenet_dropout,
- forward_attn, trans_agent, forward_attn_mask,
- location_attn, attn_K, separate_stopnet,
- proj_speaker_dim)
- if self.bidirectional_decoder:
- self.decoder_backward = copy.deepcopy(self.decoder)
+ self.encoder = Encoder(encoder_in_features)
+ self.decoder = Decoder(decoder_in_features, decoder_output_dim, r,
+ memory_size, attn_type, attn_win, attn_norm,
+ prenet_type, prenet_dropout, forward_attn,
+ trans_agent, forward_attn_mask, location_attn,
+ attn_K, separate_stopnet, proj_speaker_dim)
self.postnet = PostCBHG(decoder_output_dim)
self.last_linear = nn.Linear(self.postnet.cbhg.gru_features * 2,
postnet_output_dim)
# speaker embedding layers
if num_speakers > 1:
- self.speaker_embedding = nn.Embedding(num_speakers, 256)
+ self.speaker_embedding = nn.Embedding(num_speakers, speaker_embedding_dim)
self.speaker_embedding.weight.data.normal_(0, 0.3)
self.speaker_project_mel = nn.Sequential(
- nn.Linear(256, proj_speaker_dim), nn.Tanh())
+ nn.Linear(speaker_embedding_dim, proj_speaker_dim), nn.Tanh())
self.speaker_embeddings = None
self.speaker_embeddings_projected = None
# global style token layers
@@ -68,28 +69,19 @@ class Tacotron(nn.Module):
num_heads=4,
num_style_tokens=10,
embedding_dim=gst_embedding_dim)
+ # backward pass decoder
+ if self.bidirectional_decoder:
+ self._init_backward_decoder()
+ # setup DDC
+ if self.double_decoder_consistency:
+ self.coarse_decoder = Decoder(
+ decoder_in_features, decoder_output_dim, ddc_r, memory_size,
+ attn_type, attn_win, attn_norm, prenet_type, prenet_dropout,
+ forward_attn, trans_agent, forward_attn_mask, location_attn,
+ attn_K, separate_stopnet, proj_speaker_dim)
- def _init_states(self):
- self.speaker_embeddings = None
- self.speaker_embeddings_projected = None
- def compute_speaker_embedding(self, speaker_ids):
- if hasattr(self, "speaker_embedding") and speaker_ids is None:
- raise RuntimeError(
- " [!] Model has speaker embedding layer but speaker_id is not provided"
- )
- if hasattr(self, "speaker_embedding") and speaker_ids is not None:
- self.speaker_embeddings = self._compute_speaker_embedding(
- speaker_ids)
- self.speaker_embeddings_projected = self.speaker_project_mel(
- self.speaker_embeddings).squeeze(1)
-
- def compute_gst(self, inputs, mel_specs):
- gst_outputs = self.gst_layer(mel_specs)
- inputs = self._add_speaker_embedding(inputs, gst_outputs)
- return inputs
-
- def forward(self, characters, text_lengths, mel_specs, speaker_ids=None):
+ def forward(self, characters, text_lengths, mel_specs, mel_lengths=None, speaker_ids=None):
"""
Shapes:
- characters: B x T_in
@@ -98,45 +90,59 @@ class Tacotron(nn.Module):
- speaker_ids: B x 1
"""
self._init_states()
- mask = sequence_mask(text_lengths).to(characters.device)
+ input_mask, output_mask = self.compute_masks(text_lengths, mel_lengths)
# B x T_in x embed_dim
inputs = self.embedding(characters)
# B x speaker_embed_dim
- self.compute_speaker_embedding(speaker_ids)
+ if speaker_ids is not None:
+ self.compute_speaker_embedding(speaker_ids)
if self.num_speakers > 1:
# B x T_in x embed_dim + speaker_embed_dim
inputs = self._concat_speaker_embedding(inputs,
self.speaker_embeddings)
- # B x T_in x encoder_dim
+ # B x T_in x encoder_in_features
encoder_outputs = self.encoder(inputs)
+ # sequence masking
+ encoder_outputs = encoder_outputs * input_mask.unsqueeze(2).expand_as(encoder_outputs)
+ # global style token
if self.gst:
# B x gst_dim
encoder_outputs = self.compute_gst(encoder_outputs, mel_specs)
if self.num_speakers > 1:
encoder_outputs = self._concat_speaker_embedding(
encoder_outputs, self.speaker_embeddings)
- # decoder_outputs: B x decoder_dim x T_out
- # alignments: B x T_in x encoder_dim
+ # decoder_outputs: B x decoder_in_features x T_out
+ # alignments: B x T_in x encoder_in_features
# stop_tokens: B x T_in
decoder_outputs, alignments, stop_tokens = self.decoder(
- encoder_outputs, mel_specs, mask,
+ encoder_outputs, mel_specs, input_mask,
self.speaker_embeddings_projected)
- # B x T_out x decoder_dim
+ # sequence masking
+ if output_mask is not None:
+ decoder_outputs = decoder_outputs * output_mask.unsqueeze(1).expand_as(decoder_outputs)
+ # B x T_out x decoder_in_features
postnet_outputs = self.postnet(decoder_outputs)
+ # sequence masking
+ if output_mask is not None:
+ postnet_outputs = postnet_outputs * output_mask.unsqueeze(2).expand_as(postnet_outputs)
# B x T_out x posnet_dim
postnet_outputs = self.last_linear(postnet_outputs)
- # B x T_out x decoder_dim
+ # B x T_out x decoder_in_features
decoder_outputs = decoder_outputs.transpose(1, 2).contiguous()
if self.bidirectional_decoder:
- decoder_outputs_backward, alignments_backward = self._backward_inference(mel_specs, encoder_outputs, mask)
+ decoder_outputs_backward, alignments_backward = self._backward_pass(mel_specs, encoder_outputs, input_mask)
return decoder_outputs, postnet_outputs, alignments, stop_tokens, decoder_outputs_backward, alignments_backward
+ if self.double_decoder_consistency:
+ decoder_outputs_backward, alignments_backward = self._coarse_decoder_pass(mel_specs, encoder_outputs, alignments, input_mask)
+ return decoder_outputs, postnet_outputs, alignments, stop_tokens, decoder_outputs_backward, alignments_backward
return decoder_outputs, postnet_outputs, alignments, stop_tokens
@torch.no_grad()
def inference(self, characters, speaker_ids=None, style_mel=None):
inputs = self.embedding(characters)
self._init_states()
- self.compute_speaker_embedding(speaker_ids)
+ if speaker_ids is not None:
+ self.compute_speaker_embedding(speaker_ids)
if self.num_speakers > 1:
inputs = self._concat_speaker_embedding(inputs,
self.speaker_embeddings)
@@ -152,28 +158,3 @@ class Tacotron(nn.Module):
postnet_outputs = self.last_linear(postnet_outputs)
decoder_outputs = decoder_outputs.transpose(1, 2)
return decoder_outputs, postnet_outputs, alignments, stop_tokens
-
- def _backward_inference(self, mel_specs, encoder_outputs, mask):
- decoder_outputs_b, alignments_b, _ = self.decoder_backward(
- encoder_outputs, torch.flip(mel_specs, dims=(1,)), mask,
- self.speaker_embeddings_projected)
- decoder_outputs_b = decoder_outputs_b.transpose(1, 2).contiguous()
- return decoder_outputs_b, alignments_b
-
- def _compute_speaker_embedding(self, speaker_ids):
- speaker_embeddings = self.speaker_embedding(speaker_ids)
- return speaker_embeddings.unsqueeze_(1)
-
- @staticmethod
- def _add_speaker_embedding(outputs, speaker_embeddings):
- speaker_embeddings_ = speaker_embeddings.expand(
- outputs.size(0), outputs.size(1), -1)
- outputs = outputs + speaker_embeddings_
- return outputs
-
- @staticmethod
- def _concat_speaker_embedding(outputs, speaker_embeddings):
- speaker_embeddings_ = speaker_embeddings.expand(
- outputs.size(0), outputs.size(1), -1)
- outputs = torch.cat([outputs, speaker_embeddings_], dim=-1)
- return outputs
diff --git a/models/tacotron2.py b/models/tacotron2.py
index 3e7adfca..d9570f8d 100644
--- a/models/tacotron2.py
+++ b/models/tacotron2.py
@@ -1,13 +1,15 @@
-import copy
-import torch
from math import sqrt
+
+import torch
from torch import nn
-from TTS.layers.tacotron2 import Encoder, Decoder, Postnet
-from TTS.utils.generic_utils import sequence_mask
+
+from TTS.layers.gst_layers import GST
+from TTS.layers.tacotron2 import Decoder, Encoder, Postnet
+from TTS.models.tacotron_abstract import TacotronAbstract
# TODO: match function arguments with tacotron
-class Tacotron2(nn.Module):
+class Tacotron2(TacotronAbstract):
def __init__(self,
num_chars,
num_speakers,
@@ -25,37 +27,48 @@ class Tacotron2(nn.Module):
location_attn=True,
attn_K=5,
separate_stopnet=True,
- bidirectional_decoder=False):
- super(Tacotron2, self).__init__()
- self.postnet_output_dim = postnet_output_dim
- self.decoder_output_dim = decoder_output_dim
- self.r = r
- self.bidirectional_decoder = bidirectional_decoder
- decoder_dim = 512 if num_speakers > 1 else 512
- encoder_dim = 512 if num_speakers > 1 else 512
+ bidirectional_decoder=False,
+ double_decoder_consistency=False,
+ ddc_r=None,
+ gst=False):
+ super(Tacotron2,
+ self).__init__(num_chars, num_speakers, r, postnet_output_dim,
+ decoder_output_dim, attn_type, attn_win,
+ attn_norm, prenet_type, prenet_dropout,
+ forward_attn, trans_agent, forward_attn_mask,
+ location_attn, attn_K, separate_stopnet,
+ bidirectional_decoder, double_decoder_consistency,
+ ddc_r, gst)
+ decoder_in_features = 512 if num_speakers > 1 else 512
+ encoder_in_features = 512 if num_speakers > 1 else 512
proj_speaker_dim = 80 if num_speakers > 1 else 0
- # embedding layer
+ # base layers
self.embedding = nn.Embedding(num_chars, 512, padding_idx=0)
- std = sqrt(2.0 / (num_chars + 512))
- val = sqrt(3.0) * std # uniform bounds for std
- self.embedding.weight.data.uniform_(-val, val)
if num_speakers > 1:
self.speaker_embedding = nn.Embedding(num_speakers, 512)
self.speaker_embedding.weight.data.normal_(0, 0.3)
- self.speaker_embeddings = None
- self.speaker_embeddings_projected = None
- self.encoder = Encoder(encoder_dim)
- self.decoder = Decoder(decoder_dim, self.decoder_output_dim, r, attn_type, attn_win,
+ self.encoder = Encoder(encoder_in_features)
+ self.decoder = Decoder(decoder_in_features, self.decoder_output_dim, r, attn_type, attn_win,
attn_norm, prenet_type, prenet_dropout,
forward_attn, trans_agent, forward_attn_mask,
location_attn, attn_K, separate_stopnet, proj_speaker_dim)
- if self.bidirectional_decoder:
- self.decoder_backward = copy.deepcopy(self.decoder)
self.postnet = Postnet(self.postnet_output_dim)
-
- def _init_states(self):
- self.speaker_embeddings = None
- self.speaker_embeddings_projected = None
+ # global style token layers
+ if self.gst:
+ gst_embedding_dim = encoder_in_features
+ self.gst_layer = GST(num_mel=80,
+ num_heads=4,
+ num_style_tokens=10,
+ embedding_dim=gst_embedding_dim)
+ # backward pass decoder
+ if self.bidirectional_decoder:
+ self._init_backward_decoder()
+ # setup DDC
+ if self.double_decoder_consistency:
+ self.coarse_decoder = Decoder(decoder_in_features, self.decoder_output_dim, ddc_r, attn_type, attn_win,
+ attn_norm, prenet_type, prenet_dropout,
+ forward_attn, trans_agent, forward_attn_mask,
+ location_attn, attn_K, separate_stopnet, proj_speaker_dim)
@staticmethod
def shape_outputs(mel_outputs, mel_outputs_postnet, alignments):
@@ -63,38 +76,68 @@ class Tacotron2(nn.Module):
mel_outputs_postnet = mel_outputs_postnet.transpose(1, 2)
return mel_outputs, mel_outputs_postnet, alignments
- def forward(self, text, text_lengths, mel_specs=None, speaker_ids=None):
+ def forward(self, text, text_lengths, mel_specs=None, mel_lengths=None, speaker_ids=None):
self._init_states()
# compute mask for padding
- mask = sequence_mask(text_lengths).to(text.device)
+ # B x T_in_max (boolean)
+ input_mask, output_mask = self.compute_masks(text_lengths, mel_lengths)
+ # B x D_embed x T_in_max
embedded_inputs = self.embedding(text).transpose(1, 2)
+ # B x T_in_max x D_en
encoder_outputs = self.encoder(embedded_inputs, text_lengths)
- encoder_outputs = self._add_speaker_embedding(encoder_outputs,
- speaker_ids)
+ # adding speaker embeddding to encoder output
+ # TODO: multi-speaker
+ # B x speaker_embed_dim
+ if speaker_ids is not None:
+ self.compute_speaker_embedding(speaker_ids)
+ if self.num_speakers > 1:
+ # B x T_in x embed_dim + speaker_embed_dim
+ encoder_outputs = self._add_speaker_embedding(encoder_outputs,
+ self.speaker_embeddings)
+ encoder_outputs = encoder_outputs * input_mask.unsqueeze(2).expand_as(encoder_outputs)
+ # global style token
+ if self.gst:
+ # B x gst_dim
+ encoder_outputs = self.compute_gst(encoder_outputs, mel_specs)
+ # B x mel_dim x T_out -- B x T_out//r x T_in -- B x T_out//r
decoder_outputs, alignments, stop_tokens = self.decoder(
- encoder_outputs, mel_specs, mask)
+ encoder_outputs, mel_specs, input_mask)
+ # sequence masking
+ if mel_lengths is not None:
+ decoder_outputs = decoder_outputs * output_mask.unsqueeze(1).expand_as(decoder_outputs)
+ # B x mel_dim x T_out
postnet_outputs = self.postnet(decoder_outputs)
postnet_outputs = decoder_outputs + postnet_outputs
+ # sequence masking
+ if output_mask is not None:
+ postnet_outputs = postnet_outputs * output_mask.unsqueeze(1).expand_as(postnet_outputs)
+ # B x T_out x mel_dim -- B x T_out x mel_dim -- B x T_out//r x T_in
decoder_outputs, postnet_outputs, alignments = self.shape_outputs(
decoder_outputs, postnet_outputs, alignments)
if self.bidirectional_decoder:
- decoder_outputs_backward, alignments_backward = self._backward_inference(mel_specs, encoder_outputs, mask)
+ decoder_outputs_backward, alignments_backward = self._backward_pass(mel_specs, encoder_outputs, input_mask)
return decoder_outputs, postnet_outputs, alignments, stop_tokens, decoder_outputs_backward, alignments_backward
+ if self.double_decoder_consistency:
+ decoder_outputs_backward, alignments_backward = self._coarse_decoder_pass(mel_specs, encoder_outputs, alignments, input_mask)
+ return decoder_outputs, postnet_outputs, alignments, stop_tokens, decoder_outputs_backward, alignments_backward
return decoder_outputs, postnet_outputs, alignments, stop_tokens
@torch.no_grad()
def inference(self, text, speaker_ids=None):
embedded_inputs = self.embedding(text).transpose(1, 2)
encoder_outputs = self.encoder.inference(embedded_inputs)
- encoder_outputs = self._add_speaker_embedding(encoder_outputs,
- speaker_ids)
- mel_outputs, alignments, stop_tokens = self.decoder.inference(
+ if speaker_ids is not None:
+ self.compute_speaker_embedding(speaker_ids)
+ if self.num_speakers > 1:
+ encoder_outputs = self._add_speaker_embedding(encoder_outputs,
+ self.speaker_embeddings)
+ decoder_outputs, alignments, stop_tokens = self.decoder.inference(
encoder_outputs)
- mel_outputs_postnet = self.postnet(mel_outputs)
- mel_outputs_postnet = mel_outputs + mel_outputs_postnet
- mel_outputs, mel_outputs_postnet, alignments = self.shape_outputs(
- mel_outputs, mel_outputs_postnet, alignments)
- return mel_outputs, mel_outputs_postnet, alignments, stop_tokens
+ postnet_outputs = self.postnet(decoder_outputs)
+ postnet_outputs = decoder_outputs + postnet_outputs
+ decoder_outputs, postnet_outputs, alignments = self.shape_outputs(
+ decoder_outputs, postnet_outputs, alignments)
+ return decoder_outputs, postnet_outputs, alignments, stop_tokens
def inference_truncated(self, text, speaker_ids=None):
"""
@@ -112,22 +155,16 @@ class Tacotron2(nn.Module):
mel_outputs, mel_outputs_postnet, alignments)
return mel_outputs, mel_outputs_postnet, alignments, stop_tokens
- def _backward_inference(self, mel_specs, encoder_outputs, mask):
- decoder_outputs_b, alignments_b, _ = self.decoder_backward(
- encoder_outputs, torch.flip(mel_specs, dims=(1,)), mask,
- self.speaker_embeddings_projected)
- decoder_outputs_b = decoder_outputs_b.transpose(1, 2)
- return decoder_outputs_b, alignments_b
- def _add_speaker_embedding(self, encoder_outputs, speaker_ids):
- if hasattr(self, "speaker_embedding") and speaker_ids is None:
- raise RuntimeError(" [!] Model has speaker embedding layer but speaker_id is not provided")
- if hasattr(self, "speaker_embedding") and speaker_ids is not None:
- speaker_embeddings = self.speaker_embedding(speaker_ids)
+ def _speaker_embedding_pass(self, encoder_outputs, speaker_ids):
+ # TODO: multi-speaker
+ # if hasattr(self, "speaker_embedding") and speaker_ids is None:
+ # raise RuntimeError(" [!] Model has speaker embedding layer but speaker_id is not provided")
+ # if hasattr(self, "speaker_embedding") and speaker_ids is not None:
- speaker_embeddings.unsqueeze_(1)
- speaker_embeddings = speaker_embeddings.expand(encoder_outputs.size(0),
- encoder_outputs.size(1),
- -1)
- encoder_outputs = encoder_outputs + speaker_embeddings
- return encoder_outputs
+ # speaker_embeddings = speaker_embeddings.expand(encoder_outputs.size(0),
+ # encoder_outputs.size(1),
+ # -1)
+ # encoder_outputs = encoder_outputs + speaker_embeddings
+ # return encoder_outputs
+ pass
diff --git a/models/tacotron_abstract.py b/models/tacotron_abstract.py
new file mode 100644
index 00000000..75a1a5cd
--- /dev/null
+++ b/models/tacotron_abstract.py
@@ -0,0 +1,180 @@
+import copy
+from abc import ABC, abstractmethod
+
+import torch
+from torch import nn
+
+from TTS.utils.generic_utils import sequence_mask
+
+
+class TacotronAbstract(ABC, nn.Module):
+ def __init__(self,
+ num_chars,
+ num_speakers,
+ r,
+ postnet_output_dim=80,
+ decoder_output_dim=80,
+ attn_type='original',
+ attn_win=False,
+ attn_norm="softmax",
+ prenet_type="original",
+ prenet_dropout=True,
+ forward_attn=False,
+ trans_agent=False,
+ forward_attn_mask=False,
+ location_attn=True,
+ attn_K=5,
+ separate_stopnet=True,
+ bidirectional_decoder=False,
+ double_decoder_consistency=False,
+ ddc_r=None,
+ gst=False):
+ """ Abstract Tacotron class """
+ super().__init__()
+ self.num_chars = num_chars
+ self.r = r
+ self.decoder_output_dim = decoder_output_dim
+ self.postnet_output_dim = postnet_output_dim
+ self.gst = gst
+ self.num_speakers = num_speakers
+ self.bidirectional_decoder = bidirectional_decoder
+ self.double_decoder_consistency = double_decoder_consistency
+ self.ddc_r = ddc_r
+ self.attn_type = attn_type
+ self.attn_win = attn_win
+ self.attn_norm = attn_norm
+ self.prenet_type = prenet_type
+ self.prenet_dropout = prenet_dropout
+ self.forward_attn = forward_attn
+ self.trans_agent = trans_agent
+ self.forward_attn_mask = forward_attn_mask
+ self.location_attn = location_attn
+ self.attn_K = attn_K
+ self.separate_stopnet = separate_stopnet
+
+ # layers
+ self.embedding = None
+ self.encoder = None
+ self.decoder = None
+ self.postnet = None
+
+ # global style token
+ if self.gst:
+ self.gst_layer = None
+
+ # model states
+ self.speaker_embeddings = None
+ self.speaker_embeddings_projected = None
+
+ # additional layers
+ self.decoder_backward = None
+ self.coarse_decoder = None
+
+ #############################
+ # INIT FUNCTIONS
+ #############################
+
+ def _init_states(self):
+ self.speaker_embeddings = None
+ self.speaker_embeddings_projected = None
+
+ def _init_backward_decoder(self):
+ self.decoder_backward = copy.deepcopy(self.decoder)
+
+ def _init_coarse_decoder(self):
+ self.coarse_decoder = copy.deepcopy(self.decoder)
+ self.coarse_decoder.r_init = self.ddc_r
+ self.coarse_decoder.set_r(self.ddc_r)
+
+ #############################
+ # CORE FUNCTIONS
+ #############################
+
+ @abstractmethod
+ def forward(self):
+ pass
+
+ @abstractmethod
+ def inference(self):
+ pass
+
+ #############################
+ # COMMON COMPUTE FUNCTIONS
+ #############################
+
+ def compute_masks(self, text_lengths, mel_lengths):
+ """Compute masks against sequence paddings."""
+ # B x T_in_max (boolean)
+ device = text_lengths.device
+ input_mask = sequence_mask(text_lengths).to(device)
+ output_mask = None
+ if mel_lengths is not None:
+ max_len = mel_lengths.max()
+ r = self.decoder.r
+ max_len = max_len + (r - (max_len % r)) if max_len % r > 0 else max_len
+ output_mask = sequence_mask(mel_lengths, max_len=max_len).to(device)
+ return input_mask, output_mask
+
+ def _backward_pass(self, mel_specs, encoder_outputs, mask):
+ """ Run backwards decoder """
+ decoder_outputs_b, alignments_b, _ = self.decoder_backward(
+ encoder_outputs, torch.flip(mel_specs, dims=(1,)), mask,
+ self.speaker_embeddings_projected)
+ decoder_outputs_b = decoder_outputs_b.transpose(1, 2).contiguous()
+ return decoder_outputs_b, alignments_b
+
+ def _coarse_decoder_pass(self, mel_specs, encoder_outputs, alignments,
+ input_mask):
+ """ Double Decoder Consistency """
+ T = mel_specs.shape[1]
+ if T % self.coarse_decoder.r > 0:
+ padding_size = self.coarse_decoder.r - (T % self.coarse_decoder.r)
+ mel_specs = torch.nn.functional.pad(mel_specs,
+ (0, 0, 0, padding_size, 0, 0))
+ decoder_outputs_backward, alignments_backward, _ = self.coarse_decoder(
+ encoder_outputs.detach(), mel_specs, input_mask)
+ # scale_factor = self.decoder.r_init / self.decoder.r
+ alignments_backward = torch.nn.functional.interpolate(
+ alignments_backward.transpose(1, 2),
+ size=alignments.shape[1],
+ mode='nearest').transpose(1, 2)
+ decoder_outputs_backward = decoder_outputs_backward.transpose(1, 2)
+ decoder_outputs_backward = decoder_outputs_backward[:, :T, :]
+ return decoder_outputs_backward, alignments_backward
+
+ #############################
+ # EMBEDDING FUNCTIONS
+ #############################
+
+ def compute_speaker_embedding(self, speaker_ids):
+ """ Compute speaker embedding vectors """
+ if hasattr(self, "speaker_embedding") and speaker_ids is None:
+ raise RuntimeError(
+ " [!] Model has speaker embedding layer but speaker_id is not provided"
+ )
+ if hasattr(self, "speaker_embedding") and speaker_ids is not None:
+ self.speaker_embeddings = self.speaker_embedding(speaker_ids).unsqueeze(1)
+ if hasattr(self, "speaker_project_mel") and speaker_ids is not None:
+ self.speaker_embeddings_projected = self.speaker_project_mel(
+ self.speaker_embeddings).squeeze(1)
+
+ def compute_gst(self, inputs, mel_specs):
+ """ Compute global style token """
+ # pylint: disable=not-callable
+ gst_outputs = self.gst_layer(mel_specs)
+ inputs = self._add_speaker_embedding(inputs, gst_outputs)
+ return inputs
+
+ @staticmethod
+ def _add_speaker_embedding(outputs, speaker_embeddings):
+ speaker_embeddings_ = speaker_embeddings.expand(
+ outputs.size(0), outputs.size(1), -1)
+ outputs = outputs + speaker_embeddings_
+ return outputs
+
+ @staticmethod
+ def _concat_speaker_embedding(outputs, speaker_embeddings):
+ speaker_embeddings_ = speaker_embeddings.expand(
+ outputs.size(0), outputs.size(1), -1)
+ outputs = torch.cat([outputs, speaker_embeddings_], dim=-1)
+ return outputs
diff --git a/requirements.txt b/requirements.txt
index 5f31db70..862cb229 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
numpy>=1.16.0
-torch>=0.4.1
+torch>=1.5
librosa>=0.5.1
Unidecode>=0.4.20
tensorboard
diff --git a/requirements_tests.txt b/requirements_tests.txt
index 1e0615b2..36243398 100644
--- a/requirements_tests.txt
+++ b/requirements_tests.txt
@@ -1,4 +1,5 @@
numpy>=1.16.0
+numba==0.48
torch>=0.4.1
tensorflow>=2.2
librosa>=0.5.1
@@ -13,3 +14,4 @@ tqdm
soundfile
phonemizer
bokeh==1.4.0
+nose
diff --git a/server/server.py b/server/server.py
index 705937e2..43f1b3c4 100644
--- a/server/server.py
+++ b/server/server.py
@@ -21,6 +21,8 @@ def create_argparser():
parser.add_argument('--pwgan_lib_path', type=str, default=None, help='path to ParallelWaveGAN project folder to be imported. If this is not passed, model uses Griffin-Lim for synthesis.')
parser.add_argument('--pwgan_file', type=str, default=None, help='path to ParallelWaveGAN checkpoint file.')
parser.add_argument('--pwgan_config', type=str, default=None, help='path to ParallelWaveGAN config file.')
+ parser.add_argument('--vocoder_config', type=str, default=None, help='path to TTS.vocoder config file.')
+ parser.add_argument('--vocoder_checkpoint', type=str, default=None, help='path to TTS.vocoder checkpoint file.')
parser.add_argument('--port', type=int, default=5002, help='port to listen on.')
parser.add_argument('--use_cuda', type=convert_boolean, default=False, help='true to use CUDA.')
parser.add_argument('--debug', type=convert_boolean, default=False, help='true to enable Flask debug mode.')
@@ -35,6 +37,11 @@ embedded_tts_folder = os.path.join(embedded_models_folder, 'tts')
tts_checkpoint_file = os.path.join(embedded_tts_folder, 'checkpoint.pth.tar')
tts_config_file = os.path.join(embedded_tts_folder, 'config.json')
+embedded_vocoder_folder = os.path.join(embedded_models_folder, 'vocoder')
+vocoder_checkpoint_file = os.path.join(embedded_vocoder_folder, 'checkpoint.pth.tar')
+vocoder_config_file = os.path.join(embedded_vocoder_folder, 'config.json')
+
+# These models are soon to be deprecated
embedded_wavernn_folder = os.path.join(embedded_models_folder, 'wavernn')
wavernn_checkpoint_file = os.path.join(embedded_wavernn_folder, 'checkpoint.pth.tar')
wavernn_config_file = os.path.join(embedded_wavernn_folder, 'config.json')
@@ -50,6 +57,11 @@ if not args.tts_checkpoint and os.path.isfile(tts_checkpoint_file):
args.tts_checkpoint = tts_checkpoint_file
if not args.tts_config and os.path.isfile(tts_config_file):
args.tts_config = tts_config_file
+if not args.vocoder_checkpoint and os.path.isfile(tts_checkpoint_file):
+ args.tts_checkpoint = tts_checkpoint_file
+if not args.vocoder_config and os.path.isfile(tts_config_file):
+ args.tts_config = tts_config_file
+
if not args.wavernn_file and os.path.isfile(wavernn_checkpoint_file):
args.wavernn_file = wavernn_checkpoint_file
if not args.wavernn_config and os.path.isfile(wavernn_config_file):
@@ -76,5 +88,9 @@ def tts():
return send_file(data, mimetype='audio/wav')
-if __name__ == '__main__':
+def main():
app.run(debug=args.debug, host='0.0.0.0', port=args.port)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/server/synthesizer.py b/server/synthesizer.py
index 765aab8a..d85bbebc 100644
--- a/server/synthesizer.py
+++ b/server/synthesizer.py
@@ -1,6 +1,7 @@
import io
import re
import sys
+import time
import numpy as np
import torch
@@ -10,6 +11,7 @@ from TTS.utils.audio import AudioProcessor
from TTS.utils.io import load_config
from TTS.utils.generic_utils import setup_model
from TTS.utils.speakers import load_speaker_mapping
+from TTS.vocoder.utils.generic_utils import setup_generator
# pylint: disable=unused-wildcard-import
# pylint: disable=wildcard-import
from TTS.utils.synthesis import *
@@ -34,10 +36,12 @@ class Synthesizer(object):
assert torch.cuda.is_available(), "CUDA is not availabe on this machine."
self.load_tts(self.config.tts_checkpoint, self.config.tts_config,
self.config.use_cuda)
+ if self.config.vocoder_checkpoint:
+ self.load_vocoder(self.config.vocoder_checkpoint, self.config.vocoder_config, self.config.use_cuda)
if self.config.wavernn_lib_path:
self.load_wavernn(self.config.wavernn_lib_path, self.config.wavernn_file,
self.config.wavernn_config, self.config.use_cuda)
- if self.config.pwgan_lib_path:
+ if self.config.pwgan_file:
self.load_pwgan(self.config.pwgan_lib_path, self.config.pwgan_file,
self.config.pwgan_config, self.config.use_cuda)
@@ -77,6 +81,19 @@ class Synthesizer(object):
self.tts_model.decoder.max_decoder_steps = 3000
if 'r' in cp:
self.tts_model.decoder.set_r(cp['r'])
+ print(f" > model reduction factor: {cp['r']}")
+
+ def load_vocoder(self, model_file, model_config, use_cuda):
+ self.vocoder_config = load_config(model_config)
+ self.vocoder_model = setup_generator(self.vocoder_config)
+ self.vocoder_model.load_state_dict(torch.load(model_file, map_location="cpu")["model"])
+ self.vocoder_model.remove_weight_norm()
+ self.vocoder_model.inference_padding = 0
+ self.vocoder_config = load_config(model_config)
+
+ if use_cuda:
+ self.vocoder_model.cuda()
+ self.vocoder_model.eval()
def load_wavernn(self, lib_path, model_file, model_config, use_cuda):
# TODO: set a function in wavernn code base for model setup and call it here.
@@ -113,9 +130,14 @@ class Synthesizer(object):
self.wavernn.eval()
def load_pwgan(self, lib_path, model_file, model_config, use_cuda):
- sys.path.append(lib_path) # set this if ParallelWaveGAN is not installed globally
- #pylint: disable=import-outside-toplevel
- from parallel_wavegan.models import ParallelWaveGANGenerator
+ if lib_path:
+ # set this if ParallelWaveGAN is not installed globally
+ sys.path.append(lib_path)
+ try:
+ #pylint: disable=import-outside-toplevel
+ from parallel_wavegan.models import ParallelWaveGANGenerator
+ except ImportError as e:
+ raise RuntimeError(f"cannot import parallel-wavegan, either install it or set its directory using the --pwgan_lib_path command line argument: {e}")
print(" > Loading PWGAN model ...")
print(" | > model config: ", model_config)
print(" | > model file: ", model_file)
@@ -166,6 +188,7 @@ class Synthesizer(object):
return sentences
def tts(self, text, speaker_id=None):
+ start_time = time.time()
wavs = []
sens = self.split_into_sentences(text)
print(sens)
@@ -179,24 +202,25 @@ class Synthesizer(object):
inputs = numpy_to_torch(inputs, torch.long, cuda=self.use_cuda)
inputs = inputs.unsqueeze(0)
# synthesize voice
- decoder_output, postnet_output, alignments, stop_tokens = run_model_torch(
- self.tts_model, inputs, self.tts_config, False, speaker_id, None)
+ decoder_output, postnet_output, alignments, stop_tokens = run_model_torch(self.tts_model, inputs, self.tts_config, False, speaker_id, None)
# convert outputs to numpy
- postnet_output, decoder_output, _, _ = parse_outputs_torch(
- postnet_output, decoder_output, alignments, stop_tokens)
-
- if self.pwgan:
- vocoder_input = torch.FloatTensor(postnet_output.T).unsqueeze(0)
+ if self.vocoder_model:
+ vocoder_input = postnet_output[0].transpose(0, 1).unsqueeze(0)
+ wav = self.vocoder_model.inference(vocoder_input)
if self.use_cuda:
- vocoder_input.cuda()
- wav = self.pwgan.inference(vocoder_input, hop_size=self.ap.hop_length)
+ wav = wav.cpu().numpy()
+ else:
+ wav = wav.numpy()
+ wav = wav.flatten()
elif self.wavernn:
- vocoder_input = torch.FloatTensor(postnet_output.T).unsqueeze(0)
+ vocoder_input = None
+ if self.tts_config.model == "Tacotron":
+ vocoder_input = torch.FloatTensor(self.ap.out_linear_to_mel(linear_spec=postnet_output.T).T).T.unsqueeze(0)
+ else:
+ vocoder_input = postnet_output[0].transpose(0, 1).unsqueeze(0)
if self.use_cuda:
vocoder_input.cuda()
wav = self.wavernn.generate(vocoder_input, batched=self.config.is_wavernn_batched, target=11000, overlap=550)
- else:
- wav = inv_spectrogram(postnet_output, self.ap, self.tts_config)
# trim silence
wav = trim_silence(wav, self.ap)
@@ -205,4 +229,10 @@ class Synthesizer(object):
out = io.BytesIO()
self.save_wav(wavs, out)
+
+ # compute stats
+ process_time = time.time() - start_time
+ audio_time = len(wavs) / self.tts_config.audio['sample_rate']
+ print(f" > Processing time: {process_time}")
+ print(f" > Real-time factor: {process_time / audio_time}")
return out
diff --git a/server/templates/index.html b/server/templates/index.html
index d1bde024..45b874a9 100644
--- a/server/templates/index.html
+++ b/server/templates/index.html
@@ -8,10 +8,10 @@
- Mozillia - Text2Speech engine
+ Mozilla - Text2Speech engine
-
@@ -27,7 +27,7 @@
-
+
@@ -60,7 +60,7 @@
Mozilla TTS
-
+
diff --git a/setup.py b/setup.py
index f92dac8a..96032de0 100644
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,7 @@ args, unknown_args = parser.parse_known_args()
# Remove our arguments from argv so that setuptools doesn't see them
sys.argv = [sys.argv[0]] + unknown_args
-version = '0.0.1'
+version = '0.0.3'
# Adapted from https://github.com/pytorch/pytorch
cwd = os.path.dirname(os.path.abspath(__file__))
@@ -75,6 +75,11 @@ setup(
url='https://github.com/mozilla/TTS',
description='Text to Speech with Deep Learning',
license='MPL-2.0',
+ entry_points={
+ 'console_scripts': [
+ 'tts-server = TTS.server.server:main'
+ ]
+ },
package_dir={'': 'tts_namespace'},
packages=find_packages('tts_namespace'),
package_data={
@@ -92,8 +97,8 @@ setup(
},
install_requires=[
"scipy>=0.19.0",
- "torch>=0.4.1",
- "numpy==1.15.4",
+ "torch>=1.5",
+ "numpy>=1.16.0",
"librosa==0.6.2",
"unidecode==0.4.20",
"attrdict",
diff --git a/speaker_encoder/README.md b/speaker_encoder/README.md
index 38b4bb1b..b6f541f8 100644
--- a/speaker_encoder/README.md
+++ b/speaker_encoder/README.md
@@ -1,16 +1,16 @@
-### Speaker embedding (Experimental)
+### Speaker Encoder
This is an implementation of https://arxiv.org/abs/1710.10467. This model can be used for voice and speaker embedding.
With the code here you can generate d-vectors for both multi-speaker and single-speaker TTS datasets, then visualise and explore them along with the associated audio files in an interactive chart.
-Below is an example showing embedding results of various speakers. You can generate the same plot with the provided notebook as demonstrated in [this video](https://youtu.be/KW3oO7JVa7Q).
+Below is an example showing embedding results of various speakers. You can generate the same plot with the provided notebook as demonstrated in [this video](https://youtu.be/KW3oO7JVa7Q).

Download a pretrained model from [Released Models](https://github.com/mozilla/TTS/wiki/Released-Models) page.
-To run the code, you need to follow the same flow as in TTS.
+To run the code, you need to follow the same flow as in TTS.
- Define 'config.json' for your needs. Note that, audio parameters should match your TTS model.
- Example training call ```python speaker_encoder/train.py --config_path speaker_encoder/config.json --data_path ~/Data/Libri-TTS/train-clean-360```
diff --git a/speaker_encoder/requirements.txt b/speaker_encoder/requirements.txt
new file mode 100644
index 00000000..a486cc45
--- /dev/null
+++ b/speaker_encoder/requirements.txt
@@ -0,0 +1,2 @@
+umap-learn
+numpy>=1.17.0
diff --git a/speaker_encoder/train.py b/speaker_encoder/train.py
index 0a137360..f74d0880 100644
--- a/speaker_encoder/train.py
+++ b/speaker_encoder/train.py
@@ -13,12 +13,11 @@ from TTS.speaker_encoder.model import SpeakerEncoder
from TTS.speaker_encoder.visual import plot_embeddings
from TTS.speaker_encoder.generic_utils import save_best_model
from TTS.utils.audio import AudioProcessor
-from TTS.utils.generic_utils import (NoamLR, check_update, copy_config_file,
- count_parameters,
- create_experiment_folder, get_git_branch,
- load_config,
+from TTS.utils.generic_utils import (create_experiment_folder, get_git_branch,
remove_experiment_folder, set_init_dict)
-from TTS.utils.logger import Logger
+from TTS.utils.io import load_config, copy_config_file
+from TTS.utils.training import check_update, NoamLR
+from TTS.utils.tensorboard_logger import TensorboardLogger
from TTS.utils.radam import RAdam
torch.backends.cudnn.enabled = True
@@ -237,7 +236,7 @@ if __name__ == '__main__':
new_fields)
LOG_DIR = OUT_PATH
- tb_logger = Logger(LOG_DIR)
+ tb_logger = TensorboardLogger(LOG_DIR)
try:
main(args)
diff --git a/synthesize.py b/synthesize.py
index 1a760268..18048c2f 100644
--- a/synthesize.py
+++ b/synthesize.py
@@ -7,7 +7,8 @@ import json
import string
from TTS.utils.synthesis import synthesis
-from TTS.utils.generic_utils import load_config, setup_model
+from TTS.utils.generic_utils import setup_model
+from TTS.utils.io import load_config
from TTS.utils.text.symbols import make_symbols, symbols, phonemes
from TTS.utils.audio import AudioProcessor
diff --git a/tests/data/ljspeech/wavs/LJ001-0001.wav b/tests/data/ljspeech/wavs/LJ001-0001.wav
index a4662ab0..a274be89 100644
Binary files a/tests/data/ljspeech/wavs/LJ001-0001.wav and b/tests/data/ljspeech/wavs/LJ001-0001.wav differ
diff --git a/tests/outputs/dummy_model_config.json b/tests/outputs/dummy_model_config.json
index 2f56c6ce..d301b61a 100644
--- a/tests/outputs/dummy_model_config.json
+++ b/tests/outputs/dummy_model_config.json
@@ -4,7 +4,7 @@
"audio":{
// Audio processing parameters
- "num_mels": 80, // size of the mel spec frame.
+ "num_mels": 80, // size of the mel spec frame.
"num_freq": 1025, // number of stft frequency levels. Size of the linear spectogram frame.
"sample_rate": 22050, // DATASET-RELATED: wav sample-rate. If different than the original data, it is resampled.
"frame_length_ms": 50, // stft window length in ms.
@@ -31,19 +31,19 @@
"reinit_layers": [],
- "model": "Tacotron2", // one of the model in models/
+ "model": "Tacotron2", // one of the model in models/
"grad_clip": 1, // 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"
"windowing": false, // Enables attention windowing. Used only in eval mode.
- "memory_size": 5, // ONLY TACOTRON - memory queue size used to queue network predictions to feed autoregressive connection. Useful if r < 5.
+ "memory_size": 5, // ONLY TACOTRON - memory queue size used to queue network predictions to feed autoregressive connection. Useful if r < 5.
"attention_norm": "sigmoid", // softmax or sigmoid. Suggested to use softmax for Tacotron2 and sigmoid for Tacotron.
"prenet_type": "original", // ONLY TACOTRON2 - "original" or "bn".
- "prenet_dropout": true, // ONLY TACOTRON2 - enable/disable dropout at prenet.
+ "prenet_dropout": true, // ONLY TACOTRON2 - enable/disable dropout at prenet.
"use_forward_attn": true, // ONLY TACOTRON2 - if it uses forward attention. In general, it aligns faster.
- "forward_attn_mask": false,
+ "forward_attn_mask": false,
"attention_type": "original",
"attention_heads": 5,
"bidirectional_decoder": false,
@@ -51,13 +51,15 @@
"location_attn": false, // ONLY TACOTRON2 - enable_disable location sensitive attention. It is enabled for TACOTRON by default.
"loss_masking": true, // enable / disable loss masking against the sequence padding.
"enable_eos_bos_chars": false, // enable/disable beginning of sentence and end of sentence chars.
- "stopnet": true, // Train stopnet predicting the end of synthesis.
+ "stopnet": true, // Train stopnet predicting the end of synthesis.
"separate_stopnet": true, // Train stopnet seperately if 'stopnet==true'. It prevents stopnet loss to influence the rest of the model. It causes a better model, but it trains SLOWER.
"tb_model_param_stats": false, // true, plots param stats per layer on tensorboard. Might be memory consuming, but good for debugging.
- "use_gst": false,
-
+ "use_gst": false,
+ "double_decoder_consistency": true, // use DDC explained here https://erogol.com/solving-attention-problems-of-tts-models-with-double-decoder-consistency-draft/
+ "ddc_r": 7, // reduction rate for coarse decoder.
+
"batch_size": 32, // Batch size for training. Lower values than 32 might cause hard to learn attention.
- "eval_batch_size":16,
+ "eval_batch_size":16,
"r": 1, // Number of frames to predict for step.
"wd": 0.000001, // Weight decay weight.
"checkpoint": true, // If true, it saves checkpoints per "save_step"
diff --git a/tests/test_tacotron2_model.py b/tests/test_tacotron2_model.py
index eb91b3cc..cf1d0778 100644
--- a/tests/test_tacotron2_model.py
+++ b/tests/test_tacotron2_model.py
@@ -51,7 +51,7 @@ class TacotronTrainTest(unittest.TestCase):
optimizer = optim.Adam(model.parameters(), lr=c.lr)
for i in range(5):
mel_out, mel_postnet_out, align, stop_tokens = model.forward(
- input, input_lengths, mel_spec, speaker_ids)
+ input, input_lengths, mel_spec, mel_lengths, speaker_ids)
assert torch.sigmoid(stop_tokens).data.max() <= 1.0
assert torch.sigmoid(stop_tokens).data.min() >= 0.0
optimizer.zero_grad()
diff --git a/tests/test_tacotron_model.py b/tests/test_tacotron_model.py
index 7053a580..2bbb3c8d 100644
--- a/tests/test_tacotron_model.py
+++ b/tests/test_tacotron_model.py
@@ -66,7 +66,7 @@ class TacotronTrainTest(unittest.TestCase):
optimizer = optim.Adam(model.parameters(), lr=c.lr)
for _ in range(5):
mel_out, linear_out, align, stop_tokens = model.forward(
- input_dummy, input_lengths, mel_spec, speaker_ids)
+ input_dummy, input_lengths, mel_spec, mel_lengths, speaker_ids)
optimizer.zero_grad()
loss = criterion(mel_out, mel_spec, mel_lengths)
stop_loss = criterion_st(stop_tokens, stop_targets)
@@ -95,6 +95,7 @@ class TacotronGSTTrainTest(unittest.TestCase):
mel_spec = torch.rand(8, 120, c.audio['num_mels']).to(device)
linear_spec = torch.rand(8, 120, c.audio['num_freq']).to(device)
mel_lengths = torch.randint(20, 120, (8, )).long().to(device)
+ mel_lengths[-1] = 120
stop_targets = torch.zeros(8, 120, 1).float().to(device)
speaker_ids = torch.randint(0, 5, (8, )).long().to(device)
@@ -130,7 +131,7 @@ class TacotronGSTTrainTest(unittest.TestCase):
optimizer = optim.Adam(model.parameters(), lr=c.lr)
for _ in range(10):
mel_out, linear_out, align, stop_tokens = model.forward(
- input_dummy, input_lengths, mel_spec, speaker_ids)
+ input_dummy, input_lengths, mel_spec, mel_lengths, speaker_ids)
optimizer.zero_grad()
loss = criterion(mel_out, mel_spec, mel_lengths)
stop_loss = criterion_st(stop_tokens, stop_targets)
diff --git a/train.py b/train.py
index e4963ee7..189a6baa 100644
--- a/train.py
+++ b/train.py
@@ -20,7 +20,8 @@ from TTS.utils.generic_utils import (count_parameters, create_experiment_folder,
from TTS.utils.io import (save_best_model, save_checkpoint,
load_config, copy_config_file)
from TTS.utils.training import (NoamLR, check_update, adam_weight_decay,
- gradual_training_scheduler, set_weight_decay)
+ gradual_training_scheduler, set_weight_decay,
+ setup_torch_training_env)
from TTS.utils.tensorboard_logger import TensorboardLogger
from TTS.utils.console_logger import ConsoleLogger
from TTS.utils.speakers import load_speaker_mapping, save_speaker_mapping, \
@@ -32,13 +33,8 @@ from TTS.datasets.preprocess import load_meta_data
from TTS.utils.radam import RAdam
from TTS.utils.measures import alignment_diagonal_score
-torch.backends.cudnn.enabled = True
-torch.backends.cudnn.benchmark = False
-torch.manual_seed(54321)
-use_cuda = torch.cuda.is_available()
-num_gpus = torch.cuda.device_count()
-print(" > Using CUDA: ", use_cuda)
-print(" > Number of GPUs: ", num_gpus)
+
+use_cuda, num_gpus = setup_torch_training_env(True, False)
def setup_loader(ap, r, is_val=False, verbose=False):
@@ -123,21 +119,7 @@ def train(model, criterion, optimizer, optimizer_st, scheduler,
verbose=(epoch == 0))
model.train()
epoch_time = 0
- train_values = {
- 'avg_postnet_loss': 0,
- 'avg_decoder_loss': 0,
- 'avg_stopnet_loss': 0,
- 'avg_align_error': 0,
- 'avg_step_time': 0,
- 'avg_loader_time': 0
- }
- if c.bidirectional_decoder:
- train_values['avg_decoder_b_loss'] = 0 # decoder backward loss
- train_values['avg_decoder_c_loss'] = 0 # decoder consistency loss
- if c.ga_alpha > 0:
- train_values['avg_ga_loss'] = 0 # guidede attention loss
keep_avg = KeepAverage()
- keep_avg.add_values(train_values)
if use_cuda:
batch_n_iter = int(
len(data_loader.dataset) / (c.batch_size * num_gpus))
@@ -162,13 +144,14 @@ def train(model, criterion, optimizer, optimizer_st, scheduler,
optimizer_st.zero_grad()
# forward pass model
- if c.bidirectional_decoder:
+ if c.bidirectional_decoder or c.double_decoder_consistency:
decoder_output, postnet_output, alignments, stop_tokens, decoder_backward_output, alignments_backward = model(
- text_input, text_lengths, mel_input, speaker_ids=speaker_ids)
+ text_input, text_lengths, mel_input, mel_lengths, speaker_ids=speaker_ids)
else:
decoder_output, postnet_output, alignments, stop_tokens = model(
- text_input, text_lengths, mel_input, speaker_ids=speaker_ids)
+ text_input, text_lengths, mel_input, mel_lengths, speaker_ids=speaker_ids)
decoder_backward_output = None
+ alignments_backward = None
# set the alignment lengths wrt reduction factor for guided attention
if mel_lengths.max() % model.decoder.r != 0:
@@ -180,12 +163,8 @@ def train(model, criterion, optimizer, optimizer_st, scheduler,
loss_dict = criterion(postnet_output, decoder_output, mel_input,
linear_input, stop_tokens, stop_targets,
mel_lengths, decoder_backward_output,
- alignments, alignment_lengths, text_lengths)
- if c.bidirectional_decoder:
- keep_avg.update_values({'avg_decoder_b_loss': loss_dict['decoder_backward_loss'].item(),
- 'avg_decoder_c_loss': loss_dict['decoder_c_loss'].item()})
- if c.ga_alpha > 0:
- keep_avg.update_values({'avg_ga_loss': loss_dict['ga_loss'].item()})
+ alignments, alignment_lengths, alignments_backward,
+ text_lengths)
# backward pass
loss_dict['loss'].backward()
@@ -195,7 +174,6 @@ def train(model, criterion, optimizer, optimizer_st, scheduler,
# compute alignment error (the lower the better )
align_error = 1 - alignment_diagonal_score(alignments)
- keep_avg.update_value('avg_align_error', align_error)
loss_dict['align_error'] = align_error
# backpass and check the grad norm for stop loss
@@ -210,23 +188,6 @@ def train(model, criterion, optimizer, optimizer_st, scheduler,
step_time = time.time() - start_time
epoch_time += step_time
- # update avg stats
- update_train_values = {
- 'avg_postnet_loss': float(loss_dict['postnet_loss'].item()),
- 'avg_decoder_loss': float(loss_dict['decoder_loss'].item()),
- 'avg_stopnet_loss': loss_dict['stopnet_loss'].item() \
- if isinstance(loss_dict['stopnet_loss'], float) else float(loss_dict['stopnet_loss'].item()),
- 'avg_step_time': step_time,
- 'avg_loader_time': loader_time
- }
- keep_avg.update_values(update_train_values)
-
- if global_step % c.print_step == 0:
- c_logger.print_train_step(batch_n_iter, num_iter, global_step,
- avg_spec_length, avg_text_length,
- step_time, loader_time, current_lr,
- loss_dict, keep_avg.avg_values)
-
# aggregate losses from processes
if num_gpus > 1:
loss_dict['postnet_loss'] = reduce_tensor(loss_dict['postnet_loss'].data, num_gpus)
@@ -234,18 +195,41 @@ def train(model, criterion, optimizer, optimizer_st, scheduler,
loss_dict['loss'] = reduce_tensor(loss_dict['loss'] .data, num_gpus)
loss_dict['stopnet_loss'] = reduce_tensor(loss_dict['stopnet_loss'].data, num_gpus) if c.stopnet else loss_dict['stopnet_loss']
+ # detach loss values
+ loss_dict_new = dict()
+ for key, value in loss_dict.items():
+ if isinstance(value, (int, float)):
+ loss_dict_new[key] = value
+ else:
+ loss_dict_new[key] = value.item()
+ loss_dict = loss_dict_new
+
+ # update avg stats
+ update_train_values = dict()
+ for key, value in loss_dict.items():
+ update_train_values['avg_' + key] = value
+ update_train_values['avg_loader_time'] = loader_time
+ update_train_values['avg_step_time'] = step_time
+ keep_avg.update_values(update_train_values)
+
+ # print training progress
+ if global_step % c.print_step == 0:
+ c_logger.print_train_step(batch_n_iter, num_iter, global_step,
+ avg_spec_length, avg_text_length,
+ step_time, loader_time, current_lr,
+ loss_dict, keep_avg.avg_values)
+
if args.rank == 0:
# Plot Training Iter Stats
# reduce TB load
- if global_step % 10 == 0:
+ if global_step % c.tb_plot_step == 0:
iter_stats = {
- "loss_posnet": loss_dict['postnet_loss'].item(),
- "loss_decoder": loss_dict['decoder_loss'].item(),
"lr": current_lr,
"grad_norm": grad_norm,
"grad_norm_st": grad_norm_st,
"step_time": step_time
}
+ iter_stats.update(loss_dict)
tb_logger.tb_train_iter_stats(global_step, iter_stats)
if global_step % c.save_step == 0:
@@ -253,7 +237,7 @@ def train(model, criterion, optimizer, optimizer_st, scheduler,
# save model
save_checkpoint(model, optimizer, global_step, epoch, model.decoder.r, OUT_PATH,
optimizer_st=optimizer_st,
- model_loss=loss_dict['postnet_loss'].item())
+ model_loss=loss_dict['postnet_loss'])
# Diagnostic visualizations
const_spec = postnet_output[0].data.cpu().numpy()
@@ -268,7 +252,7 @@ def train(model, criterion, optimizer, optimizer_st, scheduler,
"alignment": plot_alignment(align_img),
}
- if c.bidirectional_decoder:
+ if c.bidirectional_decoder or c.double_decoder_consistency:
figures["alignment_backward"] = plot_alignment(alignments_backward[0].data.cpu().numpy())
tb_logger.tb_train_figures(global_step, figures)
@@ -288,16 +272,8 @@ def train(model, criterion, optimizer, optimizer_st, scheduler,
# Plot Epoch Stats
if args.rank == 0:
- # Plot Training Epoch Stats
- epoch_stats = {
- "loss_postnet": keep_avg['avg_postnet_loss'],
- "loss_decoder": keep_avg['avg_decoder_loss'],
- "stopnet_loss": keep_avg['avg_stopnet_loss'],
- "alignment_score": keep_avg['avg_align_error'],
- "epoch_time": epoch_time
- }
- if c.ga_alpha > 0:
- epoch_stats['guided_attention_loss'] = keep_avg['avg_ga_loss']
+ epoch_stats = {"epoch_time": epoch_time}
+ epoch_stats.update(keep_avg.avg_values)
tb_logger.tb_train_epoch_stats(global_step, epoch_stats)
if c.tb_model_param_stats:
tb_logger.tb_model_weights(model, global_step)
@@ -309,20 +285,7 @@ def evaluate(model, criterion, ap, global_step, epoch):
data_loader = setup_loader(ap, model.decoder.r, is_val=True)
model.eval()
epoch_time = 0
- eval_values_dict = {
- 'avg_postnet_loss': 0,
- 'avg_decoder_loss': 0,
- 'avg_stopnet_loss': 0,
- 'avg_align_error': 0
- }
- if c.bidirectional_decoder:
- eval_values_dict['avg_decoder_b_loss'] = 0 # decoder backward loss
- eval_values_dict['avg_decoder_c_loss'] = 0 # decoder consistency loss
- if c.ga_alpha > 0:
- eval_values_dict['avg_ga_loss'] = 0 # guidede attention loss
keep_avg = KeepAverage()
- keep_avg.add_values(eval_values_dict)
-
c_logger.print_eval_start()
if data_loader is not None:
for num_iter, data in enumerate(data_loader):
@@ -333,13 +296,14 @@ def evaluate(model, criterion, ap, global_step, epoch):
assert mel_input.shape[1] % model.decoder.r == 0
# forward pass model
- if c.bidirectional_decoder:
+ if c.bidirectional_decoder or c.double_decoder_consistency:
decoder_output, postnet_output, alignments, stop_tokens, decoder_backward_output, alignments_backward = model(
text_input, text_lengths, mel_input, speaker_ids=speaker_ids)
else:
decoder_output, postnet_output, alignments, stop_tokens = model(
text_input, text_lengths, mel_input, speaker_ids=speaker_ids)
decoder_backward_output = None
+ alignments_backward = None
# set the alignment lengths wrt reduction factor for guided attention
if mel_lengths.max() % model.decoder.r != 0:
@@ -351,12 +315,8 @@ def evaluate(model, criterion, ap, global_step, epoch):
loss_dict = criterion(postnet_output, decoder_output, mel_input,
linear_input, stop_tokens, stop_targets,
mel_lengths, decoder_backward_output,
- alignments, alignment_lengths, text_lengths)
- if c.bidirectional_decoder:
- keep_avg.update_values({'avg_decoder_b_loss': loss_dict['decoder_b_loss'].item(),
- 'avg_decoder_c_loss': loss_dict['decoder_c_loss'].item()})
- if c.ga_alpha > 0:
- keep_avg.update_values({'avg_ga_loss': loss_dict['ga_loss'].item()})
+ alignments, alignment_lengths, alignments_backward,
+ text_lengths)
# step time
step_time = time.time() - start_time
@@ -364,7 +324,7 @@ def evaluate(model, criterion, ap, global_step, epoch):
# compute alignment score
align_error = 1 - alignment_diagonal_score(alignments)
- keep_avg.update_value('avg_align_error', align_error)
+ loss_dict['align_error'] = align_error
# aggregate losses from processes
if num_gpus > 1:
@@ -373,14 +333,20 @@ def evaluate(model, criterion, ap, global_step, epoch):
if c.stopnet:
loss_dict['stopnet_loss'] = reduce_tensor(loss_dict['stopnet_loss'].data, num_gpus)
- keep_avg.update_values({
- 'avg_postnet_loss':
- float(loss_dict['postnet_loss'].item()),
- 'avg_decoder_loss':
- float(loss_dict['decoder_loss'].item()),
- 'avg_stopnet_loss':
- float(loss_dict['stopnet_loss'].item()),
- })
+ # detach loss values
+ loss_dict_new = dict()
+ for key, value in loss_dict.items():
+ if isinstance(value, (int, float)):
+ loss_dict_new[key] = value
+ else:
+ loss_dict_new[key] = value.item()
+ loss_dict = loss_dict_new
+
+ # update avg stats
+ update_train_values = dict()
+ for key, value in loss_dict.items():
+ update_train_values['avg_' + key] = value
+ keep_avg.update_values(update_train_values)
if c.print_eval:
c_logger.print_eval_step(num_iter, loss_dict, keep_avg.avg_values)
@@ -409,20 +375,11 @@ def evaluate(model, criterion, ap, global_step, epoch):
c.audio["sample_rate"])
# Plot Validation Stats
- epoch_stats = {
- "loss_postnet": keep_avg['avg_postnet_loss'],
- "loss_decoder": keep_avg['avg_decoder_loss'],
- "stopnet_loss": keep_avg['avg_stopnet_loss'],
- "alignment_score": keep_avg['avg_align_error'],
- }
- if c.bidirectional_decoder:
- epoch_stats['loss_decoder_backward'] = keep_avg['avg_decoder_b_loss']
+ if c.bidirectional_decoder or c.double_decoder_consistency:
align_b_img = alignments_backward[idx].data.cpu().numpy()
- eval_figures['alignment_backward'] = plot_alignment(align_b_img)
- if c.ga_alpha > 0:
- epoch_stats['guided_attention_loss'] = keep_avg['avg_ga_loss']
- tb_logger.tb_eval_stats(global_step, epoch_stats)
+ eval_figures['alignment2'] = plot_alignment(align_b_img)
+ tb_logger.tb_eval_stats(global_step, keep_avg.avg_values)
tb_logger.tb_eval_figures(global_step, eval_figures)
if args.rank == 0 and epoch > c.test_delay_epochs:
@@ -431,7 +388,8 @@ def evaluate(model, criterion, ap, global_step, epoch):
"It took me quite a long time to develop a voice, and now that I have it I'm not going to be silent.",
"Be a voice, not an echo.",
"I'm sorry Dave. I'm afraid I can't do that.",
- "This cake is great. It's so delicious and moist."
+ "This cake is great. It's so delicious and moist.",
+ "Prior to November 22, 1963."
]
else:
with open(c.test_sentences_file, "r") as f:
@@ -516,8 +474,6 @@ def main(args): # pylint: disable=redefined-outer-name
model = setup_model(num_chars, num_speakers, c)
- print(" | > Num output units : {}".format(ap.num_freq), flush=True)
-
params = set_weight_decay(model, c.wd)
optimizer = RAdam(params, lr=c.lr, weight_decay=0)
if c.stopnet and c.separate_stopnet:
@@ -542,7 +498,7 @@ def main(args): # pylint: disable=redefined-outer-name
except:
print(" > Partial model initialization.")
model_dict = model.state_dict()
- model_dict = set_init_dict(model_dict, checkpoint, c)
+ model_dict = set_init_dict(model_dict, checkpoint['model'], c)
model.load_state_dict(model_dict)
del model_dict
for group in optimizer.param_groups:
@@ -585,7 +541,6 @@ def main(args): # pylint: disable=redefined-outer-name
if c.bidirectional_decoder:
model.decoder_backward.set_r(r)
print("\n > Number of output frames:", model.decoder.r)
-
train_avg_loss_dict, global_step = train(model, criterion, optimizer,
optimizer_st, scheduler, ap,
global_step, epoch)
@@ -667,7 +622,7 @@ if __name__ == '__main__':
os.chmod(OUT_PATH, 0o775)
LOG_DIR = OUT_PATH
- tb_logger = TensorboardLogger(LOG_DIR)
+ tb_logger = TensorboardLogger(LOG_DIR, model_name='TTS')
# write model desc to tensorboard
tb_logger.tb_add_text('model-description', c['run_description'], 0)
diff --git a/utils/audio.py b/utils/audio.py
index 13eab3d6..31c065ff 100644
--- a/utils/audio.py
+++ b/utils/audio.py
@@ -17,7 +17,7 @@ class AudioProcessor(object):
hop_length=None,
win_length=None,
ref_level_db=None,
- num_freq=None,
+ fft_size=1024,
power=None,
preemphasis=0.0,
signal_norm=None,
@@ -25,6 +25,8 @@ class AudioProcessor(object):
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,
@@ -41,7 +43,7 @@ class AudioProcessor(object):
self.frame_shift_ms = frame_shift_ms
self.frame_length_ms = frame_length_ms
self.ref_level_db = ref_level_db
- self.num_freq = num_freq
+ self.fft_size = fft_size
self.power = power
self.preemphasis = preemphasis
self.griffin_lim_iters = griffin_lim_iters
@@ -49,6 +51,8 @@ class AudioProcessor(object):
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 = 'reflect'
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
@@ -57,11 +61,12 @@ class AudioProcessor(object):
self.stats_path = stats_path
# setup stft parameters
if hop_length is None:
- self.n_fft, self.hop_length, self.win_length = self._stft_parameters()
+ # 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
- self.n_fft = (self.num_freq - 1) * 2
assert min_level_db != 0.0, " [!] min_level_db is 0"
members = vars(self)
for key, value in members.items():
@@ -84,19 +89,18 @@ class AudioProcessor(object):
assert self.mel_fmax <= self.sample_rate // 2
return librosa.filters.mel(
self.sample_rate,
- self.n_fft,
+ self.fft_size,
n_mels=self.num_mels,
fmin=self.mel_fmin,
fmax=self.mel_fmax)
def _stft_parameters(self, ):
"""Compute necessary stft parameters with given time values"""
- n_fft = (self.num_freq - 1) * 2
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 n_fft, hop_length, win_length
+ return hop_length, win_length
### normalization ###
def _normalize(self, S):
@@ -108,7 +112,7 @@ class AudioProcessor(object):
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.n_fft / 2:
+ 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.')
@@ -137,7 +141,7 @@ class AudioProcessor(object):
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.n_fft / 2:
+ 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.')
@@ -182,11 +186,11 @@ class AudioProcessor(object):
### DB and AMP conversion ###
# pylint: disable=no-self-use
def _amp_to_db(self, x):
- return 20 * np.log10(np.maximum(1e-5, x))
+ return self.spec_gain * np.log10(np.maximum(1e-5, x))
# pylint: disable=no-self-use
def _db_to_amp(self, x):
- return np.power(10.0, x * 0.05)
+ return np.power(10.0, x / self.spec_gain)
### Preemphasis ###
def apply_preemphasis(self, x):
@@ -252,10 +256,10 @@ class AudioProcessor(object):
def _stft(self, y):
return librosa.stft(
y=y,
- n_fft=self.n_fft,
+ n_fft=self.fft_size,
hop_length=self.hop_length,
win_length=self.win_length,
- pad_mode='constant'
+ pad_mode=self.stft_pad_mode,
)
def _istft(self, y):
diff --git a/utils/console_logger.py b/utils/console_logger.py
index 5c6ec75f..0b361bb8 100644
--- a/utils/console_logger.py
+++ b/utils/console_logger.py
@@ -87,9 +87,9 @@ class ConsoleLogger():
diff = 0
if self.old_eval_loss_dict is not None:
diff = value - self.old_eval_loss_dict[key]
- if diff < 0:
+ if diff <= 0:
color = tcolors.OKGREEN
sign = ''
log_text += "{}{}:{} {:.5f} {}({}{:.5f})\n".format(indent, key, color, value, tcolors.ENDC, sign, diff)
self.old_eval_loss_dict = avg_loss_dict
- print(log_text, flush=True)
\ No newline at end of file
+ print(log_text, flush=True)
diff --git a/utils/generic_utils.py b/utils/generic_utils.py
index 1c7dd5e4..c806bdf3 100644
--- a/utils/generic_utils.py
+++ b/utils/generic_utils.py
@@ -75,6 +75,7 @@ def split_dataset(items):
is_multi_speaker = len(set(speakers)) > 1
eval_split_size = 500 if len(items) * 0.01 > 500 else int(
len(items) * 0.01)
+ assert eval_split_size > 0, " [!] You do not have enough samples to train. You need at least 100 samples."
np.random.seed(0)
np.random.shuffle(items)
if is_multi_speaker:
@@ -106,15 +107,15 @@ def sequence_mask(sequence_length, max_len=None):
return seq_range_expand < seq_length_expand
-def set_init_dict(model_dict, checkpoint, c):
+def set_init_dict(model_dict, checkpoint_state, c):
# Partial initialization: if there is a mismatch with new and old layer, it is skipped.
- for k, v in checkpoint['model'].items():
+ for k, v in checkpoint_state.items():
if k not in model_dict:
print(" | > Layer missing in the model definition: {}".format(k))
# 1. filter out unnecessary keys
pretrained_dict = {
k: v
- for k, v in checkpoint['model'].items() if k in model_dict
+ for k, v in checkpoint_state.items() if k in model_dict
}
# 2. filter out different size layers
pretrained_dict = {
@@ -145,7 +146,7 @@ def setup_model(num_chars, num_speakers, c):
model = MyModel(num_chars=num_chars,
num_speakers=num_speakers,
r=c.r,
- postnet_output_dim=c.audio['num_freq'],
+ postnet_output_dim=int(c.audio['fft_size'] / 2 + 1),
decoder_output_dim=c.audio['num_mels'],
gst=c.use_gst,
memory_size=c.memory_size,
@@ -160,13 +161,16 @@ def setup_model(num_chars, num_speakers, c):
location_attn=c.location_attn,
attn_K=c.attention_heads,
separate_stopnet=c.separate_stopnet,
- bidirectional_decoder=c.bidirectional_decoder)
+ bidirectional_decoder=c.bidirectional_decoder,
+ double_decoder_consistency=c.double_decoder_consistency,
+ ddc_r=c.ddc_r)
elif c.model.lower() == "tacotron2":
model = MyModel(num_chars=num_chars,
num_speakers=num_speakers,
r=c.r,
postnet_output_dim=c.audio['num_mels'],
decoder_output_dim=c.audio['num_mels'],
+ gst=c.use_gst,
attn_type=c.attention_type,
attn_win=c.windowing,
attn_norm=c.attention_norm,
@@ -178,7 +182,9 @@ def setup_model(num_chars, num_speakers, c):
location_attn=c.location_attn,
attn_K=c.attention_heads,
separate_stopnet=c.separate_stopnet,
- bidirectional_decoder=c.bidirectional_decoder)
+ bidirectional_decoder=c.bidirectional_decoder,
+ double_decoder_consistency=c.double_decoder_consistency,
+ ddc_r=c.ddc_r)
return model
class KeepAverage():
@@ -197,14 +203,19 @@ class KeepAverage():
self.iters[name] = init_iter
def update_value(self, name, value, weighted_avg=False):
- if weighted_avg:
- self.avg_values[name] = 0.99 * self.avg_values[name] + 0.01 * value
- self.iters[name] += 1
+ if name not in self.avg_values:
+ # add value if not exist before
+ self.add_value(name, init_val=value)
else:
- self.avg_values[name] = self.avg_values[name] * \
- self.iters[name] + value
- self.iters[name] += 1
- self.avg_values[name] /= self.iters[name]
+ # else update existing value
+ if weighted_avg:
+ self.avg_values[name] = 0.99 * self.avg_values[name] + 0.01 * value
+ self.iters[name] += 1
+ else:
+ self.avg_values[name] = self.avg_values[name] * \
+ self.iters[name] + value
+ self.iters[name] += 1
+ self.avg_values[name] /= self.iters[name]
def add_values(self, name_dict):
for key, value in name_dict.items():
@@ -241,7 +252,7 @@ def check_config(c):
# audio processing parameters
_check_argument('num_mels', c['audio'], restricted=True, val_type=int, min_val=10, max_val=2056)
- _check_argument('num_freq', c['audio'], restricted=True, val_type=int, min_val=128, max_val=4058)
+ _check_argument('fft_size', c['audio'], restricted=True, val_type=int, min_val=128, max_val=4058)
_check_argument('sample_rate', c['audio'], restricted=True, val_type=int, min_val=512, max_val=100000)
_check_argument('frame_length_ms', c['audio'], restricted=True, val_type=float, min_val=10, max_val=1000, alternative='win_length')
_check_argument('frame_shift_ms', c['audio'], restricted=True, val_type=float, min_val=1, max_val=1000, alternative='hop_length')
@@ -267,6 +278,7 @@ def check_config(c):
_check_argument('clip_norm', c['audio'], restricted=True, val_type=bool)
_check_argument('mel_fmin', c['audio'], restricted=True, val_type=float, min_val=0.0, max_val=1000)
_check_argument('mel_fmax', c['audio'], restricted=True, val_type=float, min_val=500.0)
+ _check_argument('spec_gain', c['audio'], restricted=True, val_type=float, min_val=1, max_val=100)
_check_argument('do_trim_silence', c['audio'], restricted=True, val_type=bool)
_check_argument('trim_db', c['audio'], restricted=True, val_type=int)
@@ -308,6 +320,8 @@ def check_config(c):
_check_argument('transition_agent', c, restricted=True, val_type=bool)
_check_argument('location_attn', c, restricted=True, val_type=bool)
_check_argument('bidirectional_decoder', c, restricted=True, val_type=bool)
+ _check_argument('double_decoder_consistency', c, restricted=True, val_type=bool)
+ _check_argument('ddc_r', c, restricted='double_decoder_consistency' in c.keys(), min_val=1, max_val=7, val_type=int)
# stopnet
_check_argument('stopnet', c, restricted=True, val_type=bool)
@@ -315,6 +329,7 @@ def check_config(c):
# tensorboard
_check_argument('print_step', c, restricted=True, val_type=int, min_val=1)
+ _check_argument('tb_plot_step', c, restricted=True, val_type=int, min_val=1)
_check_argument('save_step', c, restricted=True, val_type=int, min_val=1)
_check_argument('checkpoint', c, restricted=True, val_type=bool)
_check_argument('tb_model_param_stats', c, restricted=True, val_type=bool)
diff --git a/utils/tensorboard_logger.py b/utils/tensorboard_logger.py
index 15fe04e4..cbf68ad6 100644
--- a/utils/tensorboard_logger.py
+++ b/utils/tensorboard_logger.py
@@ -3,7 +3,8 @@ from tensorboardX import SummaryWriter
class TensorboardLogger(object):
- def __init__(self, log_dir):
+ def __init__(self, log_dir, model_name):
+ self.model_name = model_name
self.writer = SummaryWriter(log_dir)
self.train_stats = {}
self.eval_stats = {}
@@ -50,31 +51,31 @@ class TensorboardLogger(object):
traceback.print_exc()
def tb_train_iter_stats(self, step, stats):
- self.dict_to_tb_scalar("TrainIterStats", stats, step)
+ self.dict_to_tb_scalar(f"{self.model_name}_TrainIterStats", stats, step)
def tb_train_epoch_stats(self, step, stats):
- self.dict_to_tb_scalar("TrainEpochStats", stats, step)
+ self.dict_to_tb_scalar(f"{self.model_name}_TrainEpochStats", stats, step)
def tb_train_figures(self, step, figures):
- self.dict_to_tb_figure("TrainFigures", figures, step)
+ self.dict_to_tb_figure(f"{self.model_name}_TrainFigures", figures, step)
def tb_train_audios(self, step, audios, sample_rate):
- self.dict_to_tb_audios("TrainAudios", audios, step, sample_rate)
+ self.dict_to_tb_audios(f"{self.model_name}_TrainAudios", audios, step, sample_rate)
def tb_eval_stats(self, step, stats):
- self.dict_to_tb_scalar("EvalStats", stats, step)
+ self.dict_to_tb_scalar(f"{self.model_name}_EvalStats", stats, step)
def tb_eval_figures(self, step, figures):
- self.dict_to_tb_figure("EvalFigures", figures, step)
+ self.dict_to_tb_figure(f"{self.model_name}_EvalFigures", figures, step)
def tb_eval_audios(self, step, audios, sample_rate):
- self.dict_to_tb_audios("EvalAudios", audios, step, sample_rate)
+ self.dict_to_tb_audios(f"{self.model_name}_EvalAudios", audios, step, sample_rate)
def tb_test_audios(self, step, audios, sample_rate):
- self.dict_to_tb_audios("TestAudios", audios, step, sample_rate)
+ self.dict_to_tb_audios(f"{self.model_name}_TestAudios", audios, step, sample_rate)
def tb_test_figures(self, step, figures):
- self.dict_to_tb_figure("TestFigures", figures, step)
+ self.dict_to_tb_figure(f"{self.model_name}_TestFigures", figures, step)
def tb_add_text(self, title, text, step):
self.writer.add_text(title, text, step)
diff --git a/utils/text/__init__.py b/utils/text/__init__.py
index 79069192..41aa6778 100644
--- a/utils/text/__init__.py
+++ b/utils/text/__init__.py
@@ -77,7 +77,6 @@ def phoneme_to_sequence(text, cleaner_names, language, enable_eos_bos=False, tp=
_phonemes_to_id = {s: i for i, s in enumerate(_phonemes)}
sequence = []
- text = text.replace(":", "")
clean_text = _clean_text(text, cleaner_names)
to_phonemes = text2phone(clean_text, language)
if to_phonemes is None:
diff --git a/utils/text/cleaners.py b/utils/text/cleaners.py
index 35da8aef..f0a66f57 100644
--- a/utils/text/cleaners.py
+++ b/utils/text/cleaners.py
@@ -71,7 +71,7 @@ def remove_aux_symbols(text):
def replace_symbols(text):
text = text.replace(';', ',')
text = text.replace('-', ' ')
- text = text.replace(':', ' ')
+ text = text.replace(':', ',')
text = text.replace('&', 'and')
return text
diff --git a/utils/text/number_norm.py b/utils/text/number_norm.py
index d3d9a46b..54fa1093 100644
--- a/utils/text/number_norm.py
+++ b/utils/text/number_norm.py
@@ -1,127 +1,72 @@
+# -*- coding: utf-8 -*-
+""" from https://github.com/keithito/tacotron """
+
+import inflect
import re
+
+_inflect = inflect.engine()
_comma_number_re = re.compile(r'([0-9][0-9\,]+[0-9])')
_decimal_number_re = re.compile(r'([0-9]+\.[0-9]+)')
_pounds_re = re.compile(r'£([0-9\,]*[0-9]+)')
_dollars_re = re.compile(r'\$([0-9\.\,]*[0-9]+)')
-_ordinal_re = re.compile(r'([0-9]+)(st|nd|rd|th)')
+_ordinal_re = re.compile(r'[0-9]+(st|nd|rd|th)')
_number_re = re.compile(r'[0-9]+')
-_units = [
- '', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine',
- 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen',
- 'seventeen', 'eighteen', 'nineteen'
-]
-
-_tens = [
- '',
- 'ten',
- 'twenty',
- 'thirty',
- 'forty',
- 'fifty',
- 'sixty',
- 'seventy',
- 'eighty',
- 'ninety',
-]
-
-_digit_groups = [
- '',
- 'thousand',
- 'million',
- 'billion',
- 'trillion',
- 'quadrillion',
-]
-
-_ordinal_suffixes = [
- ('one', 'first'),
- ('two', 'second'),
- ('three', 'third'),
- ('five', 'fifth'),
- ('eight', 'eighth'),
- ('nine', 'ninth'),
- ('twelve', 'twelfth'),
- ('ty', 'tieth'),
-]
-
def _remove_commas(m):
- return m.group(1).replace(',', '')
+ return m.group(1).replace(',', '')
def _expand_decimal_point(m):
- return m.group(1).replace('.', ' point ')
+ return m.group(1).replace('.', ' point ')
def _expand_dollars(m):
- match = m.group(1)
- parts = match.split('.')
- if len(parts) > 2:
- return match + ' dollars' # Unexpected format
- dollars = int(parts[0]) if parts[0] else 0
- cents = int(parts[1]) if len(parts) > 1 and parts[1] else 0
- if dollars and cents:
- dollar_unit = 'dollar' if dollars == 1 else 'dollars'
- cent_unit = 'cent' if cents == 1 else 'cents'
- return '%s %s, %s %s' % (dollars, dollar_unit, cents, cent_unit)
- if dollars:
- dollar_unit = 'dollar' if dollars == 1 else 'dollars'
- return '%s %s' % (dollars, dollar_unit)
- if cents:
- cent_unit = 'cent' if cents == 1 else 'cents'
- return '%s %s' % (cents, cent_unit)
+ match = m.group(1)
+ parts = match.split('.')
+ if len(parts) > 2:
+ return match + ' dollars' # Unexpected format
+ dollars = int(parts[0]) if parts[0] else 0
+ cents = int(parts[1]) if len(parts) > 1 and parts[1] else 0
+ if dollars and cents:
+ dollar_unit = 'dollar' if dollars == 1 else 'dollars'
+ cent_unit = 'cent' if cents == 1 else 'cents'
+ return '%s %s, %s %s' % (dollars, dollar_unit, cents, cent_unit)
+ elif dollars:
+ dollar_unit = 'dollar' if dollars == 1 else 'dollars'
+ return '%s %s' % (dollars, dollar_unit)
+ elif cents:
+ cent_unit = 'cent' if cents == 1 else 'cents'
+ return '%s %s' % (cents, cent_unit)
+ else:
return 'zero dollars'
-def _standard_number_to_words(n, digit_group):
- parts = []
- if n >= 1000:
- # Format next higher digit group.
- parts.append(_standard_number_to_words(n // 1000, digit_group + 1))
- n = n % 1000
-
- if n >= 100:
- parts.append('%s hundred' % _units[n // 100])
- if n % 100 >= len(_units):
- parts.append(_tens[(n % 100) // 10])
- parts.append(_units[(n % 100) % 10])
- else:
- parts.append(_units[n % 100])
- if n > 0:
- parts.append(_digit_groups[digit_group])
- return ' '.join([x for x in parts if x])
-
-
-def _number_to_words(n):
- # Handle special cases first, then go to the standard case:
- if n >= 1000000000000000000:
- return str(n) # Too large, just return the digits
- if n == 0:
- return 'zero'
- if n % 100 == 0 and n % 1000 != 0 and n < 3000:
- return _standard_number_to_words(n // 100, 0) + ' hundred'
- return _standard_number_to_words(n, 0)
+def _expand_ordinal(m):
+ return _inflect.number_to_words(m.group(0))
def _expand_number(m):
- return _number_to_words(int(m.group(0)))
-
-
-def _expand_ordinal(m):
- num = _number_to_words(int(m.group(1)))
- for suffix, replacement in _ordinal_suffixes:
- if num.endswith(suffix):
- return num[:-len(suffix)] + replacement
- return num + 'th'
+ num = int(m.group(0))
+ if num > 1000 and num < 3000:
+ if num == 2000:
+ return 'two thousand'
+ elif num > 2000 and num < 2010:
+ return 'two thousand ' + _inflect.number_to_words(num % 100)
+ elif num % 100 == 0:
+ return _inflect.number_to_words(num // 100) + ' hundred'
+ else:
+ return _inflect.number_to_words(num, andword='', zero='oh', group=2).replace(', ', ' ')
+ else:
+ return _inflect.number_to_words(num, andword='')
def normalize_numbers(text):
- text = re.sub(_comma_number_re, _remove_commas, text)
- text = re.sub(_pounds_re, r'\1 pounds', text)
- text = re.sub(_dollars_re, _expand_dollars, text)
- text = re.sub(_decimal_number_re, _expand_decimal_point, text)
- text = re.sub(_ordinal_re, _expand_ordinal, text)
- text = re.sub(_number_re, _expand_number, text)
- return text
+ text = re.sub(_comma_number_re, _remove_commas, text)
+ text = re.sub(_pounds_re, r'\1 pounds', text)
+ text = re.sub(_dollars_re, _expand_dollars, text)
+ text = re.sub(_decimal_number_re, _expand_decimal_point, text)
+ text = re.sub(_ordinal_re, _expand_ordinal, text)
+ text = re.sub(_number_re, _expand_number, text)
+ return text
\ No newline at end of file
diff --git a/utils/training.py b/utils/training.py
index ebf8fd13..9046f9e0 100644
--- a/utils/training.py
+++ b/utils/training.py
@@ -2,6 +2,17 @@ import torch
import numpy as np
+def setup_torch_training_env(cudnn_enable, cudnn_benchmark):
+ torch.backends.cudnn.enabled = cudnn_enable
+ torch.backends.cudnn.benchmark = cudnn_benchmark
+ torch.manual_seed(54321)
+ use_cuda = torch.cuda.is_available()
+ num_gpus = torch.cuda.device_count()
+ print(" > Using CUDA: ", use_cuda)
+ print(" > Number of GPUs: ", num_gpus)
+ return use_cuda, num_gpus
+
+
def check_update(model, grad_clip, ignore_stopnet=False):
r'''Check model gradient against unexpected jumps and failures'''
skip_flag = False
@@ -9,9 +20,15 @@ def check_update(model, grad_clip, ignore_stopnet=False):
grad_norm = torch.nn.utils.clip_grad_norm_([param for name, param in model.named_parameters() if 'stopnet' not in name], grad_clip)
else:
grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip)
- if torch.isinf(grad_norm):
- print(" | > Gradient is INF !!")
- skip_flag = True
+ # compatibility with different torch versions
+ if isinstance(grad_norm, float):
+ if np.isinf(grad_norm):
+ print(" | > Gradient is INF !!")
+ skip_flag = True
+ else:
+ if torch.isinf(grad_norm):
+ print(" | > Gradient is INF !!")
+ skip_flag = True
return grad_norm, skip_flag
diff --git a/utils/visual.py b/utils/visual.py
index 87fbc8e4..b4ebec9a 100644
--- a/utils/visual.py
+++ b/utils/visual.py
@@ -27,14 +27,15 @@ def plot_alignment(alignment, info=None, fig_size=(16, 10), title=None):
return fig
-def plot_spectrogram(linear_output, audio, fig_size=(16, 10)):
- if isinstance(linear_output, torch.Tensor):
- linear_output_ = linear_output.detach().cpu().numpy().squeeze()
+def plot_spectrogram(spectrogram, ap=None, fig_size=(16, 10)):
+ if isinstance(spectrogram, torch.Tensor):
+ spectrogram_ = spectrogram.detach().cpu().numpy().squeeze().T
else:
- linear_output_ = linear_output
- spectrogram = audio._denormalize(linear_output_.T) # pylint: disable=protected-access
+ spectrogram_ = spectrogram.T
+ if ap is not None:
+ spectrogram_ = ap._denormalize(spectrogram_) # pylint: disable=protected-access
fig = plt.figure(figsize=fig_size)
- plt.imshow(spectrogram, aspect="auto", origin="lower")
+ plt.imshow(spectrogram_, aspect="auto", origin="lower")
plt.colorbar()
plt.tight_layout()
return fig
diff --git a/vocoder/README.md b/vocoder/README.md
new file mode 100644
index 00000000..1b65f929
--- /dev/null
+++ b/vocoder/README.md
@@ -0,0 +1,38 @@
+# Mozilla TTS Vocoders (Experimental)
+
+We provide here different vocoder implementations which can be combined with our TTS models to enable "FASTER THAN REAL-TIME" end-to-end TTS stack.
+
+Currently, there are implementations of the following models.
+
+- Melgan
+- MultiBand-Melgan
+- GAN-TTS (Discriminator Only)
+
+It is also very easy to adapt different vocoder models as we provide here a flexible and modular (but not too modular) framework.
+
+## Training a model
+
+You can see here an example (Soon)[Colab Notebook]() training MelGAN with LJSpeech dataset.
+
+In order to train a new model, you need to collecto all your wav files under a common parent folder and give this path to `data_path` field in '''config.json'''
+
+You need to define other relevant parameters in your ```config.json``` and then start traning with the following command from Mozilla TTS root path.
+
+```CUDA_VISIBLE_DEVICES='1' python vocoder/train.py --config_path path/to/config.json```
+
+Exampled config files can be found under `vocoder/configs/` folder.
+
+You can continue a previous training by the following command.
+
+```CUDA_VISIBLE_DEVICES='1' python vocoder/train.py --continue_path path/to/your/model/folder```
+
+You can fine-tune a pre-trained model by the following command.
+
+```CUDA_VISIBLE_DEVICES='1' python vocoder/train.py --restore_path path/to/your/model.pth.tar```
+
+Restoring a model starts a new training in a different output folder. It only restores model weights with the given checkpoint file. However, continuing a training starts from the same conditions the previous training run left off.
+
+You can also follow your training runs on Tensorboard as you do with our TTS models.
+
+## Acknowledgement
+Thanks to @kan-bayashi for his [repository](https://github.com/kan-bayashi/ParallelWaveGAN) being the start point of our work.
\ No newline at end of file
diff --git a/vocoder/__init__.py b/vocoder/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vocoder/compute_tts_features.py b/vocoder/compute_tts_features.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vocoder/configs/multiband-melgan_and_rwd_config.json b/vocoder/configs/multiband-melgan_and_rwd_config.json
new file mode 100644
index 00000000..0b751854
--- /dev/null
+++ b/vocoder/configs/multiband-melgan_and_rwd_config.json
@@ -0,0 +1,151 @@
+{
+ "run_name": "multiband-melgan-rwd",
+ "run_description": "multiband melgan with random window discriminator from https://arxiv.org/pdf/1909.11646.pdf",
+
+ // AUDIO PARAMETERS
+ "audio":{
+ // stft parameters
+ "num_freq": 513, // number of stft frequency levels. Size of the linear spectogram frame.
+ "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.
+
+ // Audio processing parameters
+ "sample_rate": 22050, // DATASET-RELATED: wav sample-rate. If different than the original data, it is resampled.
+ "preemphasis": 0.0, // pre-emphasis to reduce spec noise and make it more structured. If 0.0, no -pre-emphasis.
+ "ref_level_db": 20, // reference level db, theoretically 20db is the sound of air.
+
+ // Silence trimming
+ "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.
+
+ // Griffin-Lim
+ "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.
+
+ // MelSpectrogram parameters
+ "num_mels": 80, // size of the mel spec frame.
+ "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!!
+
+ // Normalization parameters
+ "signal_norm": true, // normalize spec values. Mean-Var normalization if 'stats_path' is defined otherwise range normalization defined by the other params.
+ "min_level_db": -100, // lower bound for normalization
+ "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.
+ "stats_path": null // DO NOT USE WITH MULTI_SPEAKER MODEL. scaler stats file computed by 'compute_statistics.py'. If it is defined, mean-std based notmalization is used and other normalization params are ignored
+ },
+
+ // DISTRIBUTED TRAINING
+ // "distributed":{
+ // "backend": "nccl",
+ // "url": "tcp:\/\/localhost:54321"
+ // },
+
+ // MODEL PARAMETERS
+ "use_pqmf": true,
+
+ // LOSS PARAMETERS
+ "use_stft_loss": true,
+ "use_subband_stft_loss": true,
+ "use_mse_gan_loss": true,
+ "use_hinge_gan_loss": false,
+ "use_feat_match_loss": false, // use only with melgan discriminators
+
+ // loss weights
+ "stft_loss_weight": 0.5,
+ "subband_stft_loss_weight": 0.5,
+ "mse_G_loss_weight": 2.5,
+ "hinge_G_loss_weight": 2.5,
+ "feat_match_loss_weight": 25,
+
+ // multiscale stft loss parameters
+ "stft_loss_params": {
+ "n_ffts": [1024, 2048, 512],
+ "hop_lengths": [120, 240, 50],
+ "win_lengths": [600, 1200, 240]
+ },
+
+ // subband multiscale stft loss parameters
+ "subband_stft_loss_params":{
+ "n_ffts": [384, 683, 171],
+ "hop_lengths": [30, 60, 10],
+ "win_lengths": [150, 300, 60]
+ },
+
+ "target_loss": "avg_G_loss", // loss value to pick the best model to save after each epoch
+
+ // DISCRIMINATOR
+ "discriminator_model": "random_window_discriminator",
+ "discriminator_model_params":{
+ "uncond_disc_donwsample_factors": [8, 4],
+ "cond_disc_downsample_factors": [[8, 4, 2, 2, 2], [8, 4, 2, 2], [8, 4, 2], [8, 4], [4, 2, 2]],
+ "cond_disc_out_channels": [[128, 128, 256, 256], [128, 256, 256], [128, 256], [256], [128, 256]],
+ "window_sizes": [512, 1024, 2048, 4096, 8192]
+ },
+ "steps_to_start_discriminator": 200000, // steps required to start GAN trainining.1
+
+ // GENERATOR
+ "generator_model": "multiband_melgan_generator",
+ "generator_model_params": {
+ "upsample_factors":[8, 4, 2],
+ "num_res_blocks": 4
+ },
+
+ // DATASET
+ "data_path": "/home/erogol/Data/LJSpeech-1.1/wavs/",
+ "seq_len": 16384,
+ "pad_short": 2000,
+ "conv_pad": 0,
+ "use_noise_augment": false,
+ "use_cache": true,
+
+ "reinit_layers": [], // give a list of layer names to restore from the given checkpoint. If not defined, it reloads all heuristically matching layers.
+
+ // TRAINING
+ "batch_size": 64, // Batch size for training. Lower values than 32 might cause hard to learn attention. It is overwritten by 'gradual_training'.
+
+ // VALIDATION
+ "run_eval": true,
+ "test_delay_epochs": 10, //Until attention is aligned, testing only wastes computation time.
+ "test_sentences_file": null, // set a file to load sentences to be used for testing. If it is null then we use default english sentences.
+
+ // OPTIMIZER
+ "noam_schedule": false, // use noam warmup and lr schedule.
+ "warmup_steps_gen": 4000, // Noam decay steps to increase the learning rate from 0 to "lr"
+ "warmup_steps_disc": 4000,
+ "epochs": 10000, // total number of epochs to train.
+ "wd": 0.0, // Weight decay weight.
+ "gen_clip_grad": -1, // Generator gradient clipping threshold. Apply gradient clipping if > 0
+ "disc_clip_grad": -1, // Discriminator gradient clipping threshold.
+ "lr_scheduler_gen": "MultiStepLR", // one of the schedulers from https://pytorch.org/docs/stable/optim.html#how-to-adjust-learning-rate
+ "lr_scheduler_gen_params": {
+ "gamma": 0.5,
+ "milestones": [100000, 200000, 300000, 400000, 500000, 600000]
+ },
+ "lr_scheduler_disc": "MultiStepLR", // one of the schedulers from https://pytorch.org/docs/stable/optim.html#how-to-adjust-learning-rate
+ "lr_scheduler_disc_params": {
+ "gamma": 0.5,
+ "milestones": [100000, 200000, 300000, 400000, 500000, 600000]
+ },
+ "lr_gen": 1e-4, // Initial learning rate. If Noam decay is active, maximum learning rate.
+ "lr_disc": 1e-4,
+
+ // TENSORBOARD and LOGGING
+ "print_step": 25, // Number of steps to log traning on console.
+ "print_eval": false, // If True, it prints loss values for each step in eval run.
+ "save_step": 25000, // Number of training steps expected to plot training stats on TB and save model checkpoints.
+ "checkpoint": true, // If true, it saves checkpoints per "save_step"
+ "tb_model_param_stats": false, // true, plots param stats per layer on tensorboard. Might be memory consuming, but good for debugging.
+
+ // DATA LOADING
+ "num_loader_workers": 4, // number of training data loader processes. Don't set it too big. 4-8 are good values.
+ "num_val_loader_workers": 4, // number of evaluation data loader processes.
+ "eval_split_size": 10,
+
+ // PATHS
+ "output_path": "/home/erogol/Models/LJSpeech/"
+}
+
diff --git a/vocoder/configs/multiband_melgan_config.json b/vocoder/configs/multiband_melgan_config.json
new file mode 100644
index 00000000..a89d43bb
--- /dev/null
+++ b/vocoder/configs/multiband_melgan_config.json
@@ -0,0 +1,144 @@
+{
+ "run_name": "multiband-melgan",
+ "run_description": "multiband melgan mean-var scaling",
+
+ // AUDIO PARAMETERS
+ "audio":{
+ "fft_size": 1024, // number of stft frequency levels. Size of the linear spectogram frame.
+ "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.
+
+ // Audio processing parameters
+ "sample_rate": 22050, // DATASET-RELATED: wav sample-rate. If different than the original data, it is resampled.
+ "preemphasis": 0.0, // pre-emphasis to reduce spec noise and make it more structured. If 0.0, no -pre-emphasis.
+ "ref_level_db": 0, // reference level db, theoretically 20db is the sound of air.
+
+ // Silence trimming
+ "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.
+
+ // MelSpectrogram parameters
+ "num_mels": 80, // size of the mel spec frame.
+ "mel_fmin": 50.0, // minimum freq level for mel-spec. ~50 for male and ~95 for female voices. Tune for dataset!!
+ "mel_fmax": 7600.0, // maximum freq level for mel-spec. Tune for dataset!!
+ "spec_gain": 1.0, // scaler value appplied after log transform of spectrogram.
+
+ // Normalization parameters
+ "signal_norm": true, // normalize spec values. Mean-Var normalization if 'stats_path' is defined otherwise range normalization defined by the other params.
+ "min_level_db": -100, // lower bound for normalization
+ "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.
+ "stats_path": "/home/erogol/Data/LJSpeech-1.1/scale_stats.npy" // DO NOT USE WITH MULTI_SPEAKER MODEL. scaler stats file computed by 'compute_statistics.py'. If it is defined, mean-std based notmalization is used and other normalization params are ignored
+ },
+
+ // DISTRIBUTED TRAINING
+ // "distributed":{
+ // "backend": "nccl",
+ // "url": "tcp:\/\/localhost:54321"
+ // },
+
+ // MODEL PARAMETERS
+ "use_pqmf": true,
+
+ // LOSS PARAMETERS
+ "use_stft_loss": true,
+ "use_subband_stft_loss": true,
+ "use_mse_gan_loss": true,
+ "use_hinge_gan_loss": false,
+ "use_feat_match_loss": false, // use only with melgan discriminators
+
+ // loss weights
+ "stft_loss_weight": 0.5,
+ "subband_stft_loss_weight": 0.5,
+ "mse_G_loss_weight": 2.5,
+ "hinge_G_loss_weight": 2.5,
+ "feat_match_loss_weight": 25,
+
+ // multiscale stft loss parameters
+ "stft_loss_params": {
+ "n_ffts": [1024, 2048, 512],
+ "hop_lengths": [120, 240, 50],
+ "win_lengths": [600, 1200, 240]
+ },
+
+ // subband multiscale stft loss parameters
+ "subband_stft_loss_params":{
+ "n_ffts": [384, 683, 171],
+ "hop_lengths": [30, 60, 10],
+ "win_lengths": [150, 300, 60]
+ },
+
+ "target_loss": "avg_G_loss", // loss value to pick the best model to save after each epoch
+
+ // DISCRIMINATOR
+ "discriminator_model": "melgan_multiscale_discriminator",
+ "discriminator_model_params":{
+ "base_channels": 16,
+ "max_channels":512,
+ "downsample_factors":[4, 4, 4]
+ },
+ "steps_to_start_discriminator": 200000, // steps required to start GAN trainining.1
+
+ // GENERATOR
+ "generator_model": "multiband_melgan_generator",
+ "generator_model_params": {
+ "upsample_factors":[8, 4, 2],
+ "num_res_blocks": 4
+ },
+
+ // DATASET
+ "data_path": "/home/erogol/Data/LJSpeech-1.1/wavs/",
+ "feature_path": null,
+ "seq_len": 16384,
+ "pad_short": 2000,
+ "conv_pad": 0,
+ "use_noise_augment": false,
+ "use_cache": true,
+
+ "reinit_layers": [], // give a list of layer names to restore from the given checkpoint. If not defined, it reloads all heuristically matching layers.
+
+ // TRAINING
+ "batch_size": 64, // Batch size for training. Lower values than 32 might cause hard to learn attention. It is overwritten by 'gradual_training'.
+
+ // VALIDATION
+ "run_eval": true,
+ "test_delay_epochs": 10, //Until attention is aligned, testing only wastes computation time.
+ "test_sentences_file": null, // set a file to load sentences to be used for testing. If it is null then we use default english sentences.
+
+ // OPTIMIZER
+ "epochs": 10000, // total number of epochs to train.
+ "wd": 0.0, // Weight decay weight.
+ "gen_clip_grad": -1, // Generator gradient clipping threshold. Apply gradient clipping if > 0
+ "disc_clip_grad": -1, // Discriminator gradient clipping threshold.
+ "lr_scheduler_gen": "MultiStepLR", // one of the schedulers from https://pytorch.org/docs/stable/optim.html#how-to-adjust-learning-rate
+ "lr_scheduler_gen_params": {
+ "gamma": 0.5,
+ "milestones": [100000, 200000, 300000, 400000, 500000, 600000]
+ },
+ "lr_scheduler_disc": "MultiStepLR", // one of the schedulers from https://pytorch.org/docs/stable/optim.html#how-to-adjust-learning-rate
+ "lr_scheduler_disc_params": {
+ "gamma": 0.5,
+ "milestones": [100000, 200000, 300000, 400000, 500000, 600000]
+ },
+ "lr_gen": 1e-4, // Initial learning rate. If Noam decay is active, maximum learning rate.
+ "lr_disc": 1e-4,
+
+ // TENSORBOARD and LOGGING
+ "print_step": 25, // Number of steps to log traning on console.
+ "print_eval": false, // If True, it prints loss values for each step in eval run.
+ "save_step": 25000, // Number of training steps expected to plot training stats on TB and save model checkpoints.
+ "checkpoint": true, // If true, it saves checkpoints per "save_step"
+ "tb_model_param_stats": false, // true, plots param stats per layer on tensorboard. Might be memory consuming, but good for debugging.
+
+ // DATA LOADING
+ "num_loader_workers": 4, // number of training data loader processes. Don't set it too big. 4-8 are good values.
+ "num_val_loader_workers": 4, // number of evaluation data loader processes.
+ "eval_split_size": 10,
+
+ // PATHS
+ "output_path": "/home/erogol/Models/LJSpeech/"
+}
+
diff --git a/vocoder/datasets/__init__.py b/vocoder/datasets/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vocoder/datasets/gan_dataset.py b/vocoder/datasets/gan_dataset.py
new file mode 100644
index 00000000..55513e7d
--- /dev/null
+++ b/vocoder/datasets/gan_dataset.py
@@ -0,0 +1,126 @@
+import os
+import glob
+import torch
+import random
+import numpy as np
+from torch.utils.data import Dataset
+from multiprocessing import Manager
+
+
+class GANDataset(Dataset):
+ """
+ GAN Dataset searchs for all the wav files under root path
+ and converts them to acoustic features on the fly and returns
+ random segments of (audio, feature) couples.
+ """
+ def __init__(self,
+ ap,
+ items,
+ seq_len,
+ hop_len,
+ pad_short,
+ conv_pad=2,
+ is_training=True,
+ return_segments=True,
+ use_noise_augment=False,
+ use_cache=False,
+ verbose=False):
+
+ self.ap = ap
+ self.item_list = items
+ self.compute_feat = not isinstance(items[0], (tuple, list))
+ self.seq_len = seq_len
+ self.hop_len = hop_len
+ self.pad_short = pad_short
+ self.conv_pad = conv_pad
+ self.is_training = is_training
+ self.return_segments = return_segments
+ self.use_cache = use_cache
+ self.use_noise_augment = use_noise_augment
+ self.verbose = verbose
+
+ assert seq_len % hop_len == 0, " [!] seq_len has to be a multiple of hop_len."
+ self.feat_frame_len = seq_len // hop_len + (2 * conv_pad)
+
+ # map G and D instances
+ self.G_to_D_mappings = list(range(len(self.item_list)))
+ self.shuffle_mapping()
+
+ # cache acoustic features
+ if use_cache:
+ self.create_feature_cache()
+
+ def create_feature_cache(self):
+ self.manager = Manager()
+ self.cache = self.manager.list()
+ self.cache += [None for _ in range(len(self.item_list))]
+
+ @staticmethod
+ def find_wav_files(path):
+ return glob.glob(os.path.join(path, '**', '*.wav'), recursive=True)
+
+ def __len__(self):
+ return len(self.item_list)
+
+ def __getitem__(self, idx):
+ """ Return different items for Generator and Discriminator and
+ cache acoustic features """
+ if self.return_segments:
+ idx2 = self.G_to_D_mappings[idx]
+ item1 = self.load_item(idx)
+ item2 = self.load_item(idx2)
+ return item1, item2
+ item1 = self.load_item(idx)
+ return item1
+
+ def shuffle_mapping(self):
+ random.shuffle(self.G_to_D_mappings)
+
+ def load_item(self, idx):
+ """ load (audio, feat) couple """
+ if self.compute_feat:
+ # compute features from wav
+ wavpath = self.item_list[idx]
+ # print(wavpath)
+
+ if self.use_cache and self.cache[idx] is not None:
+ audio, mel = self.cache[idx]
+ else:
+ audio = self.ap.load_wav(wavpath)
+ mel = self.ap.melspectrogram(audio)
+ else:
+
+ # load precomputed features
+ wavpath, feat_path = self.item_list[idx]
+
+ if self.use_cache and self.cache[idx] is not None:
+ audio, mel = self.cache[idx]
+ else:
+ audio = self.ap.load_wav(wavpath)
+ mel = np.load(feat_path)
+
+ if len(audio) < self.seq_len + self.pad_short:
+ audio = np.pad(audio, (0, self.seq_len + self.pad_short - len(audio)), \
+ mode='constant', constant_values=0.0)
+
+ # correct the audio length wrt padding applied in stft
+ audio = np.pad(audio, (0, self.hop_len), mode="edge")
+ audio = audio[:mel.shape[-1] * self.hop_len]
+ assert mel.shape[-1] * self.hop_len == audio.shape[-1], f' [!] {mel.shape[-1] * self.hop_len} vs {audio.shape[-1]}'
+
+ audio = torch.from_numpy(audio).float().unsqueeze(0)
+ mel = torch.from_numpy(mel).float().squeeze(0)
+
+ if self.return_segments:
+ max_mel_start = mel.shape[1] - self.feat_frame_len
+ mel_start = random.randint(0, max_mel_start)
+ mel_end = mel_start + self.feat_frame_len
+ mel = mel[:, mel_start:mel_end]
+
+ audio_start = mel_start * self.hop_len
+ audio = audio[:, audio_start:audio_start +
+ self.seq_len]
+
+ if self.use_noise_augment and self.is_training and self.return_segments:
+ audio = audio + (1 / 32768) * torch.randn_like(audio)
+ return (mel, audio)
diff --git a/vocoder/datasets/preprocess.py b/vocoder/datasets/preprocess.py
new file mode 100644
index 00000000..be60c13a
--- /dev/null
+++ b/vocoder/datasets/preprocess.py
@@ -0,0 +1,37 @@
+import glob
+import os
+from pathlib import Path
+
+import numpy as np
+
+
+def find_wav_files(data_path):
+ wav_paths = glob.glob(os.path.join(data_path, '**', '*.wav'), recursive=True)
+ return wav_paths
+
+
+def find_feat_files(data_path):
+ feat_paths = glob.glob(os.path.join(data_path, '**', '*.npy'), recursive=True)
+ return feat_paths
+
+
+def load_wav_data(data_path, eval_split_size):
+ wav_paths = find_wav_files(data_path)
+ np.random.seed(0)
+ np.random.shuffle(wav_paths)
+ return wav_paths[:eval_split_size], wav_paths[eval_split_size:]
+
+
+def load_wav_feat_data(data_path, feat_path, eval_split_size):
+ wav_paths = sorted(find_wav_files(data_path))
+ feat_paths = sorted(find_feat_files(feat_path))
+ assert len(wav_paths) == len(feat_paths)
+ for wav, feat in zip(wav_paths, feat_paths):
+ wav_name = Path(wav).stem
+ feat_name = Path(feat).stem
+ assert wav_name == feat_name
+
+ items = list(zip(wav_paths, feat_paths))
+ np.random.seed(0)
+ np.random.shuffle(items)
+ return items[:eval_split_size], items[eval_split_size:]
diff --git a/vocoder/layers/__init__.py b/vocoder/layers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vocoder/layers/losses.py b/vocoder/layers/losses.py
new file mode 100644
index 00000000..431f7f45
--- /dev/null
+++ b/vocoder/layers/losses.py
@@ -0,0 +1,309 @@
+import torch
+
+from torch import nn
+from torch.nn import functional as F
+
+
+class TorchSTFT():
+ def __init__(self, n_fft, hop_length, win_length, window='hann_window'):
+ """ Torch based STFT operation """
+ self.n_fft = n_fft
+ self.hop_length = hop_length
+ self.win_length = win_length
+ self.window = getattr(torch, window)(win_length)
+
+ def __call__(self, x):
+ # B x D x T x 2
+ o = torch.stft(x,
+ self.n_fft,
+ self.hop_length,
+ self.win_length,
+ self.window,
+ center=True,
+ pad_mode="reflect", # compatible with audio.py
+ normalized=False,
+ onesided=True)
+ M = o[:, :, :, 0]
+ P = o[:, :, :, 1]
+ return torch.sqrt(torch.clamp(M ** 2 + P ** 2, min=1e-8))
+
+
+#################################
+# GENERATOR LOSSES
+#################################
+
+
+class STFTLoss(nn.Module):
+ """ Single scale STFT Loss """
+ def __init__(self, n_fft, hop_length, win_length):
+ super(STFTLoss, self).__init__()
+ self.n_fft = n_fft
+ self.hop_length = hop_length
+ self.win_length = win_length
+ self.stft = TorchSTFT(n_fft, hop_length, win_length)
+
+ def forward(self, y_hat, y):
+ y_hat_M = self.stft(y_hat)
+ y_M = self.stft(y)
+ # magnitude loss
+ loss_mag = F.l1_loss(torch.log(y_M), torch.log(y_hat_M))
+ # spectral convergence loss
+ loss_sc = torch.norm(y_M - y_hat_M, p="fro") / torch.norm(y_M, p="fro")
+ return loss_mag, loss_sc
+
+class MultiScaleSTFTLoss(torch.nn.Module):
+ """ Multi scale STFT loss """
+ def __init__(self,
+ n_ffts=(1024, 2048, 512),
+ hop_lengths=(120, 240, 50),
+ win_lengths=(600, 1200, 240)):
+ super(MultiScaleSTFTLoss, self).__init__()
+ self.loss_funcs = torch.nn.ModuleList()
+ for n_fft, hop_length, win_length in zip(n_ffts, hop_lengths, win_lengths):
+ self.loss_funcs.append(STFTLoss(n_fft, hop_length, win_length))
+
+ def forward(self, y_hat, y):
+ N = len(self.loss_funcs)
+ loss_sc = 0
+ loss_mag = 0
+ for f in self.loss_funcs:
+ lm, lsc = f(y_hat, y)
+ loss_mag += lm
+ loss_sc += lsc
+ loss_sc /= N
+ loss_mag /= N
+ return loss_mag, loss_sc
+
+
+class MultiScaleSubbandSTFTLoss(MultiScaleSTFTLoss):
+ """ Multiscale STFT loss for multi band model outputs """
+ # pylint: disable=no-self-use
+ def forward(self, y_hat, y):
+ y_hat = y_hat.view(-1, 1, y_hat.shape[2])
+ y = y.view(-1, 1, y.shape[2])
+ return super().forward(y_hat.squeeze(1), y.squeeze(1))
+
+
+class MSEGLoss(nn.Module):
+ """ Mean Squared Generator Loss """
+ # pylint: disable=no-self-use
+ def forward(self, score_real):
+ loss_fake = F.mse_loss(score_real, score_real.new_ones(score_real.shape))
+ return loss_fake
+
+
+class HingeGLoss(nn.Module):
+ """ Hinge Discriminator Loss """
+ # pylint: disable=no-self-use
+ def forward(self, score_real):
+ # TODO: this might be wrong
+ loss_fake = torch.mean(F.relu(1. - score_real))
+ return loss_fake
+
+
+##################################
+# DISCRIMINATOR LOSSES
+##################################
+
+
+class MSEDLoss(nn.Module):
+ """ Mean Squared Discriminator Loss """
+ def __init__(self,):
+ super(MSEDLoss, self).__init__()
+ self.loss_func = nn.MSELoss()
+
+ # pylint: disable=no-self-use
+ def forward(self, score_fake, score_real):
+ loss_real = self.loss_func(score_real, score_real.new_ones(score_real.shape))
+ loss_fake = self.loss_func(score_fake, score_fake.new_zeros(score_fake.shape))
+ loss_d = loss_real + loss_fake
+ return loss_d, loss_real, loss_fake
+
+
+class HingeDLoss(nn.Module):
+ """ Hinge Discriminator Loss """
+ # pylint: disable=no-self-use
+ def forward(self, score_fake, score_real):
+ loss_real = torch.mean(F.relu(1. - score_real))
+ loss_fake = torch.mean(F.relu(1. + score_fake))
+ loss_d = loss_real + loss_fake
+ return loss_d, loss_real, loss_fake
+
+
+class MelganFeatureLoss(nn.Module):
+ def __init__(self,):
+ super(MelganFeatureLoss, self).__init__()
+ self.loss_func = nn.L1Loss()
+
+ # pylint: disable=no-self-use
+ def forward(self, fake_feats, real_feats):
+ loss_feats = 0
+ for fake_feat, real_feat in zip(fake_feats, real_feats):
+ loss_feats += self.loss_func(fake_feat, real_feat)
+ loss_feats /= len(fake_feats) + len(real_feats)
+ return loss_feats
+
+
+#####################################
+# LOSS WRAPPERS
+#####################################
+
+
+def _apply_G_adv_loss(scores_fake, loss_func):
+ """ Compute G adversarial loss function
+ and normalize values """
+ adv_loss = 0
+ if isinstance(scores_fake, list):
+ for score_fake in scores_fake:
+ fake_loss = loss_func(score_fake)
+ adv_loss += fake_loss
+ adv_loss /= len(scores_fake)
+ else:
+ fake_loss = loss_func(scores_fake)
+ adv_loss = fake_loss
+ return adv_loss
+
+
+def _apply_D_loss(scores_fake, scores_real, loss_func):
+ """ Compute D loss func and normalize loss values """
+ loss = 0
+ real_loss = 0
+ fake_loss = 0
+ if isinstance(scores_fake, list):
+ # multi-scale loss
+ for score_fake, score_real in zip(scores_fake, scores_real):
+ total_loss, real_loss, fake_loss = loss_func(score_fake=score_fake, score_real=score_real)
+ loss += total_loss
+ real_loss += real_loss
+ fake_loss += fake_loss
+ # normalize loss values with number of scales
+ loss /= len(scores_fake)
+ real_loss /= len(scores_real)
+ fake_loss /= len(scores_fake)
+ else:
+ # single scale loss
+ total_loss, real_loss, fake_loss = loss_func(scores_fake, scores_real)
+ loss = total_loss
+ return loss, real_loss, fake_loss
+
+
+##################################
+# MODEL LOSSES
+##################################
+
+
+class GeneratorLoss(nn.Module):
+ def __init__(self, C):
+ """ Compute Generator Loss values depending on training
+ configuration """
+ super(GeneratorLoss, self).__init__()
+ assert not(C.use_mse_gan_loss and C.use_hinge_gan_loss),\
+ " [!] Cannot use HingeGANLoss and MSEGANLoss together."
+
+ self.use_stft_loss = C.use_stft_loss
+ self.use_subband_stft_loss = C.use_subband_stft_loss
+ self.use_mse_gan_loss = C.use_mse_gan_loss
+ self.use_hinge_gan_loss = C.use_hinge_gan_loss
+ self.use_feat_match_loss = C.use_feat_match_loss
+
+ self.stft_loss_weight = C.stft_loss_weight
+ self.subband_stft_loss_weight = C.subband_stft_loss_weight
+ self.mse_gan_loss_weight = C.mse_G_loss_weight
+ self.hinge_gan_loss_weight = C.hinge_G_loss_weight
+ self.feat_match_loss_weight = C.feat_match_loss_weight
+
+ if C.use_stft_loss:
+ self.stft_loss = MultiScaleSTFTLoss(**C.stft_loss_params)
+ if C.use_subband_stft_loss:
+ self.subband_stft_loss = MultiScaleSubbandSTFTLoss(**C.subband_stft_loss_params)
+ if C.use_mse_gan_loss:
+ self.mse_loss = MSEGLoss()
+ if C.use_hinge_gan_loss:
+ self.hinge_loss = HingeGLoss()
+ if C.use_feat_match_loss:
+ self.feat_match_loss = MelganFeatureLoss()
+
+ def forward(self, y_hat=None, y=None, scores_fake=None, feats_fake=None, feats_real=None, y_hat_sub=None, y_sub=None):
+ gen_loss = 0
+ adv_loss = 0
+ return_dict = {}
+
+ # STFT Loss
+ if self.use_stft_loss:
+ stft_loss_mg, stft_loss_sc = self.stft_loss(y_hat.squeeze(1), y.squeeze(1))
+ return_dict['G_stft_loss_mg'] = stft_loss_mg
+ return_dict['G_stft_loss_sc'] = stft_loss_sc
+ gen_loss += self.stft_loss_weight * (stft_loss_mg + stft_loss_sc)
+
+ # subband STFT Loss
+ if self.use_subband_stft_loss:
+ subband_stft_loss_mg, subband_stft_loss_sc = self.subband_stft_loss(y_hat_sub, y_sub)
+ return_dict['G_subband_stft_loss_mg'] = subband_stft_loss_mg
+ return_dict['G_subband_stft_loss_sc'] = subband_stft_loss_sc
+ gen_loss += self.subband_stft_loss_weight * (subband_stft_loss_mg + subband_stft_loss_sc)
+
+ # multiscale MSE adversarial loss
+ if self.use_mse_gan_loss and scores_fake is not None:
+ mse_fake_loss = _apply_G_adv_loss(scores_fake, self.mse_loss)
+ return_dict['G_mse_fake_loss'] = mse_fake_loss
+ adv_loss += self.mse_gan_loss_weight * mse_fake_loss
+
+ # multiscale Hinge adversarial loss
+ if self.use_hinge_gan_loss and not scores_fake is not None:
+ hinge_fake_loss = _apply_G_adv_loss(scores_fake, self.hinge_loss)
+ return_dict['G_hinge_fake_loss'] = hinge_fake_loss
+ adv_loss += self.hinge_gan_loss_weight * hinge_fake_loss
+
+ # Feature Matching Loss
+ if self.use_feat_match_loss and not feats_fake:
+ feat_match_loss = self.feat_match_loss(feats_fake, feats_real)
+ return_dict['G_feat_match_loss'] = feat_match_loss
+ adv_loss += self.feat_match_loss_weight * feat_match_loss
+ return_dict['G_loss'] = gen_loss + adv_loss
+ return_dict['G_gen_loss'] = gen_loss
+ return_dict['G_adv_loss'] = adv_loss
+ return return_dict
+
+
+class DiscriminatorLoss(nn.Module):
+ """ Compute Discriminator Loss values depending on training
+ configuration """
+ def __init__(self, C):
+ super(DiscriminatorLoss, self).__init__()
+ assert not(C.use_mse_gan_loss and C.use_hinge_gan_loss),\
+ " [!] Cannot use HingeGANLoss and MSEGANLoss together."
+
+ self.use_mse_gan_loss = C.use_mse_gan_loss
+ self.use_hinge_gan_loss = C.use_hinge_gan_loss
+
+ if C.use_mse_gan_loss:
+ self.mse_loss = MSEDLoss()
+ if C.use_hinge_gan_loss:
+ self.hinge_loss = HingeDLoss()
+
+ def forward(self, scores_fake, scores_real):
+ loss = 0
+ return_dict = {}
+
+ if self.use_mse_gan_loss:
+ mse_D_loss, mse_D_real_loss, mse_D_fake_loss = _apply_D_loss(
+ scores_fake=scores_fake,
+ scores_real=scores_real,
+ loss_func=self.mse_loss)
+ return_dict['D_mse_gan_loss'] = mse_D_loss
+ return_dict['D_mse_gan_real_loss'] = mse_D_real_loss
+ return_dict['D_mse_gan_fake_loss'] = mse_D_fake_loss
+ loss += mse_D_loss
+
+ if self.use_hinge_gan_loss:
+ hinge_D_loss, hinge_D_real_loss, hinge_D_fake_loss = _apply_D_loss(
+ scores_fake=scores_fake,
+ scores_real=scores_real,
+ loss_func=self.hinge_loss)
+ return_dict['D_hinge_gan_loss'] = hinge_D_loss
+ return_dict['D_hinge_gan_real_loss'] = hinge_D_real_loss
+ return_dict['D_hinge_gan_fake_loss'] = hinge_D_fake_loss
+ loss += hinge_D_loss
+
+ return_dict['D_loss'] = loss
+ return return_dict
\ No newline at end of file
diff --git a/vocoder/layers/melgan.py b/vocoder/layers/melgan.py
new file mode 100644
index 00000000..58c12a2e
--- /dev/null
+++ b/vocoder/layers/melgan.py
@@ -0,0 +1,45 @@
+from torch import nn
+from torch.nn.utils import weight_norm
+
+
+class ResidualStack(nn.Module):
+ def __init__(self, channels, num_res_blocks, kernel_size):
+ super(ResidualStack, self).__init__()
+
+ assert (kernel_size - 1) % 2 == 0, " [!] kernel_size has to be odd."
+ base_padding = (kernel_size - 1) // 2
+
+ self.blocks = nn.ModuleList()
+ for idx in range(num_res_blocks):
+ layer_kernel_size = kernel_size
+ layer_dilation = layer_kernel_size**idx
+ layer_padding = base_padding * layer_dilation
+ self.blocks += [nn.Sequential(
+ nn.LeakyReLU(0.2),
+ nn.ReflectionPad1d(layer_padding),
+ weight_norm(
+ nn.Conv1d(channels,
+ channels,
+ kernel_size=kernel_size,
+ dilation=layer_dilation,
+ bias=True)),
+ nn.LeakyReLU(0.2),
+ weight_norm(
+ nn.Conv1d(channels, channels, kernel_size=1, bias=True)),
+ )]
+
+ self.shortcuts = nn.ModuleList([
+ weight_norm(nn.Conv1d(channels, channels, kernel_size=1,
+ bias=True)) for i in range(num_res_blocks)
+ ])
+
+ def forward(self, x):
+ for block, shortcut in zip(self.blocks, self.shortcuts):
+ x = shortcut(x) + block(x)
+ return x
+
+ def remove_weight_norm(self):
+ for block, shortcut in zip(self.blocks, self.shortcuts):
+ nn.utils.remove_weight_norm(block[2])
+ nn.utils.remove_weight_norm(block[4])
+ nn.utils.remove_weight_norm(shortcut)
diff --git a/vocoder/layers/pqmf.py b/vocoder/layers/pqmf.py
new file mode 100644
index 00000000..ef5a3507
--- /dev/null
+++ b/vocoder/layers/pqmf.py
@@ -0,0 +1,56 @@
+import numpy as np
+import torch
+import torch.nn.functional as F
+
+from scipy import signal as sig
+
+
+# adapted from
+# https://github.com/kan-bayashi/ParallelWaveGAN/tree/master/parallel_wavegan
+class PQMF(torch.nn.Module):
+ def __init__(self, N=4, taps=62, cutoff=0.15, beta=9.0):
+ super(PQMF, self).__init__()
+
+ self.N = N
+ self.taps = taps
+ self.cutoff = cutoff
+ self.beta = beta
+
+ QMF = sig.firwin(taps + 1, cutoff, window=('kaiser', beta))
+ H = np.zeros((N, len(QMF)))
+ G = np.zeros((N, len(QMF)))
+ for k in range(N):
+ constant_factor = (2 * k + 1) * (np.pi /
+ (2 * N)) * (np.arange(taps + 1) -
+ ((taps - 1) / 2))
+ phase = (-1)**k * np.pi / 4
+ H[k] = 2 * QMF * np.cos(constant_factor + phase)
+
+ G[k] = 2 * QMF * np.cos(constant_factor - phase)
+
+ H = torch.from_numpy(H[:, None, :]).float()
+ G = torch.from_numpy(G[None, :, :]).float()
+
+ self.register_buffer("H", H)
+ self.register_buffer("G", G)
+
+ updown_filter = torch.zeros((N, N, N)).float()
+ for k in range(N):
+ updown_filter[k, k, 0] = 1.0
+ self.register_buffer("updown_filter", updown_filter)
+ self.N = N
+
+ self.pad_fn = torch.nn.ConstantPad1d(taps // 2, 0.0)
+
+ def forward(self, x):
+ return self.analysis(x)
+
+ def analysis(self, x):
+ return F.conv1d(x, self.H, padding=self.taps // 2, stride=self.N)
+
+ def synthesis(self, x):
+ x = F.conv_transpose1d(x,
+ self.updown_filter * self.N,
+ stride=self.N)
+ x = F.conv1d(x, self.G, padding=self.taps // 2)
+ return x
diff --git a/vocoder/layers/qmf.dat b/vocoder/layers/qmf.dat
new file mode 100644
index 00000000..17eab137
--- /dev/null
+++ b/vocoder/layers/qmf.dat
@@ -0,0 +1,640 @@
+ 0.0000000e+000
+ -5.5252865e-004
+ -5.6176926e-004
+ -4.9475181e-004
+ -4.8752280e-004
+ -4.8937912e-004
+ -5.0407143e-004
+ -5.2265643e-004
+ -5.4665656e-004
+ -5.6778026e-004
+ -5.8709305e-004
+ -6.1327474e-004
+ -6.3124935e-004
+ -6.5403334e-004
+ -6.7776908e-004
+ -6.9416146e-004
+ -7.1577365e-004
+ -7.2550431e-004
+ -7.4409419e-004
+ -7.4905981e-004
+ -7.6813719e-004
+ -7.7248486e-004
+ -7.8343323e-004
+ -7.7798695e-004
+ -7.8036647e-004
+ -7.8014496e-004
+ -7.7579773e-004
+ -7.6307936e-004
+ -7.5300014e-004
+ -7.3193572e-004
+ -7.2153920e-004
+ -6.9179375e-004
+ -6.6504151e-004
+ -6.3415949e-004
+ -5.9461189e-004
+ -5.5645764e-004
+ -5.1455722e-004
+ -4.6063255e-004
+ -4.0951215e-004
+ -3.5011759e-004
+ -2.8969812e-004
+ -2.0983373e-004
+ -1.4463809e-004
+ -6.1733441e-005
+ 1.3494974e-005
+ 1.0943831e-004
+ 2.0430171e-004
+ 2.9495311e-004
+ 4.0265402e-004
+ 5.1073885e-004
+ 6.2393761e-004
+ 7.4580259e-004
+ 8.6084433e-004
+ 9.8859883e-004
+ 1.1250155e-003
+ 1.2577885e-003
+ 1.3902495e-003
+ 1.5443220e-003
+ 1.6868083e-003
+ 1.8348265e-003
+ 1.9841141e-003
+ 2.1461584e-003
+ 2.3017255e-003
+ 2.4625617e-003
+ 2.6201759e-003
+ 2.7870464e-003
+ 2.9469448e-003
+ 3.1125421e-003
+ 3.2739613e-003
+ 3.4418874e-003
+ 3.6008268e-003
+ 3.7603923e-003
+ 3.9207432e-003
+ 4.0819753e-003
+ 4.2264269e-003
+ 4.3730720e-003
+ 4.5209853e-003
+ 4.6606461e-003
+ 4.7932561e-003
+ 4.9137604e-003
+ 5.0393023e-003
+ 5.1407354e-003
+ 5.2461166e-003
+ 5.3471681e-003
+ 5.4196776e-003
+ 5.4876040e-003
+ 5.5475715e-003
+ 5.5938023e-003
+ 5.6220643e-003
+ 5.6455197e-003
+ 5.6389200e-003
+ 5.6266114e-003
+ 5.5917129e-003
+ 5.5404364e-003
+ 5.4753783e-003
+ 5.3838976e-003
+ 5.2715759e-003
+ 5.1382275e-003
+ 4.9839688e-003
+ 4.8109469e-003
+ 4.6039530e-003
+ 4.3801862e-003
+ 4.1251642e-003
+ 3.8456408e-003
+ 3.5401247e-003
+ 3.2091886e-003
+ 2.8446758e-003
+ 2.4508540e-003
+ 2.0274176e-003
+ 1.5784683e-003
+ 1.0902329e-003
+ 5.8322642e-004
+ 2.7604519e-005
+ -5.4642809e-004
+ -1.1568136e-003
+ -1.8039473e-003
+ -2.4826724e-003
+ -3.1933778e-003
+ -3.9401124e-003
+ -4.7222596e-003
+ -5.5337211e-003
+ -6.3792293e-003
+ -7.2615817e-003
+ -8.1798233e-003
+ -9.1325330e-003
+ -1.0115022e-002
+ -1.1131555e-002
+ -1.2185000e-002
+ -1.3271822e-002
+ -1.4390467e-002
+ -1.5540555e-002
+ -1.6732471e-002
+ -1.7943338e-002
+ -1.9187243e-002
+ -2.0453179e-002
+ -2.1746755e-002
+ -2.3068017e-002
+ -2.4416099e-002
+ -2.5787585e-002
+ -2.7185943e-002
+ -2.8607217e-002
+ -3.0050266e-002
+ -3.1501761e-002
+ -3.2975408e-002
+ -3.4462095e-002
+ -3.5969756e-002
+ -3.7481285e-002
+ -3.9005368e-002
+ -4.0534917e-002
+ -4.2064909e-002
+ -4.3609754e-002
+ -4.5148841e-002
+ -4.6684303e-002
+ -4.8216572e-002
+ -4.9738576e-002
+ -5.1255616e-002
+ -5.2763075e-002
+ -5.4245277e-002
+ -5.5717365e-002
+ -5.7161645e-002
+ -5.8591568e-002
+ -5.9983748e-002
+ -6.1345517e-002
+ -6.2685781e-002
+ -6.3971590e-002
+ -6.5224711e-002
+ -6.6436751e-002
+ -6.7607599e-002
+ -6.8704383e-002
+ -6.9763024e-002
+ -7.0762871e-002
+ -7.1700267e-002
+ -7.2568258e-002
+ -7.3362026e-002
+ -7.4100364e-002
+ -7.4745256e-002
+ -7.5313734e-002
+ -7.5800836e-002
+ -7.6199248e-002
+ -7.6499217e-002
+ -7.6709349e-002
+ -7.6817398e-002
+ -7.6823001e-002
+ -7.6720492e-002
+ -7.6505072e-002
+ -7.6174832e-002
+ -7.5730576e-002
+ -7.5157626e-002
+ -7.4466439e-002
+ -7.3640601e-002
+ -7.2677464e-002
+ -7.1582636e-002
+ -7.0353307e-002
+ -6.8966401e-002
+ -6.7452502e-002
+ -6.5769067e-002
+ -6.3944481e-002
+ -6.1960278e-002
+ -5.9816657e-002
+ -5.7515269e-002
+ -5.5046003e-002
+ -5.2409382e-002
+ -4.9597868e-002
+ -4.6630331e-002
+ -4.3476878e-002
+ -4.0145828e-002
+ -3.6641812e-002
+ -3.2958393e-002
+ -2.9082401e-002
+ -2.5030756e-002
+ -2.0799707e-002
+ -1.6370126e-002
+ -1.1762383e-002
+ -6.9636862e-003
+ -1.9765601e-003
+ 3.2086897e-003
+ 8.5711749e-003
+ 1.4128883e-002
+ 1.9883413e-002
+ 2.5822729e-002
+ 3.1953127e-002
+ 3.8277657e-002
+ 4.4780682e-002
+ 5.1480418e-002
+ 5.8370533e-002
+ 6.5440985e-002
+ 7.2694330e-002
+ 8.0137293e-002
+ 8.7754754e-002
+ 9.5553335e-002
+ 1.0353295e-001
+ 1.1168269e-001
+ 1.2000780e-001
+ 1.2850029e-001
+ 1.3715518e-001
+ 1.4597665e-001
+ 1.5496071e-001
+ 1.6409589e-001
+ 1.7338082e-001
+ 1.8281725e-001
+ 1.9239667e-001
+ 2.0212502e-001
+ 2.1197359e-001
+ 2.2196527e-001
+ 2.3206909e-001
+ 2.4230169e-001
+ 2.5264803e-001
+ 2.6310533e-001
+ 2.7366340e-001
+ 2.8432142e-001
+ 2.9507167e-001
+ 3.0590986e-001
+ 3.1682789e-001
+ 3.2781137e-001
+ 3.3887227e-001
+ 3.4999141e-001
+ 3.6115899e-001
+ 3.7237955e-001
+ 3.8363500e-001
+ 3.9492118e-001
+ 4.0623177e-001
+ 4.1756969e-001
+ 4.2891199e-001
+ 4.4025538e-001
+ 4.5159965e-001
+ 4.6293081e-001
+ 4.7424532e-001
+ 4.8552531e-001
+ 4.9677083e-001
+ 5.0798175e-001
+ 5.1912350e-001
+ 5.3022409e-001
+ 5.4125534e-001
+ 5.5220513e-001
+ 5.6307891e-001
+ 5.7385241e-001
+ 5.8454032e-001
+ 5.9511231e-001
+ 6.0557835e-001
+ 6.1591099e-001
+ 6.2612427e-001
+ 6.3619801e-001
+ 6.4612697e-001
+ 6.5590163e-001
+ 6.6551399e-001
+ 6.7496632e-001
+ 6.8423533e-001
+ 6.9332824e-001
+ 7.0223887e-001
+ 7.1094104e-001
+ 7.1944626e-001
+ 7.2774489e-001
+ 7.3582118e-001
+ 7.4368279e-001
+ 7.5131375e-001
+ 7.5870808e-001
+ 7.6586749e-001
+ 7.7277809e-001
+ 7.7942875e-001
+ 7.8583531e-001
+ 7.9197358e-001
+ 7.9784664e-001
+ 8.0344858e-001
+ 8.0876950e-001
+ 8.1381913e-001
+ 8.1857760e-001
+ 8.2304199e-001
+ 8.2722753e-001
+ 8.3110385e-001
+ 8.3469374e-001
+ 8.3797173e-001
+ 8.4095414e-001
+ 8.4362383e-001
+ 8.4598185e-001
+ 8.4803158e-001
+ 8.4978052e-001
+ 8.5119715e-001
+ 8.5230470e-001
+ 8.5310209e-001
+ 8.5357206e-001
+ 8.5373856e-001
+ 8.5357206e-001
+ 8.5310209e-001
+ 8.5230470e-001
+ 8.5119715e-001
+ 8.4978052e-001
+ 8.4803158e-001
+ 8.4598185e-001
+ 8.4362383e-001
+ 8.4095414e-001
+ 8.3797173e-001
+ 8.3469374e-001
+ 8.3110385e-001
+ 8.2722753e-001
+ 8.2304199e-001
+ 8.1857760e-001
+ 8.1381913e-001
+ 8.0876950e-001
+ 8.0344858e-001
+ 7.9784664e-001
+ 7.9197358e-001
+ 7.8583531e-001
+ 7.7942875e-001
+ 7.7277809e-001
+ 7.6586749e-001
+ 7.5870808e-001
+ 7.5131375e-001
+ 7.4368279e-001
+ 7.3582118e-001
+ 7.2774489e-001
+ 7.1944626e-001
+ 7.1094104e-001
+ 7.0223887e-001
+ 6.9332824e-001
+ 6.8423533e-001
+ 6.7496632e-001
+ 6.6551399e-001
+ 6.5590163e-001
+ 6.4612697e-001
+ 6.3619801e-001
+ 6.2612427e-001
+ 6.1591099e-001
+ 6.0557835e-001
+ 5.9511231e-001
+ 5.8454032e-001
+ 5.7385241e-001
+ 5.6307891e-001
+ 5.5220513e-001
+ 5.4125534e-001
+ 5.3022409e-001
+ 5.1912350e-001
+ 5.0798175e-001
+ 4.9677083e-001
+ 4.8552531e-001
+ 4.7424532e-001
+ 4.6293081e-001
+ 4.5159965e-001
+ 4.4025538e-001
+ 4.2891199e-001
+ 4.1756969e-001
+ 4.0623177e-001
+ 3.9492118e-001
+ 3.8363500e-001
+ 3.7237955e-001
+ 3.6115899e-001
+ 3.4999141e-001
+ 3.3887227e-001
+ 3.2781137e-001
+ 3.1682789e-001
+ 3.0590986e-001
+ 2.9507167e-001
+ 2.8432142e-001
+ 2.7366340e-001
+ 2.6310533e-001
+ 2.5264803e-001
+ 2.4230169e-001
+ 2.3206909e-001
+ 2.2196527e-001
+ 2.1197359e-001
+ 2.0212502e-001
+ 1.9239667e-001
+ 1.8281725e-001
+ 1.7338082e-001
+ 1.6409589e-001
+ 1.5496071e-001
+ 1.4597665e-001
+ 1.3715518e-001
+ 1.2850029e-001
+ 1.2000780e-001
+ 1.1168269e-001
+ 1.0353295e-001
+ 9.5553335e-002
+ 8.7754754e-002
+ 8.0137293e-002
+ 7.2694330e-002
+ 6.5440985e-002
+ 5.8370533e-002
+ 5.1480418e-002
+ 4.4780682e-002
+ 3.8277657e-002
+ 3.1953127e-002
+ 2.5822729e-002
+ 1.9883413e-002
+ 1.4128883e-002
+ 8.5711749e-003
+ 3.2086897e-003
+ -1.9765601e-003
+ -6.9636862e-003
+ -1.1762383e-002
+ -1.6370126e-002
+ -2.0799707e-002
+ -2.5030756e-002
+ -2.9082401e-002
+ -3.2958393e-002
+ -3.6641812e-002
+ -4.0145828e-002
+ -4.3476878e-002
+ -4.6630331e-002
+ -4.9597868e-002
+ -5.2409382e-002
+ -5.5046003e-002
+ -5.7515269e-002
+ -5.9816657e-002
+ -6.1960278e-002
+ -6.3944481e-002
+ -6.5769067e-002
+ -6.7452502e-002
+ -6.8966401e-002
+ -7.0353307e-002
+ -7.1582636e-002
+ -7.2677464e-002
+ -7.3640601e-002
+ -7.4466439e-002
+ -7.5157626e-002
+ -7.5730576e-002
+ -7.6174832e-002
+ -7.6505072e-002
+ -7.6720492e-002
+ -7.6823001e-002
+ -7.6817398e-002
+ -7.6709349e-002
+ -7.6499217e-002
+ -7.6199248e-002
+ -7.5800836e-002
+ -7.5313734e-002
+ -7.4745256e-002
+ -7.4100364e-002
+ -7.3362026e-002
+ -7.2568258e-002
+ -7.1700267e-002
+ -7.0762871e-002
+ -6.9763024e-002
+ -6.8704383e-002
+ -6.7607599e-002
+ -6.6436751e-002
+ -6.5224711e-002
+ -6.3971590e-002
+ -6.2685781e-002
+ -6.1345517e-002
+ -5.9983748e-002
+ -5.8591568e-002
+ -5.7161645e-002
+ -5.5717365e-002
+ -5.4245277e-002
+ -5.2763075e-002
+ -5.1255616e-002
+ -4.9738576e-002
+ -4.8216572e-002
+ -4.6684303e-002
+ -4.5148841e-002
+ -4.3609754e-002
+ -4.2064909e-002
+ -4.0534917e-002
+ -3.9005368e-002
+ -3.7481285e-002
+ -3.5969756e-002
+ -3.4462095e-002
+ -3.2975408e-002
+ -3.1501761e-002
+ -3.0050266e-002
+ -2.8607217e-002
+ -2.7185943e-002
+ -2.5787585e-002
+ -2.4416099e-002
+ -2.3068017e-002
+ -2.1746755e-002
+ -2.0453179e-002
+ -1.9187243e-002
+ -1.7943338e-002
+ -1.6732471e-002
+ -1.5540555e-002
+ -1.4390467e-002
+ -1.3271822e-002
+ -1.2185000e-002
+ -1.1131555e-002
+ -1.0115022e-002
+ -9.1325330e-003
+ -8.1798233e-003
+ -7.2615817e-003
+ -6.3792293e-003
+ -5.5337211e-003
+ -4.7222596e-003
+ -3.9401124e-003
+ -3.1933778e-003
+ -2.4826724e-003
+ -1.8039473e-003
+ -1.1568136e-003
+ -5.4642809e-004
+ 2.7604519e-005
+ 5.8322642e-004
+ 1.0902329e-003
+ 1.5784683e-003
+ 2.0274176e-003
+ 2.4508540e-003
+ 2.8446758e-003
+ 3.2091886e-003
+ 3.5401247e-003
+ 3.8456408e-003
+ 4.1251642e-003
+ 4.3801862e-003
+ 4.6039530e-003
+ 4.8109469e-003
+ 4.9839688e-003
+ 5.1382275e-003
+ 5.2715759e-003
+ 5.3838976e-003
+ 5.4753783e-003
+ 5.5404364e-003
+ 5.5917129e-003
+ 5.6266114e-003
+ 5.6389200e-003
+ 5.6455197e-003
+ 5.6220643e-003
+ 5.5938023e-003
+ 5.5475715e-003
+ 5.4876040e-003
+ 5.4196776e-003
+ 5.3471681e-003
+ 5.2461166e-003
+ 5.1407354e-003
+ 5.0393023e-003
+ 4.9137604e-003
+ 4.7932561e-003
+ 4.6606461e-003
+ 4.5209853e-003
+ 4.3730720e-003
+ 4.2264269e-003
+ 4.0819753e-003
+ 3.9207432e-003
+ 3.7603923e-003
+ 3.6008268e-003
+ 3.4418874e-003
+ 3.2739613e-003
+ 3.1125421e-003
+ 2.9469448e-003
+ 2.7870464e-003
+ 2.6201759e-003
+ 2.4625617e-003
+ 2.3017255e-003
+ 2.1461584e-003
+ 1.9841141e-003
+ 1.8348265e-003
+ 1.6868083e-003
+ 1.5443220e-003
+ 1.3902495e-003
+ 1.2577885e-003
+ 1.1250155e-003
+ 9.8859883e-004
+ 8.6084433e-004
+ 7.4580259e-004
+ 6.2393761e-004
+ 5.1073885e-004
+ 4.0265402e-004
+ 2.9495311e-004
+ 2.0430171e-004
+ 1.0943831e-004
+ 1.3494974e-005
+ -6.1733441e-005
+ -1.4463809e-004
+ -2.0983373e-004
+ -2.8969812e-004
+ -3.5011759e-004
+ -4.0951215e-004
+ -4.6063255e-004
+ -5.1455722e-004
+ -5.5645764e-004
+ -5.9461189e-004
+ -6.3415949e-004
+ -6.6504151e-004
+ -6.9179375e-004
+ -7.2153920e-004
+ -7.3193572e-004
+ -7.5300014e-004
+ -7.6307936e-004
+ -7.7579773e-004
+ -7.8014496e-004
+ -7.8036647e-004
+ -7.7798695e-004
+ -7.8343323e-004
+ -7.7248486e-004
+ -7.6813719e-004
+ -7.4905981e-004
+ -7.4409419e-004
+ -7.2550431e-004
+ -7.1577365e-004
+ -6.9416146e-004
+ -6.7776908e-004
+ -6.5403334e-004
+ -6.3124935e-004
+ -6.1327474e-004
+ -5.8709305e-004
+ -5.6778026e-004
+ -5.4665656e-004
+ -5.2265643e-004
+ -5.0407143e-004
+ -4.8937912e-004
+ -4.8752280e-004
+ -4.9475181e-004
+ -5.6176926e-004
+ -5.5252865e-004
diff --git a/vocoder/models/__init__.py b/vocoder/models/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vocoder/models/melgan_discriminator.py b/vocoder/models/melgan_discriminator.py
new file mode 100644
index 00000000..3847babb
--- /dev/null
+++ b/vocoder/models/melgan_discriminator.py
@@ -0,0 +1,78 @@
+import numpy as np
+from torch import nn
+from torch.nn.utils import weight_norm
+
+
+class MelganDiscriminator(nn.Module):
+ def __init__(self,
+ in_channels=1,
+ out_channels=1,
+ kernel_sizes=(5, 3),
+ base_channels=16,
+ max_channels=1024,
+ downsample_factors=(4, 4, 4, 4)):
+ super(MelganDiscriminator, self).__init__()
+ self.layers = nn.ModuleList()
+
+ layer_kernel_size = np.prod(kernel_sizes)
+ layer_padding = (layer_kernel_size - 1) // 2
+
+ # initial layer
+ self.layers += [
+ nn.Sequential(
+ nn.ReflectionPad1d(layer_padding),
+ weight_norm(
+ nn.Conv1d(in_channels,
+ base_channels,
+ layer_kernel_size,
+ stride=1)), nn.LeakyReLU(0.2, inplace=True))
+ ]
+
+ # downsampling layers
+ layer_in_channels = base_channels
+ for downsample_factor in downsample_factors:
+ layer_out_channels = min(layer_in_channels * downsample_factor,
+ max_channels)
+ layer_kernel_size = downsample_factor * 10 + 1
+ layer_padding = (layer_kernel_size - 1) // 2
+ layer_groups = layer_in_channels // 4
+ self.layers += [
+ nn.Sequential(
+ weight_norm(
+ nn.Conv1d(layer_in_channels,
+ layer_out_channels,
+ kernel_size=layer_kernel_size,
+ stride=downsample_factor,
+ padding=layer_padding,
+ groups=layer_groups)),
+ nn.LeakyReLU(0.2, inplace=True))
+ ]
+ layer_in_channels = layer_out_channels
+
+ # last 2 layers
+ layer_padding1 = (kernel_sizes[0] - 1) // 2
+ layer_padding2 = (kernel_sizes[1] - 1) // 2
+ self.layers += [
+ nn.Sequential(
+ weight_norm(
+ nn.Conv1d(layer_out_channels,
+ layer_out_channels,
+ kernel_size=kernel_sizes[0],
+ stride=1,
+ padding=layer_padding1)),
+ nn.LeakyReLU(0.2, inplace=True),
+ ),
+ weight_norm(
+ nn.Conv1d(layer_out_channels,
+ out_channels,
+ kernel_size=kernel_sizes[1],
+ stride=1,
+ padding=layer_padding2)),
+ ]
+
+ def forward(self, x):
+ feats = []
+ for layer in self.layers:
+ x = layer(x)
+ feats.append(x)
+ return x, feats
diff --git a/vocoder/models/melgan_generator.py b/vocoder/models/melgan_generator.py
new file mode 100644
index 00000000..01b52ea8
--- /dev/null
+++ b/vocoder/models/melgan_generator.py
@@ -0,0 +1,98 @@
+import torch
+from torch import nn
+from torch.nn.utils import weight_norm
+
+from TTS.vocoder.layers.melgan import ResidualStack
+
+
+class MelganGenerator(nn.Module):
+ def __init__(self,
+ in_channels=80,
+ out_channels=1,
+ proj_kernel=7,
+ base_channels=512,
+ upsample_factors=(8, 8, 2, 2),
+ res_kernel=3,
+ num_res_blocks=3):
+ super(MelganGenerator, self).__init__()
+
+ # assert model parameters
+ assert (proj_kernel -
+ 1) % 2 == 0, " [!] proj_kernel should be an odd number."
+
+ # setup additional model parameters
+ base_padding = (proj_kernel - 1) // 2
+ act_slope = 0.2
+ self.inference_padding = 2
+
+ # initial layer
+ layers = []
+ layers += [
+ nn.ReflectionPad1d(base_padding),
+ weight_norm(
+ nn.Conv1d(in_channels,
+ base_channels,
+ kernel_size=proj_kernel,
+ stride=1,
+ bias=True))
+ ]
+
+ # upsampling layers and residual stacks
+ for idx, upsample_factor in enumerate(upsample_factors):
+ layer_in_channels = base_channels // (2**idx)
+ layer_out_channels = base_channels // (2**(idx + 1))
+ layer_filter_size = upsample_factor * 2
+ layer_stride = upsample_factor
+ layer_output_padding = upsample_factor % 2
+ layer_padding = upsample_factor // 2 + layer_output_padding
+ layers += [
+ nn.LeakyReLU(act_slope),
+ weight_norm(
+ nn.ConvTranspose1d(layer_in_channels,
+ layer_out_channels,
+ layer_filter_size,
+ stride=layer_stride,
+ padding=layer_padding,
+ output_padding=layer_output_padding,
+ bias=True)),
+ ResidualStack(
+ channels=layer_out_channels,
+ num_res_blocks=num_res_blocks,
+ kernel_size=res_kernel
+ )
+ ]
+
+ layers += [nn.LeakyReLU(act_slope)]
+
+ # final layer
+ layers += [
+ nn.ReflectionPad1d(base_padding),
+ weight_norm(
+ nn.Conv1d(layer_out_channels,
+ out_channels,
+ proj_kernel,
+ stride=1,
+ bias=True)),
+ nn.Tanh()
+ ]
+ self.layers = nn.Sequential(*layers)
+
+ def forward(self, c):
+ return self.layers(c)
+
+ def inference(self, c):
+ c = c.to(self.layers[1].weight.device)
+ c = torch.nn.functional.pad(
+ c,
+ (self.inference_padding, self.inference_padding),
+ 'replicate')
+ return self.layers(c)
+
+ def remove_weight_norm(self):
+ for _, layer in enumerate(self.layers):
+ if len(layer.state_dict()) != 0:
+ try:
+ nn.utils.remove_weight_norm(layer)
+ except ValueError:
+ layer.remove_weight_norm()
+
diff --git a/vocoder/models/melgan_multiscale_discriminator.py b/vocoder/models/melgan_multiscale_discriminator.py
new file mode 100644
index 00000000..dbcc1f30
--- /dev/null
+++ b/vocoder/models/melgan_multiscale_discriminator.py
@@ -0,0 +1,41 @@
+from torch import nn
+
+from TTS.vocoder.models.melgan_discriminator import MelganDiscriminator
+
+
+class MelganMultiscaleDiscriminator(nn.Module):
+ def __init__(self,
+ in_channels=1,
+ out_channels=1,
+ num_scales=3,
+ kernel_sizes=(5, 3),
+ base_channels=16,
+ max_channels=1024,
+ downsample_factors=(4, 4, 4),
+ pooling_kernel_size=4,
+ pooling_stride=2,
+ pooling_padding=1):
+ super(MelganMultiscaleDiscriminator, self).__init__()
+
+ self.discriminators = nn.ModuleList([
+ MelganDiscriminator(in_channels=in_channels,
+ out_channels=out_channels,
+ kernel_sizes=kernel_sizes,
+ base_channels=base_channels,
+ max_channels=max_channels,
+ downsample_factors=downsample_factors)
+ for _ in range(num_scales)
+ ])
+
+ self.pooling = nn.AvgPool1d(kernel_size=pooling_kernel_size, stride=pooling_stride, padding=pooling_padding, count_include_pad=False)
+
+
+ def forward(self, x):
+ scores = list()
+ feats = list()
+ for disc in self.discriminators:
+ score, feat = disc(x)
+ scores.append(score)
+ feats.append(feat)
+ x = self.pooling(x)
+ return scores, feats
\ No newline at end of file
diff --git a/vocoder/models/multiband_melgan_generator.py b/vocoder/models/multiband_melgan_generator.py
new file mode 100644
index 00000000..15e7426e
--- /dev/null
+++ b/vocoder/models/multiband_melgan_generator.py
@@ -0,0 +1,39 @@
+import torch
+
+from TTS.vocoder.models.melgan_generator import MelganGenerator
+from TTS.vocoder.layers.pqmf import PQMF
+
+
+class MultibandMelganGenerator(MelganGenerator):
+ def __init__(self,
+ in_channels=80,
+ out_channels=4,
+ proj_kernel=7,
+ base_channels=384,
+ upsample_factors=(2, 8, 2, 2),
+ res_kernel=3,
+ num_res_blocks=3):
+ super(MultibandMelganGenerator,
+ self).__init__(in_channels=in_channels,
+ out_channels=out_channels,
+ proj_kernel=proj_kernel,
+ base_channels=base_channels,
+ upsample_factors=upsample_factors,
+ res_kernel=res_kernel,
+ num_res_blocks=num_res_blocks)
+ self.pqmf_layer = PQMF(N=4, taps=62, cutoff=0.15, beta=9.0)
+
+ def pqmf_analysis(self, x):
+ return self.pqmf_layer.analysis(x)
+
+ def pqmf_synthesis(self, x):
+ return self.pqmf_layer.synthesis(x)
+
+ @torch.no_grad()
+ def inference(self, cond_features):
+ cond_features = cond_features.to(self.layers[1].weight.device)
+ cond_features = torch.nn.functional.pad(
+ cond_features,
+ (self.inference_padding, self.inference_padding),
+ 'replicate')
+ return self.pqmf_synthesis(self.layers(cond_features))
diff --git a/vocoder/models/random_window_discriminator.py b/vocoder/models/random_window_discriminator.py
new file mode 100644
index 00000000..3efd395e
--- /dev/null
+++ b/vocoder/models/random_window_discriminator.py
@@ -0,0 +1,225 @@
+import numpy as np
+from torch import nn
+
+
+class GBlock(nn.Module):
+ def __init__(self, in_channels, cond_channels, downsample_factor):
+ super(GBlock, self).__init__()
+
+ self.in_channels = in_channels
+ self.cond_channels = cond_channels
+ self.downsample_factor = downsample_factor
+
+ self.start = nn.Sequential(
+ nn.AvgPool1d(downsample_factor, stride=downsample_factor),
+ nn.ReLU(),
+ nn.Conv1d(in_channels, in_channels * 2, kernel_size=3, padding=1))
+ self.lc_conv1d = nn.Conv1d(cond_channels,
+ in_channels * 2,
+ kernel_size=1)
+ self.end = nn.Sequential(
+ nn.ReLU(),
+ nn.Conv1d(in_channels * 2,
+ in_channels * 2,
+ kernel_size=3,
+ dilation=2,
+ padding=2))
+ self.residual = nn.Sequential(
+ nn.Conv1d(in_channels, in_channels * 2, kernel_size=1),
+ nn.AvgPool1d(downsample_factor, stride=downsample_factor))
+
+ def forward(self, inputs, conditions):
+ outputs = self.start(inputs) + self.lc_conv1d(conditions)
+ outputs = self.end(outputs)
+ residual_outputs = self.residual(inputs)
+ outputs = outputs + residual_outputs
+
+ return outputs
+
+
+class DBlock(nn.Module):
+ def __init__(self, in_channels, out_channels, downsample_factor):
+ super(DBlock, self).__init__()
+
+ self.in_channels = in_channels
+ self.downsample_factor = downsample_factor
+ self.out_channels = out_channels
+
+ self.donwsample_layer = nn.AvgPool1d(downsample_factor,
+ stride=downsample_factor)
+ self.layers = nn.Sequential(
+ nn.ReLU(),
+ nn.Conv1d(in_channels, out_channels, kernel_size=3, padding=1),
+ nn.ReLU(),
+ nn.Conv1d(out_channels,
+ out_channels,
+ kernel_size=3,
+ dilation=2,
+ padding=2))
+ self.residual = nn.Sequential(
+ nn.Conv1d(in_channels, out_channels, kernel_size=1), )
+
+ def forward(self, inputs):
+ if self.downsample_factor > 1:
+ outputs = self.layers(self.donwsample_layer(inputs))\
+ + self.donwsample_layer(self.residual(inputs))
+ else:
+ outputs = self.layers(inputs) + self.residual(inputs)
+ return outputs
+
+
+class ConditionalDiscriminator(nn.Module):
+ def __init__(self,
+ in_channels,
+ cond_channels,
+ downsample_factors=(2, 2, 2),
+ out_channels=(128, 256)):
+ super(ConditionalDiscriminator, self).__init__()
+
+ assert len(downsample_factors) == len(out_channels) + 1
+
+ self.in_channels = in_channels
+ self.cond_channels = cond_channels
+ self.downsample_factors = downsample_factors
+ self.out_channels = out_channels
+
+ self.pre_cond_layers = nn.ModuleList()
+ self.post_cond_layers = nn.ModuleList()
+
+ # layers before condition features
+ self.pre_cond_layers += [DBlock(in_channels, 64, 1)]
+ in_channels = 64
+ for (i, channel) in enumerate(out_channels):
+ self.pre_cond_layers.append(
+ DBlock(in_channels, channel, downsample_factors[i]))
+ in_channels = channel
+
+ # condition block
+ self.cond_block = GBlock(in_channels, cond_channels,
+ downsample_factors[-1])
+
+ # layers after condition block
+ self.post_cond_layers += [
+ DBlock(in_channels * 2, in_channels * 2, 1),
+ DBlock(in_channels * 2, in_channels * 2, 1),
+ nn.AdaptiveAvgPool1d(1),
+ nn.Conv1d(in_channels * 2, 1, kernel_size=1),
+ ]
+
+ def forward(self, inputs, conditions):
+ batch_size = inputs.size()[0]
+ outputs = inputs.view(batch_size, self.in_channels, -1)
+ for layer in self.pre_cond_layers:
+ outputs = layer(outputs)
+ outputs = self.cond_block(outputs, conditions)
+ for layer in self.post_cond_layers:
+ outputs = layer(outputs)
+
+ return outputs
+
+
+class UnconditionalDiscriminator(nn.Module):
+ def __init__(self,
+ in_channels,
+ base_channels=64,
+ downsample_factors=(8, 4),
+ out_channels=(128, 256)):
+ super(UnconditionalDiscriminator, self).__init__()
+
+ self.downsample_factors = downsample_factors
+ self.in_channels = in_channels
+ self.downsample_factors = downsample_factors
+ self.out_channels = out_channels
+
+ self.layers = nn.ModuleList()
+ self.layers += [DBlock(self.in_channels, base_channels, 1)]
+ in_channels = base_channels
+ for (i, factor) in enumerate(downsample_factors):
+ self.layers.append(DBlock(in_channels, out_channels[i], factor))
+ in_channels *= 2
+ self.layers += [
+ DBlock(in_channels, in_channels, 1),
+ DBlock(in_channels, in_channels, 1),
+ nn.AdaptiveAvgPool1d(1),
+ nn.Conv1d(in_channels, 1, kernel_size=1),
+ ]
+
+ def forward(self, inputs):
+ batch_size = inputs.size()[0]
+ outputs = inputs.view(batch_size, self.in_channels, -1)
+ for layer in self.layers:
+ outputs = layer(outputs)
+ return outputs
+
+
+class RandomWindowDiscriminator(nn.Module):
+ """Random Window Discriminator as described in
+ http://arxiv.org/abs/1909.11646"""
+ def __init__(self,
+ cond_channels,
+ hop_length,
+ uncond_disc_donwsample_factors=(8, 4),
+ cond_disc_downsample_factors=((8, 4, 2, 2, 2), (8, 4, 2, 2),
+ (8, 4, 2), (8, 4), (4, 2, 2)),
+ cond_disc_out_channels=((128, 128, 256, 256), (128, 256, 256),
+ (128, 256), (256, ), (128, 256)),
+ window_sizes=(512, 1024, 2048, 4096, 8192)):
+
+ super(RandomWindowDiscriminator, self).__init__()
+ self.cond_channels = cond_channels
+ self.window_sizes = window_sizes
+ self.hop_length = hop_length
+ self.base_window_size = self.hop_length * 2
+ self.ks = [ws // self.base_window_size for ws in window_sizes]
+
+ # check arguments
+ assert len(cond_disc_downsample_factors) == len(
+ cond_disc_out_channels) == len(window_sizes)
+ for ws in window_sizes:
+ assert ws % hop_length == 0
+
+ for idx, cf in enumerate(cond_disc_downsample_factors):
+ assert np.prod(cf) == hop_length // self.ks[idx]
+
+ # define layers
+ self.unconditional_discriminators = nn.ModuleList([])
+ for k in self.ks:
+ layer = UnconditionalDiscriminator(
+ in_channels=k,
+ base_channels=64,
+ downsample_factors=uncond_disc_donwsample_factors)
+ self.unconditional_discriminators.append(layer)
+
+ self.conditional_discriminators = nn.ModuleList([])
+ for idx, k in enumerate(self.ks):
+ layer = ConditionalDiscriminator(
+ in_channels=k,
+ cond_channels=cond_channels,
+ downsample_factors=cond_disc_downsample_factors[idx],
+ out_channels=cond_disc_out_channels[idx])
+ self.conditional_discriminators.append(layer)
+
+ def forward(self, x, c):
+ scores = []
+ feats = []
+ # unconditional pass
+ for (window_size, layer) in zip(self.window_sizes,
+ self.unconditional_discriminators):
+ index = np.random.randint(x.shape[-1] - window_size)
+
+ score = layer(x[:, :, index:index + window_size])
+ scores.append(score)
+
+ # conditional pass
+ for (window_size, layer) in zip(self.window_sizes,
+ self.conditional_discriminators):
+ frame_size = window_size // self.hop_length
+ lc_index = np.random.randint(c.shape[-1] - frame_size)
+ sample_index = lc_index * self.hop_length
+ x_sub = x[:, :,
+ sample_index:(lc_index + frame_size) * self.hop_length]
+ c_sub = c[:, :, lc_index:lc_index + frame_size]
+
+ score = layer(x_sub, c_sub)
+ scores.append(score)
+ return scores, feats
diff --git a/vocoder/notebooks/Untitled.ipynb b/vocoder/notebooks/Untitled.ipynb
new file mode 100644
index 00000000..ce49d6fa
--- /dev/null
+++ b/vocoder/notebooks/Untitled.ipynb
@@ -0,0 +1,678 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [],
+ "source": [
+ "#function example with several unknowns (variables) for optimization\n",
+ "#Gerald Schuller, Nov. 2016\n",
+ "import numpy as np\n",
+ "\n",
+ "def functionexamp(x):\n",
+ " #x: array with 2 variables\n",
+ " \n",
+ " y=np.sin(x[0])+np.cos(x[1])\n",
+ " return y"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " fun: -1.9999999999888387\n",
+ " jac: array([4.7236681e-06, 0.0000000e+00])\n",
+ " message: 'Optimization terminated successfully.'\n",
+ " nfev: 12\n",
+ " nit: 2\n",
+ " njev: 3\n",
+ " status: 0\n",
+ " success: True\n",
+ " x: array([-1.5707916 , -3.14159265])\n"
+ ]
+ }
+ ],
+ "source": [
+ "#Optimization example, see also:\n",
+ "#https://docs.scipy.org/doc/scipy-0.18.1/reference/optimize.html\n",
+ "#Gerald Schuller, Nov. 2016\n",
+ "#run it with \"python optimizationExample.py\" in a termina shell\n",
+ "#or type \"ipython\" in a termina shell and copy lines below:\n",
+ "\n",
+ "import numpy as np\n",
+ "import scipy.optimize as optimize\n",
+ "\n",
+ "#Example for 2 unknowns, args: function-name, starting point, method:\n",
+ "xmin = optimize.minimize(functionexamp, [-1.0, -3.0], method='CG')\n",
+ "print(xmin)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [],
+ "source": [
+ "function [p,passedge] = opt_filter(filtorder,N)\n",
+ "\n",
+ "% opt_filter Create Lowpass Prototype Filter for the Pseudo-QMF \n",
+ "% Filter Bank with N Subbands\n",
+ "%\n",
+ "% Adapted from the paper by C. D. Creusere and S. K. Mitra, titled \n",
+ "% \"A simple method for designing high-quality prototype filters for \n",
+ "% M-band pseudo-QMF banks,\" IEEE Trans. Signal Processing,vol. 43, \n",
+ "% pp. 1005-1007, Apr. 1995 and the book by S. K. Mitra titled \"\n",
+ "% Digital Signal Processing: A Computer-Based Approach, McGraw-Hill, 2001\n",
+ "%\n",
+ "% Arguments:\n",
+ "% filtorder Filter order (i.e., filter length - 1)\n",
+ "% N Number of subbands\n",
+ "\n",
+ "stopedge = 1/N; % Stopband edge fixed at (1/N)pi\n",
+ "passedge = 1/(4*N); % Start value for passband edge\n",
+ "tol = 0.000001; % Tolerance\n",
+ "step = 0.1*passedge; % Step size for searching the passband edge\n",
+ "way = -1; % Search direction, increase or reduce the passband edge\n",
+ "tcost = 0; % Current error calculated with the cost function\n",
+ "pcost = 10; % Previous error calculated with the cost function\n",
+ "flag = 0; % Set to 1 to stop the search\n",
+ "\n",
+ "while flag == 0\n",
+ " \n",
+ "% Design the lowpass filter using Parks-McClellan algorithm\n",
+ " \n",
+ " p = remez(filtorder,[0,passedge,stopedge,1],[1,1,0,0],[5,1]);\n",
+ " \n",
+ "% Calculates the cost function according to Eq. (2.36)\n",
+ "\n",
+ " P = fft(p,4096);\n",
+ " OptRange = floor(2048/N); % 0 to pi/N\n",
+ " phi = zeros(OptRange,1); % Initialize to zeros\n",
+ "\n",
+ "% Compute the flatness in the range from 0 to pi/N\n",
+ "\n",
+ "\tfor k = 1:OptRange\n",
+ " phi(k) = abs(P(OptRange-k+2))^2 + abs(P(k))^2;\n",
+ "\tend\n",
+ "\ttcost = max(abs(phi - ones(max(size(phi)),1)));\n",
+ " \t\n",
+ "\tif tcost > pcost % If search in wrong direction\n",
+ "\t\tstep = step/2; % Reduce step size by half \n",
+ "\t\tway = -way; % Change the search direction \n",
+ "\tend\n",
+ "\t\n",
+ "\tif abs(pcost - tcost) < tol % If improvement is below tol \n",
+ "\t\tflag = 1; % Stop the search \n",
+ "\tend\n",
+ "\t\n",
+ "\tpcost = tcost;\n",
+ "\tpassedge = passedge + way*step; % Adjust the passband edge\n",
+ " \n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [],
+ "source": [
+ "sig.remez"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 101,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "0.0125"
+ ]
+ },
+ "execution_count": 101,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "1 / 4. / 20.0"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 90,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [
+ {
+ "ename": "ValueError",
+ "evalue": "Band edges should be less than 1/2 the sampling frequency",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+ "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)",
+ "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msig\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mremez\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m64\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m/\u001b[0m\u001b[0;36m16.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m/\u001b[0m\u001b[0;36m4.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m5\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
+ "\u001b[0;32m~/miniconda3/lib/python3.7/site-packages/scipy/signal/fir_filter_design.py\u001b[0m in \u001b[0;36mremez\u001b[0;34m(numtaps, bands, desired, weight, Hz, type, maxiter, grid_density, fs)\u001b[0m\n\u001b[1;32m 854\u001b[0m \u001b[0mbands\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0masarray\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mbands\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcopy\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 855\u001b[0m return sigtools._remez(numtaps, bands, desired, weight, tnum, fs,\n\u001b[0;32m--> 856\u001b[0;31m maxiter, grid_density)\n\u001b[0m\u001b[1;32m 857\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 858\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
+ "\u001b[0;31mValueError\u001b[0m: Band edges should be less than 1/2 the sampling frequency"
+ ]
+ }
+ ],
+ "source": [
+ "p = sig.remez(65, [0, 1/16.0, 1/4.0, 1], [1, 0], [5, 1])\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [],
+ "source": [
+ "def create_pqmf_filter(filter_len=64, N=4):\n",
+ " stop_edge = 1 / N\n",
+ " pass_edge = 1 / (4 * N)\n",
+ " tol = 1e-8\n",
+ " cutoff = 0.1 * pass_edge\n",
+ " cost = 0\n",
+ " cost_prev = float('inf')\n",
+ " \n",
+ " p = sig.remez(filter_len, [0, pass_edge, stop_edge, 1], [1, 1, 0, 0], [5, 1])\n",
+ " \n",
+ " P = sig.freqz(p, workN=2048)\n",
+ " opt_range = 2048 // N\n",
+ " phi = np.zeros(opt_range)\n",
+ " \n",
+ " H = np.abs(P)\n",
+ " phi = H[opt_range + 2] \n",
+ " for i in range(opt_range):\n",
+ " phi[i] = abs(P(opt_range - i + 2)) ** 2 + abs(P[i]) ** 2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 81,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import scipy as sp\n",
+ "import scipy.signal as sig\n",
+ "import matplotlib.pyplot as plt\n",
+ "%matplotlib inline\n",
+ "\n",
+ "\n",
+ "def optimfuncQMF(x):\n",
+ " \"\"\"Optimization function for a PQMF Filterbank\n",
+ " x: coefficients to optimize (first half of prototype h because of symmetry)\n",
+ " err: resulting total error\n",
+ " \"\"\"\n",
+ " K = ntaps * N \n",
+ " h = np.append(x, np.flipud(x))\n",
+ " cutoff = 0.15\n",
+ " \n",
+ "# breakpoint()\n",
+ " f, H_im = sig.freqz(h, worN=K)\n",
+ " H = np.abs(H_im) #only keeping the real part\n",
+ " \n",
+ " posfreq = np.square(H[0:K//N])\n",
+ " \n",
+ " #Negative frequencies are symmetric around 0:\n",
+ " negfreq = np.flipud(np.square(H[0:K//N]))\n",
+ " \n",
+ " #Sum of magnitude squared frequency responses should be closed to unity (or N)\n",
+ " unitycond = np.sum(np.abs(posfreq + negfreq - 2*(N*N)*np.ones(K//N)))/K\n",
+ " \n",
+ " #plt.plot(posfreq+negfreq)\n",
+ " \n",
+ " #High attenuation after the next subband:\n",
+ " att = np.sum(np.abs(H[int(cutoff*K//N):]))/K\n",
+ " \n",
+ " #Total (weighted) error:\n",
+ " err = unitycond + 100*att\n",
+ " return err"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 85,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(32,)"
+ ]
+ },
+ "execution_count": 85,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "xmin.shape"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 86,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "8.684549400499243\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt\n",
+ "import scipy.optimize as opt\n",
+ "import scipy.signal as sig\n",
+ "\n",
+ "ntaps = 64\n",
+ "N = 4\n",
+ "\n",
+ "#optimize for 16 filter coefficients:\n",
+ "xmin = opt.minimize(optimfuncQMF, ntaps*np.ones(ntaps), method='SLSQP', tol=1e-8)\n",
+ "xmin = xmin[\"x\"]\n",
+ "\n",
+ "err = optimfuncQMF(xmin)\n",
+ "print(err)\n",
+ "\n",
+ "#Restore symmetric upper half of window:\n",
+ "h = np.concatenate((xmin, np.flipud(xmin)))\n",
+ "plt.plot(h)\n",
+ "plt.title('Resulting PQMF Window Function')\n",
+ "plt.xlabel('Sample')\n",
+ "plt.ylabel('Value')\n",
+ "plt.show()\n",
+ "\n",
+ "f, H = sig.freqz(h)\n",
+ "plt.plot(f, 20*np.log10(np.abs(H)))\n",
+ "plt.title('Resulting PQMF Magnitude Response')\n",
+ "plt.xlabel('Normalized Frequency')\n",
+ "plt.ylabel('dB')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 56,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "N = 4\n",
+ "f, H_im = sig.freqz(h)\n",
+ "posfreq = np.square(H[0:512//N])\n",
+ "negfreq = np.flipud(np.square(H[0:512//N]))\n",
+ "plt.plot((np.abs(posfreq) + np.abs(negfreq)))\n",
+ "plt.xlabel('Frequency (512 is Nyquist)')\n",
+ "plt.ylabel('Magnitude')\n",
+ "plt.title('Unity Condition, Sum of Squared Magnitude of 2 Neighboring Subbands')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [],
+ "source": [
+ "b = sig.firwin(80, 0.5, window=('kaiser', 8))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "f, H_im = sig.freqz(h)\n",
+ "posfreq = np.square(H[0:512//N])\n",
+ "negfreq = np.flipud(np.square(H[0:512//N]))\n",
+ "plt.plot((np.abs(posfreq) + np.abs(negfreq)))\n",
+ "plt.xlabel('Frequency (512 is Nyquist)')\n",
+ "plt.ylabel('Magnitude')\n",
+ "plt.title('Unity Condition, Sum of Squared Magnitude of 2 Neighboring Subbands')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 79,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(63,)"
+ ]
+ },
+ "execution_count": 79,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "b.shape"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 102,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "cutoff = 0.15\n",
+ "beta = 9\n",
+ "ntaps = 63\n",
+ "N = 4\n",
+ "\n",
+ "b = sig.firwin(ntaps, cutoff, window=('kaiser', beta))\n",
+ "w, h = sig.freqz(b)\n",
+ "\n",
+ "plt.plot(b)\n",
+ "plt.title('Resulting PQMF Window Function')\n",
+ "plt.xlabel('Sample')\n",
+ "plt.ylabel('Value')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 103,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "fig, ax1 = plt.subplots()\n",
+ "ax1.set_title('Digital filter frequency response')\n",
+ "\n",
+ "ax1.plot(w / (2 * np.pi), 20 * np.log10(abs(h)), 'b')\n",
+ "ax1.set_ylabel('Amplitude [dB]', color='b')\n",
+ "ax1.set_xlabel('Frequency [rad/sample]')\n",
+ "\n",
+ "ax2 = ax1.twinx()\n",
+ "angles = np.unwrap(np.angle(h))\n",
+ "ax2.plot(w / (2 * np.pi), angles, 'g')\n",
+ "ax2.set_ylabel('Angle (radians)', color='g')\n",
+ "ax2.grid()\n",
+ "ax2.axis('tight')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 105,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(63,)"
+ ]
+ },
+ "execution_count": 105,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "b.shape"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 129,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [],
+ "source": [
+ "def optimfuncQMF(x):\n",
+ " \"\"\"Optimization function for a PQMF Filterbank\n",
+ " x: coefficients to optimize (first half of prototype h because of symmetry)\n",
+ " err: resulting total error\n",
+ " \"\"\"\n",
+ " N = 2 #4 subbands\n",
+ " cutoff = 1.5 #1.5\n",
+ " h = np.append(x, np.flipud(x))\n",
+ " f, H_im = sig.freqz(h)\n",
+ " H = np.abs(H_im) #only keeping the real part\n",
+ " \n",
+ " posfreq = np.square(H[0:512//N])\n",
+ " \n",
+ " #Negative frequencies are symmetric around 0:\n",
+ " negfreq = np.flipud(np.square(H[0:512//N]))\n",
+ " \n",
+ " #Sum of magnitude squared frequency responses should be closed to unity (or N)\n",
+ " unitycond = np.sum(np.abs(posfreq + negfreq - 2*(N*N)*np.ones(512//N)))//512\n",
+ " \n",
+ " #plt.plot(posfreq+negfreq)\n",
+ " \n",
+ " #High attenuation after the next subband:\n",
+ " att = np.sum(np.abs(H[int(cutoff*512//N):]))//512\n",
+ " \n",
+ " #Total (weighted) error:\n",
+ " err = unitycond + 100*att\n",
+ " return err"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 131,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "3.0\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "err = optimfuncQMF(b)\n",
+ "print(err)\n",
+ "\n",
+ "#Restore symmetric upper half of window:\n",
+ "h = np.concatenate((xmin, np.flipud(xmin)))\n",
+ "plt.plot(h)\n",
+ "plt.title('Resulting PQMF Window Function')\n",
+ "plt.xlabel('Sample')\n",
+ "plt.ylabel('Value')\n",
+ "plt.show()\n",
+ "\n",
+ "f, H = sig.freqz(h)\n",
+ "plt.plot(f, 20*np.log10(np.abs(H)))\n",
+ "plt.title('Resulting PQMF Magnitude Response')\n",
+ "plt.xlabel('Normalized Frequency')\n",
+ "plt.ylabel('dB')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "Collapsed": "false"
+ },
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3.7.4 64-bit ('base': conda)",
+ "language": "python",
+ "name": "python37464bitbaseconda58faf23c4b5f4fef93406f29a1005f35"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.4"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/vocoder/notebooks/Untitled1.ipynb b/vocoder/notebooks/Untitled1.ipynb
new file mode 100644
index 00000000..7fec5150
--- /dev/null
+++ b/vocoder/notebooks/Untitled1.ipynb
@@ -0,0 +1,6 @@
+{
+ "cells": [],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/vocoder/pqmf_output.wav b/vocoder/pqmf_output.wav
new file mode 100644
index 00000000..8a77747b
Binary files /dev/null and b/vocoder/pqmf_output.wav differ
diff --git a/vocoder/tests/__init__.py b/vocoder/tests/__init__.py
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/vocoder/tests/__init__.py
@@ -0,0 +1 @@
+
diff --git a/vocoder/tests/test_config.json b/vocoder/tests/test_config.json
new file mode 100644
index 00000000..08acc48c
--- /dev/null
+++ b/vocoder/tests/test_config.json
@@ -0,0 +1,24 @@
+{
+ "audio":{
+ "num_mels": 80, // size of the mel spec frame.
+ "num_freq": 513, // number of stft frequency levels. Size of the linear spectogram frame.
+ "sample_rate": 22050, // wav sample-rate. If different than the original data, it is resampled.
+ "frame_length_ms": null, // stft window length in ms.
+ "frame_shift_ms": null, // stft window hop-lengh in ms.
+ "hop_length": 256,
+ "win_length": 1024,
+ "preemphasis": 0.97, // 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": 30,// #griffin-lim iterations. 30-60 is a good range. Larger the value, slower the generation.
+ "signal_norm": true, // normalize the spec values in range [0, 1]
+ "symmetric_norm": true, // move normalization to range [-1, 1]
+ "clip_norm": true, // clip normalized values into the range.
+ "max_norm": 4, // scale normalization to range [-max_norm, max_norm] or [0, max_norm]
+ "mel_fmin": 0, // minimum freq level for mel-spec. ~50 for male and ~95 for female voices. Tune for dataset!!
+ "mel_fmax": 8000, // maximum freq level for mel-spec. Tune for dataset!!
+ "do_trim_silence": false
+ }
+}
+
diff --git a/vocoder/tests/test_datasets.py b/vocoder/tests/test_datasets.py
new file mode 100644
index 00000000..5d409b3f
--- /dev/null
+++ b/vocoder/tests/test_datasets.py
@@ -0,0 +1,95 @@
+import os
+import numpy as np
+from torch.utils.data import DataLoader
+
+from TTS.vocoder.datasets.gan_dataset import GANDataset
+from TTS.vocoder.datasets.preprocess import load_wav_data
+from TTS.utils.audio import AudioProcessor
+from TTS.utils.io import load_config
+
+
+file_path = os.path.dirname(os.path.realpath(__file__))
+OUTPATH = os.path.join(file_path, "../../tests/outputs/loader_tests/")
+os.makedirs(OUTPATH, exist_ok=True)
+
+C = load_config(os.path.join(file_path, 'test_config.json'))
+
+test_data_path = os.path.join(file_path, "../../tests/data/ljspeech/")
+ok_ljspeech = os.path.exists(test_data_path)
+
+
+
+def gan_dataset_case(batch_size, seq_len, hop_len, conv_pad, return_segments, use_noise_augment, use_cache, num_workers):
+ ''' run dataloader with given parameters and check conditions '''
+ ap = AudioProcessor(**C.audio)
+ _, train_items = load_wav_data(test_data_path, 10)
+ dataset = GANDataset(ap,
+ train_items,
+ seq_len=seq_len,
+ hop_len=hop_len,
+ pad_short=2000,
+ conv_pad=conv_pad,
+ return_segments=return_segments,
+ use_noise_augment=use_noise_augment,
+ use_cache=use_cache)
+ loader = DataLoader(dataset=dataset,
+ batch_size=batch_size,
+ shuffle=True,
+ num_workers=num_workers,
+ pin_memory=True,
+ drop_last=True)
+
+ max_iter = 10
+ count_iter = 0
+
+ # return random segments or return the whole audio
+ if return_segments:
+ for item1, _ in loader:
+ feat1, wav1 = item1
+ # feat2, wav2 = item2
+ expected_feat_shape = (batch_size, ap.num_mels, seq_len // hop_len + conv_pad * 2)
+
+ # check shapes
+ assert np.all(feat1.shape == expected_feat_shape), f" [!] {feat1.shape} vs {expected_feat_shape}"
+ assert (feat1.shape[2] - conv_pad * 2) * hop_len == wav1.shape[2]
+
+ # check feature vs audio match
+ if not use_noise_augment:
+ for idx in range(batch_size):
+ audio = wav1[idx].squeeze()
+ feat = feat1[idx]
+ mel = ap.melspectrogram(audio)
+ # the first 2 and the last frame is skipped due to the padding
+ # applied in spec. computation.
+ assert (feat - mel[:, :feat1.shape[-1]])[:, 2:-1].sum() == 0, f' [!] {(feat - mel[:, :feat1.shape[-1]])[:, 2:-1].sum()}'
+
+ count_iter += 1
+ # if count_iter == max_iter:
+ # break
+ else:
+ for item in loader:
+ feat, wav = item
+ expected_feat_shape = (batch_size, ap.num_mels, (wav.shape[-1] // hop_len) + (conv_pad * 2))
+ assert np.all(feat.shape == expected_feat_shape), f" [!] {feat.shape} vs {expected_feat_shape}"
+ assert (feat.shape[2] - conv_pad * 2) * hop_len == wav.shape[2]
+ count_iter += 1
+ if count_iter == max_iter:
+ break
+
+
+def test_parametrized_gan_dataset():
+ ''' test dataloader with different parameters '''
+ params = [
+ [32, C.audio['hop_length'] * 10, C.audio['hop_length'], 0, True, False, True, 0],
+ [32, C.audio['hop_length'] * 10, C.audio['hop_length'], 0, True, False, True, 4],
+ [1, C.audio['hop_length'] * 10, C.audio['hop_length'], 0, True, True, True, 0],
+ [1, C.audio['hop_length'], C.audio['hop_length'], 0, True, True, True, 0],
+ [1, C.audio['hop_length'] * 10, C.audio['hop_length'], 2, True, True, True, 0],
+ [1, C.audio['hop_length'] * 10, C.audio['hop_length'], 0, False, True, True, 0],
+ [1, C.audio['hop_length'] * 10, C.audio['hop_length'], 0, True, False, True, 0],
+ [1, C.audio['hop_length'] * 10, C.audio['hop_length'], 0, True, True, False, 0],
+ [1, C.audio['hop_length'] * 10, C.audio['hop_length'], 0, False, False, False, 0],
+ ]
+ for param in params:
+ print(param)
+ gan_dataset_case(*param)
diff --git a/vocoder/tests/test_losses.py b/vocoder/tests/test_losses.py
new file mode 100644
index 00000000..44a1321d
--- /dev/null
+++ b/vocoder/tests/test_losses.py
@@ -0,0 +1,61 @@
+import os
+import torch
+
+from TTS.vocoder.layers.losses import TorchSTFT, STFTLoss, MultiScaleSTFTLoss
+
+from TTS.tests import get_tests_path, get_tests_input_path, get_tests_output_path
+from TTS.utils.audio import AudioProcessor
+from TTS.utils.io import load_config
+
+TESTS_PATH = get_tests_path()
+
+OUT_PATH = os.path.join(get_tests_output_path(), "audio_tests")
+os.makedirs(OUT_PATH, exist_ok=True)
+
+WAV_FILE = os.path.join(get_tests_input_path(), "example_1.wav")
+
+file_path = os.path.dirname(os.path.realpath(__file__))
+C = load_config(os.path.join(file_path, 'test_config.json'))
+ap = AudioProcessor(**C.audio)
+
+
+def test_torch_stft():
+ torch_stft = TorchSTFT(ap.n_fft, ap.hop_length, ap.win_length)
+ # librosa stft
+ wav = ap.load_wav(WAV_FILE)
+ M_librosa = abs(ap._stft(wav)) # pylint: disable=protected-access
+ # torch stft
+ wav = torch.from_numpy(wav[None, :]).float()
+ M_torch = torch_stft(wav)
+ # check the difference b/w librosa and torch outputs
+ assert (M_librosa - M_torch[0].data.numpy()).max() < 1e-5
+
+
+def test_stft_loss():
+ stft_loss = STFTLoss(ap.n_fft, ap.hop_length, ap.win_length)
+ wav = ap.load_wav(WAV_FILE)
+ wav = torch.from_numpy(wav[None, :]).float()
+ loss_m, loss_sc = stft_loss(wav, wav)
+ assert loss_m + loss_sc == 0
+ loss_m, loss_sc = stft_loss(wav, torch.rand_like(wav))
+ assert loss_sc < 1.0
+ assert loss_m + loss_sc > 0
+
+
+def test_multiscale_stft_loss():
+ stft_loss = MultiScaleSTFTLoss([ap.n_fft//2, ap.n_fft, ap.n_fft*2],
+ [ap.hop_length // 2, ap.hop_length, ap.hop_length * 2],
+ [ap.win_length // 2, ap.win_length, ap.win_length * 2])
+ wav = ap.load_wav(WAV_FILE)
+ wav = torch.from_numpy(wav[None, :]).float()
+ loss_m, loss_sc = stft_loss(wav, wav)
+ assert loss_m + loss_sc == 0
+ loss_m, loss_sc = stft_loss(wav, torch.rand_like(wav))
+ assert loss_sc < 1.0
+ assert loss_m + loss_sc > 0
+
+
+
+
+
+
diff --git a/vocoder/tests/test_melgan_discriminator.py b/vocoder/tests/test_melgan_discriminator.py
new file mode 100644
index 00000000..a89dad5e
--- /dev/null
+++ b/vocoder/tests/test_melgan_discriminator.py
@@ -0,0 +1,26 @@
+import numpy as np
+import torch
+
+from TTS.vocoder.models.melgan_discriminator import MelganDiscriminator
+from TTS.vocoder.models.melgan_multiscale_discriminator import MelganMultiscaleDiscriminator
+
+
+def test_melgan_discriminator():
+ model = MelganDiscriminator()
+ print(model)
+ dummy_input = torch.rand((4, 1, 256 * 10))
+ output, _ = model(dummy_input)
+ assert np.all(output.shape == (4, 1, 10))
+
+
+def test_melgan_multi_scale_discriminator():
+ model = MelganMultiscaleDiscriminator()
+ print(model)
+ dummy_input = torch.rand((4, 1, 256 * 16))
+ scores, feats = model(dummy_input)
+ assert len(scores) == 3
+ assert len(scores) == len(feats)
+ assert np.all(scores[0].shape == (4, 1, 64))
+ assert np.all(feats[0][0].shape == (4, 16, 4096))
+ assert np.all(feats[0][1].shape == (4, 64, 1024))
+ assert np.all(feats[0][2].shape == (4, 256, 256))
\ No newline at end of file
diff --git a/vocoder/tests/test_melgan_generator.py b/vocoder/tests/test_melgan_generator.py
new file mode 100644
index 00000000..e03da31b
--- /dev/null
+++ b/vocoder/tests/test_melgan_generator.py
@@ -0,0 +1,14 @@
+import numpy as np
+import torch
+
+from TTS.vocoder.models.melgan_generator import MelganGenerator
+
+def test_melgan_generator():
+ model = MelganGenerator()
+ print(model)
+ dummy_input = torch.rand((4, 80, 64))
+ output = model(dummy_input)
+ assert np.all(output.shape == (4, 1, 64 * 256))
+ output = model.inference(dummy_input)
+ assert np.all(output.shape == (4, 1, (64 + 4) * 256))
+
diff --git a/vocoder/tests/test_pqmf.py b/vocoder/tests/test_pqmf.py
new file mode 100644
index 00000000..a26bdd59
--- /dev/null
+++ b/vocoder/tests/test_pqmf.py
@@ -0,0 +1,28 @@
+import os
+import torch
+
+import soundfile as sf
+from librosa.core import load
+
+from TTS.tests import get_tests_path, get_tests_input_path
+from TTS.vocoder.layers.pqmf import PQMF
+
+
+TESTS_PATH = get_tests_path()
+WAV_FILE = os.path.join(get_tests_input_path(), "example_1.wav")
+
+
+def test_pqmf():
+ w, sr = load(WAV_FILE)
+
+ layer = PQMF(N=4, taps=62, cutoff=0.15, beta=9.0)
+ w, sr = load(WAV_FILE)
+ w2 = torch.from_numpy(w[None, None, :])
+ b2 = layer.analysis(w2)
+ w2_ = layer.synthesis(b2)
+
+ print(w2_.max())
+ print(w2_.min())
+ print(w2_.mean())
+ sf.write('pqmf_output.wav', w2_.flatten().detach(), sr)
+
diff --git a/vocoder/tests/test_rwd.py b/vocoder/tests/test_rwd.py
new file mode 100644
index 00000000..424d3b49
--- /dev/null
+++ b/vocoder/tests/test_rwd.py
@@ -0,0 +1,21 @@
+import torch
+import numpy as np
+
+from TTS.vocoder.models.random_window_discriminator import RandomWindowDiscriminator
+
+
+def test_rwd():
+ layer = RandomWindowDiscriminator(cond_channels=80,
+ window_sizes=(512, 1024, 2048, 4096,
+ 8192),
+ cond_disc_downsample_factors=[
+ (8, 4, 2, 2, 2), (8, 4, 2, 2),
+ (8, 4, 2), (8, 4), (4, 2, 2)
+ ],
+ hop_length=256)
+ x = torch.rand([4, 1, 22050])
+ c = torch.rand([4, 80, 22050 // 256])
+
+ scores, _ = layer(x, c)
+ assert len(scores) == 10
+ assert np.all(scores[0].shape == (4, 1, 1))
diff --git a/vocoder/tf/convert_melgan_torch_to_tf.py b/vocoder/tf/convert_melgan_torch_to_tf.py
new file mode 100644
index 00000000..1b3d687d
--- /dev/null
+++ b/vocoder/tf/convert_melgan_torch_to_tf.py
@@ -0,0 +1,112 @@
+import argparse
+import os
+
+import numpy as np
+import tensorflow as tf
+import torch
+from fuzzywuzzy import fuzz
+
+from TTS.utils.io import load_config
+from TTS.vocoder.tf.utils.convert_torch_to_tf_utils import (
+ compare_torch_tf, convert_tf_name, transfer_weights_torch_to_tf)
+from TTS.vocoder.tf.utils.generic_utils import \
+ setup_generator as setup_tf_generator
+from TTS.vocoder.tf.utils.io import save_checkpoint
+from TTS.vocoder.utils.generic_utils import setup_generator
+
+# prevent GPU use
+os.environ['CUDA_VISIBLE_DEVICES'] = ''
+
+# define args
+parser = argparse.ArgumentParser()
+parser.add_argument('--torch_model_path',
+ type=str,
+ help='Path to target torch model to be converted to TF.')
+parser.add_argument('--config_path',
+ type=str,
+ help='Path to config file of torch model.')
+parser.add_argument(
+ '--output_path',
+ type=str,
+ help='path to output file including file name to save TF model.')
+args = parser.parse_args()
+
+# load model config
+config_path = args.config_path
+c = load_config(config_path)
+num_speakers = 0
+
+# init torch model
+model = setup_generator(c)
+checkpoint = torch.load(args.torch_model_path,
+ map_location=torch.device('cpu'))
+state_dict = checkpoint['model']
+model.load_state_dict(state_dict)
+model.remove_weight_norm()
+state_dict = model.state_dict()
+
+# init tf model
+model_tf = setup_tf_generator(c)
+
+common_sufix = '/.ATTRIBUTES/VARIABLE_VALUE'
+# get tf_model graph by passing an input
+# B x D x T
+dummy_input = tf.random.uniform((7, 80, 64), dtype=tf.float32)
+mel_pred = model_tf(dummy_input, training=False)
+
+# get tf variables
+tf_vars = model_tf.weights
+
+# match variable names with fuzzy logic
+torch_var_names = list(state_dict.keys())
+tf_var_names = [we.name for we in model_tf.weights]
+var_map = []
+for tf_name in tf_var_names:
+ # skip re-mapped layer names
+ if tf_name in [name[0] for name in var_map]:
+ continue
+ tf_name_edited = convert_tf_name(tf_name)
+ ratios = [
+ fuzz.ratio(torch_name, tf_name_edited)
+ for torch_name in torch_var_names
+ ]
+ max_idx = np.argmax(ratios)
+ matching_name = torch_var_names[max_idx]
+ del torch_var_names[max_idx]
+ var_map.append((tf_name, matching_name))
+
+# pass weights
+tf_vars = transfer_weights_torch_to_tf(tf_vars, dict(var_map), state_dict)
+
+# Compare TF and TORCH models
+# check embedding outputs
+model.eval()
+dummy_input_torch = torch.ones((1, 80, 10))
+dummy_input_tf = tf.convert_to_tensor(dummy_input_torch.numpy())
+dummy_input_tf = tf.transpose(dummy_input_tf, perm=[0, 2, 1])
+dummy_input_tf = tf.expand_dims(dummy_input_tf, 2)
+
+out_torch = model.layers[0](dummy_input_torch)
+out_tf = model_tf.model_layers[0](dummy_input_tf)
+out_tf_ = tf.transpose(out_tf, perm=[0, 3, 2, 1])[:, :, 0, :]
+
+assert compare_torch_tf(out_torch, out_tf_) < 1e-5
+
+for i in range(1, len(model.layers)):
+ print(f"{i} -> {model.layers[i]} vs {model_tf.model_layers[i]}")
+ out_torch = model.layers[i](out_torch)
+ out_tf = model_tf.model_layers[i](out_tf)
+ out_tf_ = tf.transpose(out_tf, perm=[0, 3, 2, 1])[:, :, 0, :]
+ diff = compare_torch_tf(out_torch, out_tf_)
+ assert diff < 1e-5, diff
+
+dummy_input_torch = torch.ones((1, 80, 10))
+dummy_input_tf = tf.convert_to_tensor(dummy_input_torch.numpy())
+output_torch = model.inference(dummy_input_torch)
+output_tf = model_tf(dummy_input_tf, training=False)
+assert compare_torch_tf(output_torch, output_tf) < 1e-5, compare_torch_tf(
+ output_torch, output_tf)
+# save tf model
+save_checkpoint(model_tf, checkpoint['step'], checkpoint['epoch'],
+ args.output_path)
+print(' > Model conversion is successfully completed :).')
diff --git a/vocoder/tf/layers/melgan.py b/vocoder/tf/layers/melgan.py
new file mode 100644
index 00000000..4f195d3f
--- /dev/null
+++ b/vocoder/tf/layers/melgan.py
@@ -0,0 +1,58 @@
+import tensorflow as tf
+
+
+class ReflectionPad1d(tf.keras.layers.Layer):
+ def __init__(self, padding):
+ super(ReflectionPad1d, self).__init__()
+ self.padding = padding
+
+ def call(self, x):
+ print(x.shape)
+ return tf.pad(x, [[0, 0], [self.padding, self.padding], [0, 0], [0, 0]], "REFLECT")
+
+
+class ResidualStack(tf.keras.layers.Layer):
+ def __init__(self, channels, num_res_blocks, kernel_size, name):
+ super(ResidualStack, self).__init__(name=name)
+
+ assert (kernel_size - 1) % 2 == 0, " [!] kernel_size has to be odd."
+ base_padding = (kernel_size - 1) // 2
+
+ self.blocks = []
+ num_layers = 2
+ for idx in range(num_res_blocks):
+ layer_kernel_size = kernel_size
+ layer_dilation = layer_kernel_size**idx
+ layer_padding = base_padding * layer_dilation
+ block = [
+ tf.keras.layers.LeakyReLU(0.2),
+ ReflectionPad1d(layer_padding),
+ tf.keras.layers.Conv2D(filters=channels,
+ kernel_size=(kernel_size, 1),
+ dilation_rate=(layer_dilation, 1),
+ use_bias=True,
+ padding='valid',
+ name=f'blocks.{idx}.{num_layers}'),
+ tf.keras.layers.LeakyReLU(0.2),
+ tf.keras.layers.Conv2D(filters=channels,
+ kernel_size=(1, 1),
+ use_bias=True,
+ name=f'blocks.{idx}.{num_layers + 2}')
+ ]
+ self.blocks.append(block)
+ self.shortcuts = [
+ tf.keras.layers.Conv2D(channels,
+ kernel_size=1,
+ use_bias=True,
+ name=f'shortcuts.{i}')
+ for i in range(num_res_blocks)
+ ]
+
+ def call(self, x):
+ # breakpoint()
+ for block, shortcut in zip(self.blocks, self.shortcuts):
+ res = shortcut(x)
+ for layer in block:
+ x = layer(x)
+ x += res
+ return x
\ No newline at end of file
diff --git a/vocoder/tf/layers/pqmf.py b/vocoder/tf/layers/pqmf.py
new file mode 100644
index 00000000..6c47dfc4
--- /dev/null
+++ b/vocoder/tf/layers/pqmf.py
@@ -0,0 +1,66 @@
+import numpy as np
+import tensorflow as tf
+
+from scipy import signal as sig
+
+
+class PQMF(tf.keras.layers.Layer):
+ def __init__(self, N=4, taps=62, cutoff=0.15, beta=9.0):
+ super(PQMF, self).__init__()
+ # define filter coefficient
+ self.N = N
+ self.taps = taps
+ self.cutoff = cutoff
+ self.beta = beta
+
+ QMF = sig.firwin(taps + 1, cutoff, window=('kaiser', beta))
+ H = np.zeros((N, len(QMF)))
+ G = np.zeros((N, len(QMF)))
+ for k in range(N):
+ constant_factor = (2 * k + 1) * (np.pi /
+ (2 * N)) * (np.arange(taps + 1) -
+ ((taps - 1) / 2))
+ phase = (-1)**k * np.pi / 4
+ H[k] = 2 * QMF * np.cos(constant_factor + phase)
+
+ G[k] = 2 * QMF * np.cos(constant_factor - phase)
+
+ # [N, 1, taps + 1] == [filter_width, in_channels, out_channels]
+ self.H = np.transpose(H[:, None, :], (2, 1, 0)).astype('float32')
+ self.G = np.transpose(G[None, :, :], (2, 1, 0)).astype('float32')
+
+ # filter for downsampling & upsampling
+ updown_filter = np.zeros((N, N, N), dtype=np.float32)
+ for k in range(N):
+ updown_filter[0, k, k] = 1.0
+ self.updown_filter = updown_filter.astype(np.float32)
+
+ def analysis(self, x):
+ """
+ x : B x 1 x T
+ """
+ x = tf.transpose(x, perm=[0, 2, 1])
+ x = tf.pad(x, [[0, 0], [self.taps // 2, self.taps // 2], [0, 0]], constant_values=0.0)
+ x = tf.nn.conv1d(x, self.H, stride=1, padding='VALID')
+ x = tf.nn.conv1d(x,
+ self.updown_filter,
+ stride=self.N,
+ padding='VALID')
+ x = tf.transpose(x, perm=[0, 2, 1])
+ return x
+
+ def synthesis(self, x):
+ """
+ x : B x 1 x T
+ """
+ x = tf.transpose(x, perm=[0, 2, 1])
+ x = tf.nn.conv1d_transpose(
+ x,
+ self.updown_filter * self.N,
+ strides=self.N,
+ output_shape=(tf.shape(x)[0], tf.shape(x)[1] * self.N,
+ self.N))
+ x = tf.pad(x, [[0, 0], [self.taps // 2, self.taps // 2], [0, 0]], constant_values=0.0)
+ x = tf.nn.conv1d(x, self.G, stride=1, padding="VALID")
+ x = tf.transpose(x, perm=[0, 2, 1])
+ return x
diff --git a/vocoder/tf/models/melgan_generator.py b/vocoder/tf/models/melgan_generator.py
new file mode 100644
index 00000000..8a2a49be
--- /dev/null
+++ b/vocoder/tf/models/melgan_generator.py
@@ -0,0 +1,103 @@
+import logging
+import os
+
+os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' # FATAL
+logging.getLogger('tensorflow').setLevel(logging.FATAL)
+
+import tensorflow as tf
+from TTS.vocoder.tf.layers.melgan import ResidualStack, ReflectionPad1d
+
+
+class MelganGenerator(tf.keras.models.Model): # pylint: disable=too-many-ancestors
+ """ Melgan Generator TF implementation dedicated for inference with no
+ weight norm """
+ def __init__(self,
+ in_channels=80,
+ out_channels=1,
+ proj_kernel=7,
+ base_channels=512,
+ upsample_factors=(8, 8, 2, 2),
+ res_kernel=3,
+ num_res_blocks=3):
+ super(MelganGenerator, self).__init__()
+
+ self.in_channels = in_channels
+
+ # assert model parameters
+ assert (proj_kernel -
+ 1) % 2 == 0, " [!] proj_kernel should be an odd number."
+
+ # setup additional model parameters
+ base_padding = (proj_kernel - 1) // 2
+ act_slope = 0.2
+ self.inference_padding = 2
+
+ # initial layer
+ self.initial_layer = [
+ ReflectionPad1d(base_padding),
+ tf.keras.layers.Conv2D(filters=base_channels,
+ kernel_size=(proj_kernel, 1),
+ strides=1,
+ padding='valid',
+ use_bias=True,
+ name="1")
+ ]
+ num_layers = 3 # count number of layers for layer naming
+
+ # upsampling layers and residual stacks
+ self.upsample_layers = []
+ for idx, upsample_factor in enumerate(upsample_factors):
+ layer_out_channels = base_channels // (2**(idx + 1))
+ layer_filter_size = upsample_factor * 2
+ layer_stride = upsample_factor
+ # layer_output_padding = upsample_factor % 2
+ self.upsample_layers += [
+ tf.keras.layers.LeakyReLU(act_slope),
+ tf.keras.layers.Conv2DTranspose(
+ filters=layer_out_channels,
+ kernel_size=(layer_filter_size, 1),
+ strides=(layer_stride, 1),
+ padding='same',
+ # output_padding=layer_output_padding,
+ use_bias=True,
+ name=f'{num_layers}'),
+ ResidualStack(channels=layer_out_channels,
+ num_res_blocks=num_res_blocks,
+ kernel_size=res_kernel,
+ name=f'layers.{num_layers + 1}')
+ ]
+ num_layers += num_res_blocks - 1
+
+ self.upsample_layers += [tf.keras.layers.LeakyReLU(act_slope)]
+
+ # final layer
+ self.final_layers = [
+ ReflectionPad1d(base_padding),
+ tf.keras.layers.Conv2D(filters=out_channels,
+ kernel_size=(proj_kernel, 1),
+ use_bias=True,
+ name=f'layers.{num_layers + 1}'),
+ tf.keras.layers.Activation("tanh")
+ ]
+
+ # self.model_layers = tf.keras.models.Sequential(self.initial_layer + self.upsample_layers + self.final_layers, name="layers")
+ self.model_layers = self.initial_layer + self.upsample_layers + self.final_layers
+
+ def call(self, c, training=False):
+ """
+ c : B x C x T
+ """
+ if training:
+ raise NotImplementedError()
+ return self.inference(c)
+
+ def inference(self, c):
+ c = tf.transpose(c, perm=[0, 2, 1])
+ c = tf.expand_dims(c, 2)
+ c = tf.pad(c, [[0, 0], [self.inference_padding, self.inference_padding], [0, 0], [0, 0]], "REFLECT")
+ o = c
+ for layer in self.model_layers:
+ o = layer(o)
+ # o = self.model_layers(c)
+ o = tf.transpose(o, perm=[0, 3, 2, 1])
+ return o[:, :, 0, :]
\ No newline at end of file
diff --git a/vocoder/tf/models/multiband_melgan_generator.py b/vocoder/tf/models/multiband_melgan_generator.py
new file mode 100644
index 00000000..d3e1c754
--- /dev/null
+++ b/vocoder/tf/models/multiband_melgan_generator.py
@@ -0,0 +1,46 @@
+import tensorflow as tf
+
+from TTS.vocoder.tf.models.melgan_generator import MelganGenerator
+from TTS.vocoder.tf.layers.pqmf import PQMF
+
+
+class MultibandMelganGenerator(MelganGenerator): # pylint: disable=too-many-ancestors
+ def __init__(self,
+ in_channels=80,
+ out_channels=4,
+ proj_kernel=7,
+ base_channels=384,
+ upsample_factors=(2, 8, 2, 2),
+ res_kernel=3,
+ num_res_blocks=3):
+ super(MultibandMelganGenerator,
+ self).__init__(in_channels=in_channels,
+ out_channels=out_channels,
+ proj_kernel=proj_kernel,
+ base_channels=base_channels,
+ upsample_factors=upsample_factors,
+ res_kernel=res_kernel,
+ num_res_blocks=num_res_blocks)
+ self.pqmf_layer = PQMF(N=4, taps=62, cutoff=0.15, beta=9.0)
+
+ def pqmf_analysis(self, x):
+ return self.pqmf_layer.analysis(x)
+
+ def pqmf_synthesis(self, x):
+ return self.pqmf_layer.synthesis(x)
+
+ # def call(self, c, training=False):
+ # if training:
+ # raise NotImplementedError()
+ # return self.inference(c)
+
+ def inference(self, c):
+ c = tf.transpose(c, perm=[0, 2, 1])
+ c = tf.expand_dims(c, 2)
+ c = tf.pad(c, [[0, 0], [self.inference_padding, self.inference_padding], [0, 0], [0, 0]], "REFLECT")
+ o = c
+ for layer in self.model_layers:
+ o = layer(o)
+ o = tf.transpose(o, perm=[0, 3, 2, 1])
+ o = self.pqmf_layer.synthesis(o[:, :, 0, :])
+ return o
diff --git a/vocoder/tf/utils/__init__.py b/vocoder/tf/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vocoder/tf/utils/convert_torch_to_tf_utils.py b/vocoder/tf/utils/convert_torch_to_tf_utils.py
new file mode 100644
index 00000000..25139cc3
--- /dev/null
+++ b/vocoder/tf/utils/convert_torch_to_tf_utils.py
@@ -0,0 +1,45 @@
+import numpy as np
+import tensorflow as tf
+
+
+def compare_torch_tf(torch_tensor, tf_tensor):
+ """ Compute the average absolute difference b/w torch and tf tensors """
+ return abs(torch_tensor.detach().numpy() - tf_tensor.numpy()).mean()
+
+
+def convert_tf_name(tf_name):
+ """ Convert certain patterns in TF layer names to Torch patterns """
+ tf_name_tmp = tf_name
+ tf_name_tmp = tf_name_tmp.replace(':0', '')
+ tf_name_tmp = tf_name_tmp.replace('/forward_lstm/lstm_cell_1/recurrent_kernel', '/weight_hh_l0')
+ tf_name_tmp = tf_name_tmp.replace('/forward_lstm/lstm_cell_2/kernel', '/weight_ih_l1')
+ tf_name_tmp = tf_name_tmp.replace('/recurrent_kernel', '/weight_hh')
+ tf_name_tmp = tf_name_tmp.replace('/kernel', '/weight')
+ tf_name_tmp = tf_name_tmp.replace('/gamma', '/weight')
+ tf_name_tmp = tf_name_tmp.replace('/beta', '/bias')
+ tf_name_tmp = tf_name_tmp.replace('/', '.')
+ return tf_name_tmp
+
+
+def transfer_weights_torch_to_tf(tf_vars, var_map_dict, state_dict):
+ """ Transfer weigths from torch state_dict to TF variables """
+ print(" > Passing weights from Torch to TF ...")
+ for tf_var in tf_vars:
+ torch_var_name = var_map_dict[tf_var.name]
+ print(f' | > {tf_var.name} <-- {torch_var_name}')
+ # if tuple, it is a bias variable
+ if 'kernel' in tf_var.name:
+ torch_weight = state_dict[torch_var_name]
+ numpy_weight = torch_weight.permute([2, 1, 0]).numpy()[:, None, :, :]
+ if 'bias' in tf_var.name:
+ torch_weight = state_dict[torch_var_name]
+ numpy_weight = torch_weight
+ assert np.all(tf_var.shape == numpy_weight.shape), f" [!] weight shapes does not match: {tf_var.name} vs {torch_var_name} --> {tf_var.shape} vs {numpy_weight.shape}"
+ tf.keras.backend.set_value(tf_var, numpy_weight)
+ return tf_vars
+
+
+def load_tf_vars(model_tf, tf_vars):
+ for tf_var in tf_vars:
+ model_tf.get_layer(tf_var.name).set_weights(tf_var)
+ return model_tf
diff --git a/vocoder/tf/utils/generic_utils.py b/vocoder/tf/utils/generic_utils.py
new file mode 100644
index 00000000..580a3738
--- /dev/null
+++ b/vocoder/tf/utils/generic_utils.py
@@ -0,0 +1,35 @@
+import re
+import importlib
+
+
+def to_camel(text):
+ text = text.capitalize()
+ return re.sub(r'(?!^)_([a-zA-Z])', lambda m: m.group(1).upper(), text)
+
+
+def setup_generator(c):
+ print(" > Generator Model: {}".format(c.generator_model))
+ MyModel = importlib.import_module('TTS.vocoder.tf.models.' +
+ c.generator_model.lower())
+ MyModel = getattr(MyModel, to_camel(c.generator_model))
+ if c.generator_model in 'melgan_generator':
+ model = MyModel(
+ in_channels=c.audio['num_mels'],
+ out_channels=1,
+ proj_kernel=7,
+ base_channels=512,
+ upsample_factors=c.generator_model_params['upsample_factors'],
+ res_kernel=3,
+ num_res_blocks=c.generator_model_params['num_res_blocks'])
+ if c.generator_model in 'melgan_fb_generator':
+ pass
+ if c.generator_model in 'multiband_melgan_generator':
+ model = MyModel(
+ in_channels=c.audio['num_mels'],
+ out_channels=4,
+ proj_kernel=7,
+ base_channels=384,
+ upsample_factors=c.generator_model_params['upsample_factors'],
+ res_kernel=3,
+ num_res_blocks=c.generator_model_params['num_res_blocks'])
+ return model
\ No newline at end of file
diff --git a/vocoder/tf/utils/io.py b/vocoder/tf/utils/io.py
new file mode 100644
index 00000000..d95d972c
--- /dev/null
+++ b/vocoder/tf/utils/io.py
@@ -0,0 +1,27 @@
+import datetime
+import pickle
+import tensorflow as tf
+
+
+def save_checkpoint(model, current_step, epoch, output_path, **kwargs):
+ """ Save TF Vocoder model """
+ state = {
+ 'model': model.weights,
+ 'step': current_step,
+ 'epoch': epoch,
+ 'date': datetime.date.today().strftime("%B %d, %Y"),
+ }
+ state.update(kwargs)
+ pickle.dump(state, open(output_path, 'wb'))
+
+
+def load_checkpoint(model, checkpoint_path):
+ """ Load TF Vocoder model """
+ checkpoint = pickle.load(open(checkpoint_path, 'rb'))
+ chkp_var_dict = {var.name: var.numpy() for var in checkpoint['model']}
+ tf_vars = model.weights
+ for tf_var in tf_vars:
+ layer_name = tf_var.name
+ chkp_var_value = chkp_var_dict[layer_name]
+ tf.keras.backend.set_value(tf_var, chkp_var_value)
+ return model
\ No newline at end of file
diff --git a/vocoder/train.py b/vocoder/train.py
new file mode 100644
index 00000000..fd44c470
--- /dev/null
+++ b/vocoder/train.py
@@ -0,0 +1,658 @@
+import argparse
+import glob
+import os
+import sys
+import time
+import traceback
+
+import torch
+from torch.utils.data import DataLoader
+
+from inspect import signature
+
+from TTS.utils.audio import AudioProcessor
+from TTS.utils.generic_utils import (KeepAverage, count_parameters,
+ create_experiment_folder, get_git_branch,
+ remove_experiment_folder, set_init_dict)
+from TTS.utils.io import copy_config_file, load_config
+from TTS.utils.radam import RAdam
+from TTS.utils.tensorboard_logger import TensorboardLogger
+from TTS.utils.training import setup_torch_training_env
+from TTS.vocoder.datasets.gan_dataset import GANDataset
+from TTS.vocoder.datasets.preprocess import load_wav_data, load_wav_feat_data
+# from distribute import (DistributedSampler, apply_gradient_allreduce,
+# init_distributed, reduce_tensor)
+from TTS.vocoder.layers.losses import DiscriminatorLoss, GeneratorLoss
+from TTS.vocoder.utils.io import save_checkpoint, save_best_model
+from TTS.vocoder.utils.console_logger import ConsoleLogger
+from TTS.vocoder.utils.generic_utils import (check_config, plot_results,
+ setup_discriminator,
+ setup_generator)
+
+
+use_cuda, num_gpus = setup_torch_training_env(True, True)
+
+
+def setup_loader(ap, is_val=False, verbose=False):
+ if is_val and not c.run_eval:
+ loader = None
+ else:
+ dataset = GANDataset(ap=ap,
+ items=eval_data if is_val else train_data,
+ seq_len=c.seq_len,
+ hop_len=ap.hop_length,
+ pad_short=c.pad_short,
+ conv_pad=c.conv_pad,
+ is_training=not is_val,
+ return_segments=not is_val,
+ use_noise_augment=c.use_noise_augment,
+ use_cache=c.use_cache,
+ verbose=verbose)
+ dataset.shuffle_mapping()
+ # sampler = DistributedSampler(dataset) if num_gpus > 1 else None
+ loader = DataLoader(dataset,
+ batch_size=1 if is_val else c.batch_size,
+ shuffle=True,
+ drop_last=False,
+ sampler=None,
+ num_workers=c.num_val_loader_workers
+ if is_val else c.num_loader_workers,
+ pin_memory=False)
+ return loader
+
+
+def format_data(data):
+ if isinstance(data[0], list):
+ # setup input data
+ c_G, x_G = data[0]
+ c_D, x_D = data[1]
+
+ # dispatch data to GPU
+ if use_cuda:
+ c_G = c_G.cuda(non_blocking=True)
+ x_G = x_G.cuda(non_blocking=True)
+ c_D = c_D.cuda(non_blocking=True)
+ x_D = x_D.cuda(non_blocking=True)
+
+ return c_G, x_G, c_D, x_D
+
+ # return a whole audio segment
+ co, x = data
+ if use_cuda:
+ co = co.cuda(non_blocking=True)
+ x = x.cuda(non_blocking=True)
+ return co, x, None, None
+
+
+def train(model_G, criterion_G, optimizer_G, model_D, criterion_D, optimizer_D,
+ scheduler_G, scheduler_D, ap, global_step, epoch):
+ data_loader = setup_loader(ap, is_val=False, verbose=(epoch == 0))
+ model_G.train()
+ model_D.train()
+ epoch_time = 0
+ keep_avg = KeepAverage()
+ if use_cuda:
+ batch_n_iter = int(
+ len(data_loader.dataset) / (c.batch_size * num_gpus))
+ else:
+ batch_n_iter = int(len(data_loader.dataset) / c.batch_size)
+ end_time = time.time()
+ c_logger.print_train_start()
+ for num_iter, data in enumerate(data_loader):
+ start_time = time.time()
+
+ # format data
+ c_G, y_G, c_D, y_D = format_data(data)
+ loader_time = time.time() - end_time
+
+ global_step += 1
+
+ ##############################
+ # GENERATOR
+ ##############################
+
+ # generator pass
+ y_hat = model_G(c_G)
+ y_hat_sub = None
+ y_G_sub = None
+ y_hat_vis = y_hat # for visualization
+
+ # PQMF formatting
+ if y_hat.shape[1] > 1:
+ y_hat_sub = y_hat
+ y_hat = model_G.pqmf_synthesis(y_hat)
+ y_hat_vis = y_hat
+ y_G_sub = model_G.pqmf_analysis(y_G)
+
+ if global_step > c.steps_to_start_discriminator:
+
+ # run D with or without cond. features
+ if len(signature(model_D.forward).parameters) == 2:
+ D_out_fake = model_D(y_hat, c_G)
+ else:
+ D_out_fake = model_D(y_hat)
+ D_out_real = None
+
+ if c.use_feat_match_loss:
+ with torch.no_grad():
+ D_out_real = model_D(y_G)
+
+ # format D outputs
+ if isinstance(D_out_fake, tuple):
+ scores_fake, feats_fake = D_out_fake
+ if D_out_real is None:
+ feats_real = None
+ else:
+ _, feats_real = D_out_real
+ else:
+ scores_fake = D_out_fake
+ else:
+ scores_fake, feats_fake, feats_real = None, None, None
+
+ # compute losses
+ loss_G_dict = criterion_G(y_hat, y_G, scores_fake, feats_fake,
+ feats_real, y_hat_sub, y_G_sub)
+ loss_G = loss_G_dict['G_loss']
+
+ # optimizer generator
+ optimizer_G.zero_grad()
+ loss_G.backward()
+ if c.gen_clip_grad > 0:
+ torch.nn.utils.clip_grad_norm_(model_G.parameters(),
+ c.gen_clip_grad)
+ optimizer_G.step()
+ if scheduler_G is not None:
+ scheduler_G.step()
+
+ loss_dict = dict()
+ for key, value in loss_G_dict.items():
+ if isinstance(value, int):
+ loss_dict[key] = value
+ else:
+ loss_dict[key] = value.item()
+
+ ##############################
+ # DISCRIMINATOR
+ ##############################
+ if global_step >= c.steps_to_start_discriminator:
+ # discriminator pass
+ with torch.no_grad():
+ y_hat = model_G(c_D)
+
+ # PQMF formatting
+ if y_hat.shape[1] > 1:
+ y_hat = model_G.pqmf_synthesis(y_hat)
+
+ # run D with or without cond. features
+ if len(signature(model_D.forward).parameters) == 2:
+ D_out_fake = model_D(y_hat.detach(), c_D)
+ D_out_real = model_D(y_D, c_D)
+ else:
+ D_out_fake = model_D(y_hat.detach())
+ D_out_real = model_D(y_D)
+
+ # format D outputs
+ if isinstance(D_out_fake, tuple):
+ scores_fake, feats_fake = D_out_fake
+ if D_out_real is None:
+ scores_real, feats_real = None, None
+ else:
+ scores_real, feats_real = D_out_real
+ else:
+ scores_fake = D_out_fake
+ scores_real = D_out_real
+
+ # compute losses
+ loss_D_dict = criterion_D(scores_fake, scores_real)
+ loss_D = loss_D_dict['D_loss']
+
+ # optimizer discriminator
+ optimizer_D.zero_grad()
+ loss_D.backward()
+ if c.disc_clip_grad > 0:
+ torch.nn.utils.clip_grad_norm_(model_D.parameters(),
+ c.disc_clip_grad)
+ optimizer_D.step()
+ if scheduler_D is not None:
+ scheduler_D.step()
+
+ for key, value in loss_D_dict.items():
+ if isinstance(value, (int, float)):
+ loss_dict[key] = value
+ else:
+ loss_dict[key] = value.item()
+
+ step_time = time.time() - start_time
+ epoch_time += step_time
+
+ # get current learning rates
+ current_lr_G = list(optimizer_G.param_groups)[0]['lr']
+ current_lr_D = list(optimizer_D.param_groups)[0]['lr']
+
+ # update avg stats
+ update_train_values = dict()
+ for key, value in loss_dict.items():
+ update_train_values['avg_' + key] = value
+ update_train_values['avg_loader_time'] = loader_time
+ update_train_values['avg_step_time'] = step_time
+ keep_avg.update_values(update_train_values)
+
+ # print training stats
+ if global_step % c.print_step == 0:
+ c_logger.print_train_step(batch_n_iter, num_iter, global_step,
+ step_time, loader_time, current_lr_G,
+ current_lr_D, loss_dict,
+ keep_avg.avg_values)
+
+ # plot step stats
+ if global_step % 10 == 0:
+ iter_stats = {
+ "lr_G": current_lr_G,
+ "lr_D": current_lr_D,
+ "step_time": step_time
+ }
+ iter_stats.update(loss_dict)
+ tb_logger.tb_train_iter_stats(global_step, iter_stats)
+
+ # save checkpoint
+ if global_step % c.save_step == 0:
+ if c.checkpoint:
+ # save model
+ save_checkpoint(model_G,
+ optimizer_G,
+ scheduler_G,
+ model_D,
+ optimizer_D,
+ scheduler_D,
+ global_step,
+ epoch,
+ OUT_PATH,
+ model_losses=loss_dict)
+
+ # compute spectrograms
+ figures = plot_results(y_hat_vis, y_G, ap, global_step,
+ 'train')
+ tb_logger.tb_train_figures(global_step, figures)
+
+ # Sample audio
+ sample_voice = y_hat_vis[0].squeeze(0).detach().cpu().numpy()
+ tb_logger.tb_train_audios(global_step,
+ {'train/audio': sample_voice},
+ c.audio["sample_rate"])
+ end_time = time.time()
+
+ # print epoch stats
+ c_logger.print_train_epoch_end(global_step, epoch, epoch_time, keep_avg)
+
+ # Plot Training Epoch Stats
+ epoch_stats = {"epoch_time": epoch_time}
+ epoch_stats.update(keep_avg.avg_values)
+ tb_logger.tb_train_epoch_stats(global_step, epoch_stats)
+ # TODO: plot model stats
+ # if c.tb_model_param_stats:
+ # tb_logger.tb_model_weights(model, global_step)
+ return keep_avg.avg_values, global_step
+
+
+@torch.no_grad()
+def evaluate(model_G, criterion_G, model_D, criterion_D, ap, global_step, epoch):
+ data_loader = setup_loader(ap, is_val=True, verbose=(epoch == 0))
+ model_G.eval()
+ model_D.eval()
+ epoch_time = 0
+ keep_avg = KeepAverage()
+ end_time = time.time()
+ c_logger.print_eval_start()
+ for num_iter, data in enumerate(data_loader):
+ start_time = time.time()
+
+ # format data
+ c_G, y_G, _, _ = format_data(data)
+ loader_time = time.time() - end_time
+
+ global_step += 1
+
+ ##############################
+ # GENERATOR
+ ##############################
+
+ # generator pass
+ y_hat = model_G(c_G)
+ y_hat_sub = None
+ y_G_sub = None
+
+ # PQMF formatting
+ if y_hat.shape[1] > 1:
+ y_hat_sub = y_hat
+ y_hat = model_G.pqmf_synthesis(y_hat)
+ y_G_sub = model_G.pqmf_analysis(y_G)
+
+
+ if global_step > c.steps_to_start_discriminator:
+
+ if len(signature(model_D.forward).parameters) == 2:
+ D_out_fake = model_D(y_hat, c_G)
+ else:
+ D_out_fake = model_D(y_hat)
+ D_out_real = None
+
+ if c.use_feat_match_loss:
+ with torch.no_grad():
+ D_out_real = model_D(y_G)
+
+ # format D outputs
+ if isinstance(D_out_fake, tuple):
+ scores_fake, feats_fake = D_out_fake
+ if D_out_real is None:
+ feats_real = None
+ else:
+ _, feats_real = D_out_real
+ else:
+ scores_fake = D_out_fake
+ else:
+ scores_fake, feats_fake, feats_real = None, None, None
+
+ # compute losses
+ loss_G_dict = criterion_G(y_hat, y_G, scores_fake, feats_fake,
+ feats_real, y_hat_sub, y_G_sub)
+
+ loss_dict = dict()
+ for key, value in loss_G_dict.items():
+ if isinstance(value, (int, float)):
+ loss_dict[key] = value
+ else:
+ loss_dict[key] = value.item()
+
+ ##############################
+ # DISCRIMINATOR
+ ##############################
+
+ if global_step >= c.steps_to_start_discriminator:
+ # discriminator pass
+ with torch.no_grad():
+ y_hat = model_G(c_G)
+
+ # PQMF formatting
+ if y_hat.shape[1] > 1:
+ y_hat = model_G.pqmf_synthesis(y_hat)
+
+ # run D with or without cond. features
+ if len(signature(model_D.forward).parameters) == 2:
+ D_out_fake = model_D(y_hat.detach(), c_G)
+ D_out_real = model_D(y_G, c_G)
+ else:
+ D_out_fake = model_D(y_hat.detach())
+ D_out_real = model_D(y_G)
+
+ # format D outputs
+ if isinstance(D_out_fake, tuple):
+ scores_fake, feats_fake = D_out_fake
+ if D_out_real is None:
+ scores_real, feats_real = None, None
+ else:
+ scores_real, feats_real = D_out_real
+ else:
+ scores_fake = D_out_fake
+ scores_real = D_out_real
+
+ # compute losses
+ loss_D_dict = criterion_D(scores_fake, scores_real)
+
+ for key, value in loss_D_dict.items():
+ if isinstance(value, (int, float)):
+ loss_dict[key] = value
+ else:
+ loss_dict[key] = value.item()
+
+
+ step_time = time.time() - start_time
+ epoch_time += step_time
+
+ # update avg stats
+ update_eval_values = dict()
+ for key, value in loss_dict.items():
+ update_eval_values['avg_' + key] = value
+ update_eval_values['avg_loader_time'] = loader_time
+ update_eval_values['avg_step_time'] = step_time
+ keep_avg.update_values(update_eval_values)
+
+ # print eval stats
+ if c.print_eval:
+ c_logger.print_eval_step(num_iter, loss_dict, keep_avg.avg_values)
+
+ # compute spectrograms
+ figures = plot_results(y_hat, y_G, ap, global_step, 'eval')
+ tb_logger.tb_eval_figures(global_step, figures)
+
+ # Sample audio
+ sample_voice = y_hat[0].squeeze(0).detach().cpu().numpy()
+ tb_logger.tb_eval_audios(global_step, {'eval/audio': sample_voice},
+ c.audio["sample_rate"])
+
+ # synthesize a full voice
+ data_loader.return_segments = False
+
+ tb_logger.tb_eval_stats(global_step, keep_avg.avg_values)
+
+ return keep_avg.avg_values
+
+
+# FIXME: move args definition/parsing inside of main?
+def main(args): # pylint: disable=redefined-outer-name
+ # pylint: disable=global-variable-undefined
+ global train_data, eval_data
+ print(f" > Loading wavs from: {c.data_path}")
+ if c.feature_path is not None:
+ print(f" > Loading features from: {c.feature_path}")
+ eval_data, train_data = load_wav_feat_data(c.data_path, c.feature_path, c.eval_split_size)
+ else:
+ eval_data, train_data = load_wav_data(c.data_path, c.eval_split_size)
+
+ # setup audio processor
+ ap = AudioProcessor(**c.audio)
+
+ # DISTRUBUTED
+ # if num_gpus > 1:
+ # init_distributed(args.rank, num_gpus, args.group_id,
+ # c.distributed["backend"], c.distributed["url"])
+
+ # setup models
+ model_gen = setup_generator(c)
+ model_disc = setup_discriminator(c)
+
+ # setup optimizers
+ optimizer_gen = RAdam(model_gen.parameters(), lr=c.lr_gen, weight_decay=0)
+ optimizer_disc = RAdam(model_disc.parameters(),
+ lr=c.lr_disc,
+ weight_decay=0)
+
+ # schedulers
+ scheduler_gen = None
+ scheduler_disc = None
+ if 'lr_scheduler_gen' in c:
+ scheduler_gen = getattr(torch.optim.lr_scheduler, c.lr_scheduler_gen)
+ scheduler_gen = scheduler_gen(optimizer_gen, **c.lr_scheduler_gen_params)
+ if 'lr_scheduler_disc' in c:
+ scheduler_disc = getattr(torch.optim.lr_scheduler, c.lr_scheduler_disc)
+ scheduler_disc = scheduler_disc(optimizer_disc, **c.lr_scheduler_disc_params)
+
+ # setup criterion
+ criterion_gen = GeneratorLoss(c)
+ criterion_disc = DiscriminatorLoss(c)
+
+ if args.restore_path:
+ checkpoint = torch.load(args.restore_path, map_location='cpu')
+ try:
+ print(" > Restoring Generator Model...")
+ model_gen.load_state_dict(checkpoint['model'])
+ print(" > Restoring Generator Optimizer...")
+ optimizer_gen.load_state_dict(checkpoint['optimizer'])
+ print(" > Restoring Discriminator Model...")
+ model_disc.load_state_dict(checkpoint['model_disc'])
+ print(" > Restoring Discriminator Optimizer...")
+ optimizer_disc.load_state_dict(checkpoint['optimizer_disc'])
+ if 'scheduler' in checkpoint:
+ print(" > Restoring Generator LR Scheduler...")
+ scheduler_gen.load_state_dict(checkpoint['scheduler'])
+ # NOTE: Not sure if necessary
+ scheduler_gen.optimizer = optimizer_gen
+ if 'scheduler_disc' in checkpoint:
+ print(" > Restoring Discriminator LR Scheduler...")
+ scheduler_disc.load_state_dict(checkpoint['scheduler_disc'])
+ scheduler_disc.optimizer = optimizer_disc
+ except RuntimeError:
+ # retore only matching layers.
+ print(" > Partial model initialization...")
+ model_dict = model_gen.state_dict()
+ model_dict = set_init_dict(model_dict, checkpoint['model'], c)
+ model_gen.load_state_dict(model_dict)
+
+ model_dict = model_disc.state_dict()
+ model_dict = set_init_dict(model_dict, checkpoint['model_disc'], c)
+ model_disc.load_state_dict(model_dict)
+ del model_dict
+
+ # reset lr if not countinuining training.
+ for group in optimizer_gen.param_groups:
+ group['lr'] = c.lr_gen
+
+ for group in optimizer_disc.param_groups:
+ group['lr'] = c.lr_disc
+
+ print(" > Model restored from step %d" % checkpoint['step'],
+ flush=True)
+ args.restore_step = checkpoint['step']
+ else:
+ args.restore_step = 0
+
+ if use_cuda:
+ model_gen.cuda()
+ criterion_gen.cuda()
+ model_disc.cuda()
+ criterion_disc.cuda()
+
+ # DISTRUBUTED
+ # if num_gpus > 1:
+ # model = apply_gradient_allreduce(model)
+
+ num_params = count_parameters(model_gen)
+ print(" > Generator has {} parameters".format(num_params), flush=True)
+ num_params = count_parameters(model_disc)
+ print(" > Discriminator has {} parameters".format(num_params), flush=True)
+
+ if 'best_loss' not in locals():
+ best_loss = float('inf')
+
+ global_step = args.restore_step
+ for epoch in range(0, c.epochs):
+ c_logger.print_epoch_start(epoch, c.epochs)
+ _, global_step = train(model_gen, criterion_gen, optimizer_gen,
+ model_disc, criterion_disc, optimizer_disc,
+ scheduler_gen, scheduler_disc, ap, global_step,
+ epoch)
+ eval_avg_loss_dict = evaluate(model_gen, criterion_gen, model_disc, criterion_disc, ap,
+ global_step, epoch)
+ c_logger.print_epoch_end(epoch, eval_avg_loss_dict)
+ target_loss = eval_avg_loss_dict[c.target_loss]
+ best_loss = save_best_model(target_loss,
+ best_loss,
+ model_gen,
+ optimizer_gen,
+ scheduler_gen,
+ model_disc,
+ optimizer_disc,
+ scheduler_disc,
+ global_step,
+ epoch,
+ OUT_PATH,
+ model_losses=eval_avg_loss_dict)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '--continue_path',
+ type=str,
+ help=
+ 'Training output folder to continue training. Use to continue a training. If it is used, "config_path" is ignored.',
+ default='',
+ required='--config_path' not in sys.argv)
+ parser.add_argument(
+ '--restore_path',
+ type=str,
+ help='Model file to be restored. Use to finetune a model.',
+ default='')
+ parser.add_argument('--config_path',
+ type=str,
+ help='Path to config file for training.',
+ required='--continue_path' not in sys.argv)
+ parser.add_argument('--debug',
+ type=bool,
+ default=False,
+ help='Do not verify commit integrity to run training.')
+
+ # DISTRUBUTED
+ parser.add_argument(
+ '--rank',
+ type=int,
+ default=0,
+ help='DISTRIBUTED: process rank for distributed training.')
+ parser.add_argument('--group_id',
+ type=str,
+ default="",
+ help='DISTRIBUTED: process group id.')
+ args = parser.parse_args()
+
+ if args.continue_path != '':
+ args.output_path = args.continue_path
+ args.config_path = os.path.join(args.continue_path, 'config.json')
+ list_of_files = glob.glob(
+ args.continue_path +
+ "/*.pth.tar") # * means all if need specific format then *.csv
+ latest_model_file = max(list_of_files, key=os.path.getctime)
+ args.restore_path = latest_model_file
+ print(f" > Training continues for {args.restore_path}")
+
+ # setup output paths and read configs
+ c = load_config(args.config_path)
+ check_config(c)
+ _ = os.path.dirname(os.path.realpath(__file__))
+
+ OUT_PATH = args.continue_path
+ if args.continue_path == '':
+ OUT_PATH = create_experiment_folder(c.output_path, c.run_name,
+ args.debug)
+
+ AUDIO_PATH = os.path.join(OUT_PATH, 'test_audios')
+
+ c_logger = ConsoleLogger()
+
+ if args.rank == 0:
+ os.makedirs(AUDIO_PATH, exist_ok=True)
+ new_fields = {}
+ if args.restore_path:
+ new_fields["restore_path"] = args.restore_path
+ new_fields["github_branch"] = get_git_branch()
+ copy_config_file(args.config_path,
+ os.path.join(OUT_PATH, 'config.json'), new_fields)
+ os.chmod(AUDIO_PATH, 0o775)
+ os.chmod(OUT_PATH, 0o775)
+
+ LOG_DIR = OUT_PATH
+ tb_logger = TensorboardLogger(LOG_DIR, model_name='VOCODER')
+
+ # write model desc to tensorboard
+ tb_logger.tb_add_text('model-description', c['run_description'], 0)
+
+ try:
+ main(args)
+ except KeyboardInterrupt:
+ remove_experiment_folder(OUT_PATH)
+ try:
+ sys.exit(0)
+ except SystemExit:
+ os._exit(0) # pylint: disable=protected-access
+ except Exception: # pylint: disable=broad-except
+ remove_experiment_folder(OUT_PATH)
+ traceback.print_exc()
+ sys.exit(1)
diff --git a/vocoder/utils/__init__.py b/vocoder/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vocoder/utils/console_logger.py b/vocoder/utils/console_logger.py
new file mode 100644
index 00000000..4f44986c
--- /dev/null
+++ b/vocoder/utils/console_logger.py
@@ -0,0 +1,97 @@
+import datetime
+from TTS.utils.io import AttrDict
+
+
+tcolors = AttrDict({
+ 'OKBLUE': '\033[94m',
+ 'HEADER': '\033[95m',
+ 'OKGREEN': '\033[92m',
+ 'WARNING': '\033[93m',
+ 'FAIL': '\033[91m',
+ 'ENDC': '\033[0m',
+ 'BOLD': '\033[1m',
+ 'UNDERLINE': '\033[4m'
+})
+
+
+class ConsoleLogger():
+ # TODO: merge this with TTS ConsoleLogger
+ def __init__(self):
+ # use these to compare values between iterations
+ self.old_train_loss_dict = None
+ self.old_epoch_loss_dict = None
+ self.old_eval_loss_dict = None
+
+ # pylint: disable=no-self-use
+ def get_time(self):
+ now = datetime.datetime.now()
+ return now.strftime("%Y-%m-%d %H:%M:%S")
+
+ def print_epoch_start(self, epoch, max_epoch):
+ print("\n{}{} > EPOCH: {}/{}{}".format(tcolors.UNDERLINE, tcolors.BOLD,
+ epoch, max_epoch, tcolors.ENDC),
+ flush=True)
+
+ def print_train_start(self):
+ print(f"\n{tcolors.BOLD} > TRAINING ({self.get_time()}) {tcolors.ENDC}")
+
+ def print_train_step(self, batch_steps, step, global_step,
+ step_time, loader_time, lrG, lrD,
+ loss_dict, avg_loss_dict):
+ indent = " | > "
+ print()
+ log_text = "{} --> STEP: {}/{} -- GLOBAL_STEP: {}{}\n".format(
+ tcolors.BOLD, step, batch_steps, global_step, tcolors.ENDC)
+ for key, value in loss_dict.items():
+ # print the avg value if given
+ if f'avg_{key}' in avg_loss_dict.keys():
+ log_text += "{}{}: {:.5f} ({:.5f})\n".format(indent, key, value, avg_loss_dict[f'avg_{key}'])
+ else:
+ log_text += "{}{}: {:.5f} \n".format(indent, key, value)
+ log_text += f"{indent}step_time: {step_time:.2f}\n{indent}loader_time: {loader_time:.2f}\n{indent}lrG: {lrG}\n{indent}lrD: {lrD}"
+ print(log_text, flush=True)
+
+ # pylint: disable=unused-argument
+ def print_train_epoch_end(self, global_step, epoch, epoch_time,
+ print_dict):
+ indent = " | > "
+ log_text = f"\n{tcolors.BOLD} --> TRAIN PERFORMACE -- EPOCH TIME: {epoch} sec -- GLOBAL_STEP: {global_step}{tcolors.ENDC}\n"
+ for key, value in print_dict.items():
+ log_text += "{}{}: {:.5f}\n".format(indent, key, value)
+ print(log_text, flush=True)
+
+ def print_eval_start(self):
+ print(f"{tcolors.BOLD} > EVALUATION {tcolors.ENDC}\n")
+
+ def print_eval_step(self, step, loss_dict, avg_loss_dict):
+ indent = " | > "
+ print()
+ log_text = f"{tcolors.BOLD} --> STEP: {step}{tcolors.ENDC}\n"
+ for key, value in loss_dict.items():
+ # print the avg value if given
+ if f'avg_{key}' in avg_loss_dict.keys():
+ log_text += "{}{}: {:.5f} ({:.5f})\n".format(indent, key, value, avg_loss_dict[f'avg_{key}'])
+ else:
+ log_text += "{}{}: {:.5f} \n".format(indent, key, value)
+ print(log_text, flush=True)
+
+ def print_epoch_end(self, epoch, avg_loss_dict):
+ indent = " | > "
+ log_text = " {}--> EVAL PERFORMANCE{}\n".format(
+ tcolors.BOLD, tcolors.ENDC)
+ for key, value in avg_loss_dict.items():
+ # print the avg value if given
+ color = ''
+ sign = '+'
+ diff = 0
+ if self.old_eval_loss_dict is not None and key in self.old_eval_loss_dict:
+ diff = value - self.old_eval_loss_dict[key]
+ if diff < 0:
+ color = tcolors.OKGREEN
+ sign = ''
+ elif diff > 0:
+ color = tcolors.FAIL
+ sign = '+'
+ log_text += "{}{}:{} {:.5f} {}({}{:.5f})\n".format(indent, key, color, value, tcolors.ENDC, sign, diff)
+ self.old_eval_loss_dict = avg_loss_dict
+ print(log_text, flush=True)
diff --git a/vocoder/utils/generic_utils.py b/vocoder/utils/generic_utils.py
new file mode 100644
index 00000000..80c97f1a
--- /dev/null
+++ b/vocoder/utils/generic_utils.py
@@ -0,0 +1,102 @@
+import re
+import importlib
+import numpy as np
+from matplotlib import pyplot as plt
+
+from TTS.utils.visual import plot_spectrogram
+
+
+def plot_results(y_hat, y, ap, global_step, name_prefix):
+ """ Plot vocoder model results """
+
+ # select an instance from batch
+ y_hat = y_hat[0].squeeze(0).detach().cpu().numpy()
+ y = y[0].squeeze(0).detach().cpu().numpy()
+
+ spec_fake = ap.melspectrogram(y_hat).T
+ spec_real = ap.melspectrogram(y).T
+ spec_diff = np.abs(spec_fake - spec_real)
+
+ # plot figure and save it
+ fig_wave = plt.figure()
+ plt.subplot(2, 1, 1)
+ plt.plot(y)
+ plt.title("groundtruth speech")
+ plt.subplot(2, 1, 2)
+ plt.plot(y_hat)
+ plt.title(f"generated speech @ {global_step} steps")
+ plt.tight_layout()
+ plt.close()
+
+ figures = {
+ name_prefix + "spectrogram/fake": plot_spectrogram(spec_fake),
+ name_prefix + "spectrogram/real": plot_spectrogram(spec_real),
+ name_prefix + "spectrogram/diff": plot_spectrogram(spec_diff),
+ name_prefix + "speech_comparison": fig_wave,
+ }
+ return figures
+
+
+def to_camel(text):
+ text = text.capitalize()
+ return re.sub(r'(?!^)_([a-zA-Z])', lambda m: m.group(1).upper(), text)
+
+
+def setup_generator(c):
+ print(" > Generator Model: {}".format(c.generator_model))
+ MyModel = importlib.import_module('TTS.vocoder.models.' +
+ c.generator_model.lower())
+ MyModel = getattr(MyModel, to_camel(c.generator_model))
+ if c.generator_model in 'melgan_generator':
+ model = MyModel(
+ in_channels=c.audio['num_mels'],
+ out_channels=1,
+ proj_kernel=7,
+ base_channels=512,
+ upsample_factors=c.generator_model_params['upsample_factors'],
+ res_kernel=3,
+ num_res_blocks=c.generator_model_params['num_res_blocks'])
+ if c.generator_model in 'melgan_fb_generator':
+ pass
+ if c.generator_model in 'multiband_melgan_generator':
+ model = MyModel(
+ in_channels=c.audio['num_mels'],
+ out_channels=4,
+ proj_kernel=7,
+ base_channels=384,
+ upsample_factors=c.generator_model_params['upsample_factors'],
+ res_kernel=3,
+ num_res_blocks=c.generator_model_params['num_res_blocks'])
+ return model
+
+
+def setup_discriminator(c):
+ print(" > Discriminator Model: {}".format(c.discriminator_model))
+ MyModel = importlib.import_module('TTS.vocoder.models.' +
+ c.discriminator_model.lower())
+ MyModel = getattr(MyModel, to_camel(c.discriminator_model))
+ if c.discriminator_model in 'random_window_discriminator':
+ model = MyModel(
+ cond_channels=c.audio['num_mels'],
+ hop_length=c.audio['hop_length'],
+ uncond_disc_donwsample_factors=c.
+ discriminator_model_params['uncond_disc_donwsample_factors'],
+ cond_disc_downsample_factors=c.
+ discriminator_model_params['cond_disc_downsample_factors'],
+ cond_disc_out_channels=c.
+ discriminator_model_params['cond_disc_out_channels'],
+ window_sizes=c.discriminator_model_params['window_sizes'])
+ if c.discriminator_model in 'melgan_multiscale_discriminator':
+ model = MyModel(
+ in_channels=1,
+ out_channels=1,
+ kernel_sizes=(5, 3),
+ base_channels=c.discriminator_model_params['base_channels'],
+ max_channels=c.discriminator_model_params['max_channels'],
+ downsample_factors=c.
+ discriminator_model_params['downsample_factors'])
+ return model
+
+
+def check_config(c):
+ pass
\ No newline at end of file
diff --git a/vocoder/utils/io.py b/vocoder/utils/io.py
new file mode 100644
index 00000000..9d350238
--- /dev/null
+++ b/vocoder/utils/io.py
@@ -0,0 +1,63 @@
+import os
+import torch
+import datetime
+
+
+def save_model(model, optimizer, scheduler, model_disc, optimizer_disc,
+ scheduler_disc, current_step, epoch, output_path, **kwargs):
+ model_state = model.state_dict()
+ model_disc_state = model_disc.state_dict()\
+ if model_disc is not None else None
+ optimizer_state = optimizer.state_dict()\
+ if optimizer is not None else None
+ optimizer_disc_state = optimizer_disc.state_dict()\
+ if optimizer_disc is not None else None
+ scheduler_state = scheduler.state_dict()\
+ if scheduler is not None else None
+ scheduler_disc_state = scheduler_disc.state_dict()\
+ if scheduler_disc is not None else None
+ state = {
+ 'model': model_state,
+ 'optimizer': optimizer_state,
+ 'scheduler': scheduler_state,
+ 'model_disc': model_disc_state,
+ 'optimizer_disc': optimizer_disc_state,
+ 'scheduler_disc': scheduler_disc_state,
+ 'step': current_step,
+ 'epoch': epoch,
+ 'date': datetime.date.today().strftime("%B %d, %Y"),
+ }
+ state.update(kwargs)
+ torch.save(state, output_path)
+
+
+def save_checkpoint(model, optimizer, scheduler, model_disc, optimizer_disc,
+ scheduler_disc, current_step, epoch, output_folder,
+ **kwargs):
+ file_name = 'checkpoint_{}.pth.tar'.format(current_step)
+ checkpoint_path = os.path.join(output_folder, file_name)
+ print(" > CHECKPOINT : {}".format(checkpoint_path))
+ save_model(model, optimizer, scheduler, model_disc, optimizer_disc,
+ scheduler_disc, current_step, epoch, checkpoint_path, **kwargs)
+
+
+def save_best_model(target_loss, best_loss, model, optimizer, scheduler,
+ model_disc, optimizer_disc, scheduler_disc, current_step,
+ epoch, output_folder, **kwargs):
+ if target_loss < best_loss:
+ file_name = 'best_model.pth.tar'
+ checkpoint_path = os.path.join(output_folder, file_name)
+ print(" > BEST MODEL : {}".format(checkpoint_path))
+ save_model(model,
+ optimizer,
+ scheduler,
+ model_disc,
+ optimizer_disc,
+ scheduler_disc,
+ current_step,
+ epoch,
+ checkpoint_path,
+ model_loss=target_loss,
+ **kwargs)
+ best_loss = target_loss
+ return best_loss
\ No newline at end of file