Add models for voice conversion

This commit is contained in:
Brandon Thomas
2022-12-07 19:59:01 -05:00
parent 99a9ccf3d8
commit 8f2a815d7d
41 changed files with 4466 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
FakeYou SoftVC
==============
This work is based off research by Michael and Paul, leveraging bshall's SoftVC:
https://bshall.github.io/soft-vc/
These are the repos contained herein:
* https://github.com/bshall/hubert
* https://github.com/bshall/acoustic-model
* https://github.com/bshall/hifigan
And this is our initial notebook:
* [Colab Notebook](https://colab.research.google.com/drive/1g0YsQQY8fzXHmZUWAMzQO60c6OUlyKBl#scrollTo=Sf_rjYw0P83I)
View File
@@ -0,0 +1,132 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# VSCode project settings
.vscode
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Benjamin van Niekerk
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,180 @@
<p align="center">
<a target="_blank" href="https://colab.research.google.com/github/bshall/soft-vc/blob/main/soft-vc-demo.ipynb">
<img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/>
</a>
</p>
# Acoustic-Model
Training and inference scripts for the acoustic models in [A Comparison of Discrete and Soft Speech Units for Improved Voice Conversion](https://ieeexplore.ieee.org/abstract/document/9746484). For more details see [soft-vc](https://github.com/bshall/soft-vc). Audio samples can be found [here](https://bshall.github.io/soft-vc/). Colab demo can be found [here](https://colab.research.google.com/github/bshall/soft-vc/blob/main/soft-vc-demo.ipynb).
<div align="center">
<img width="100%" alt="Soft-VC"
src="https://raw.githubusercontent.com/bshall/acoustic-model/main/acoustic-model.png">
</div>
<div>
<sup>
<strong>Fig 1:</strong> Architecture of the voice conversion system. a) The <strong>discrete</strong> content encoder clusters audio features to produce a sequence of discrete speech units. b) The <strong>soft</strong> content encoder is trained to predict the discrete units. The acoustic model transforms the discrete/soft speech units into a target spectrogram. The vocoder converts the spectrogram into an audio waveform.
</sup>
</div>
## Example Usage
### Programmatic Usage
```python
import torch
import numpy as np
# Load checkpoint (either hubert_soft or hubert_discrete)
acoustic = torch.hub.load("bshall/acoustic-model:main", "hubert_soft").cuda()
# Load speech units
units = torch.from_numpy(np.load("path/to/units"))
# Generate mel-spectrogram
mel = acoustic.generate(units)
```
### Script-Based Usage
```
usage: generate.py [-h] {soft,discrete} in-dir out-dir
Generate spectrograms from input speech units (discrete or soft).
positional arguments:
{soft,discrete} available models (HuBERT-Soft or HuBERT-Discrete)
in-dir path to the dataset directory.
out-dir path to the output directory.
optional arguments:
-h, --help show this help message and exit
```
## Training
### Step 1: Dataset Preparation
Download and extract the [LJSpeech](https://keithito.com/LJ-Speech-Dataset/) dataset. The training script expects the following tree structure for the dataset directory:
```
└───wavs
├───dev
│ ├───LJ001-0001.wav
│ ├───...
│ └───LJ050-0278.wav
└───train
├───LJ002-0332.wav
├───...
└───LJ047-0007.wav
```
The `train` and `dev` directories should contain the training and validation splits respectively. The splits used for the paper can be found [here](https://github.com/bshall/acoustic-model/releases/tag/v0.1).
### Step 2: Extract Spectrograms
Extract mel-spectrograms using the `mel.py` script:
```
usage: mels.py [-h] in-dir out-dir
Extract mel-spectrograms for an audio dataset.
positional arguments:
in-dir path to the dataset directory.
out-dir path to the output directory.
optional arguments:
-h, --help show this help message and exit
```
for example:
```
python mel.py path/to/LJSpeech-1.1/wavs path/to/LJSpeech-1.1/mels
```
At this point the directory tree should look like:
```
├───mels
│ ├───...
└───wavs
├───...
```
### Step 3: Extract Discrete or Soft Speech Units
Use the HuBERT-Soft or HuBERT-Discrete content encoders to extract speech units. First clone the [content encoder repo](https://github.com/bshall/hubert) and then run `encode.py` (see the repo for details):
```
usage: encode.py [-h] [--extension EXTENSION] {soft,discrete} in-dir out-dir
Encode an audio dataset.
positional arguments:
{soft,discrete} available models (HuBERT-Soft or HuBERT-Discrete)
in-dir path to the dataset directory.
out-dir path to the output directory.
optional arguments:
-h, --help show this help message and exit
--extension EXTENSION
extension of the audio files (defaults to .flac).
```
for example:
```
python encode.py soft path/to/LJSpeech-1.1/wavs path/to/LJSpeech-1.1/soft --extension .wav
```
At this point the directory tree should look like:
```
├───mels
│ ├───...
├───soft/discrete
│ ├───...
└───wavs
├───...
```
### Step 4: Train the Acoustic-Model
```
usage: train.py [-h] [--resume RESUME] [--discrete] dataset-dir checkpoint-dir
Train the acoustic model.
positional arguments:
dataset-dir path to the data directory.
checkpoint-dir path to the checkpoint directory.
optional arguments:
-h, --help show this help message and exit
--resume RESUME path to the checkpoint to resume from.
--discrete Use discrete units.
```
## Links
- [Soft-VC repo](https://github.com/bshall/soft-vc)
- [Soft-VC paper](https://ieeexplore.ieee.org/abstract/document/9746484)
- [HuBERT content encoders](https://github.com/bshall/hubert)
- [HiFiGAN vocoder](https://github.com/bshall/hifigan)
## Citation
If you found this work helpful please consider citing our paper:
```
@inproceedings{
soft-vc-2022,
author={van Niekerk, Benjamin and Carbonneau, Marc-André and Zaïdi, Julian and Baas, Matthew and Seuté, Hugo and Kamper, Herman},
booktitle={ICASSP},
title={A Comparison of Discrete and Soft Speech Units for Improved Voice Conversion},
year={2022}
}
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

@@ -0,0 +1 @@
from .model import AcousticModel, hubert_discrete, hubert_soft
@@ -0,0 +1,55 @@
from pathlib import Path
import numpy as np
import torch
import torch.nn.functional as F
from torch.utils.data import Dataset
from torch.nn.utils.rnn import pad_sequence
class MelDataset(Dataset):
def __init__(self, root: Path, train: bool = True, discrete: bool = False):
self.discrete = discrete
self.mels_dir = root / "mels"
self.units_dir = root / "discrete" if discrete else root / "soft"
pattern = "train/**/*.npy" if train else "dev/**/*.npy"
self.metadata = [
path.relative_to(self.mels_dir).with_suffix("")
for path in self.mels_dir.rglob(pattern)
]
def __len__(self):
return len(self.metadata)
def __getitem__(self, index):
path = self.metadata[index]
mel_path = self.mels_dir / path
units_path = self.units_dir / path
mel = np.load(mel_path.with_suffix(".npy")).T
units = np.load(units_path.with_suffix(".npy"))
length = 2 * units.shape[0]
mel = torch.from_numpy(mel[:length, :])
mel = F.pad(mel, (0, 0, 1, 0))
units = torch.from_numpy(units)
if self.discrete:
units = units.long()
return mel, units
def pad_collate(self, batch):
mels, units = zip(*batch)
mels, units = list(mels), list(units)
mels_lengths = torch.tensor([x.size(0) - 1 for x in mels])
units_lengths = torch.tensor([x.size(0) for x in units])
mels = pad_sequence(mels, batch_first=True)
units = pad_sequence(
units, batch_first=True, padding_value=100 if self.discrete else 0
)
return mels, mels_lengths, units, units_lengths
@@ -0,0 +1,168 @@
import torch
import torch.nn as nn
from torch.nn.modules.utils import consume_prefix_in_state_dict_if_present
URLS = {
"hubert-discrete": "https://github.com/bshall/acoustic-model/releases/download/v0.1/hubert-discrete-d49e1c77.pt",
"hubert-soft": "https://github.com/bshall/acoustic-model/releases/download/v0.1/hubert-soft-0321fd7e.pt",
}
class AcousticModel(nn.Module):
def __init__(self, discrete: bool = False, upsample: bool = True):
super().__init__()
self.encoder = Encoder(discrete, upsample)
self.decoder = Decoder()
def forward(self, x: torch.Tensor, mels: torch.Tensor) -> torch.Tensor:
x = self.encoder(x)
return self.decoder(x, mels)
@torch.inference_mode()
def generate(self, x: torch.Tensor) -> torch.Tensor:
x = self.encoder(x)
return self.decoder.generate(x)
class Encoder(nn.Module):
def __init__(self, discrete: bool = False, upsample: bool = True):
super().__init__()
self.embedding = nn.Embedding(100 + 1, 256) if discrete else None
self.prenet = PreNet(256, 256, 256)
self.convs = nn.Sequential(
nn.Conv1d(256, 512, 5, 1, 2),
nn.ReLU(),
nn.InstanceNorm1d(512),
nn.ConvTranspose1d(512, 512, 4, 2, 1) if upsample else nn.Identity(),
nn.Conv1d(512, 512, 5, 1, 2),
nn.ReLU(),
nn.InstanceNorm1d(512),
nn.Conv1d(512, 512, 5, 1, 2),
nn.ReLU(),
nn.InstanceNorm1d(512),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.embedding is not None:
x = self.embedding(x)
x = self.prenet(x)
x = self.convs(x.transpose(1, 2))
return x.transpose(1, 2)
class Decoder(nn.Module):
def __init__(self):
super().__init__()
self.prenet = PreNet(128, 256, 256)
self.lstm1 = nn.LSTM(512 + 256, 768, batch_first=True)
self.lstm2 = nn.LSTM(768, 768, batch_first=True)
self.lstm3 = nn.LSTM(768, 768, batch_first=True)
self.proj = nn.Linear(768, 128, bias=False)
def forward(self, x: torch.Tensor, mels: torch.Tensor) -> torch.Tensor:
mels = self.prenet(mels)
x, _ = self.lstm1(torch.cat((x, mels), dim=-1))
res = x
x, _ = self.lstm2(x)
x = res + x
res = x
x, _ = self.lstm3(x)
x = res + x
return self.proj(x)
@torch.inference_mode()
def generate(self, xs: torch.Tensor) -> torch.Tensor:
m = torch.zeros(xs.size(0), 128, device=xs.device)
h1 = torch.zeros(1, xs.size(0), 768, device=xs.device)
c1 = torch.zeros(1, xs.size(0), 768, device=xs.device)
h2 = torch.zeros(1, xs.size(0), 768, device=xs.device)
c2 = torch.zeros(1, xs.size(0), 768, device=xs.device)
h3 = torch.zeros(1, xs.size(0), 768, device=xs.device)
c3 = torch.zeros(1, xs.size(0), 768, device=xs.device)
mel = []
for x in torch.unbind(xs, dim=1):
m = self.prenet(m)
x = torch.cat((x, m), dim=1).unsqueeze(1)
x1, (h1, c1) = self.lstm1(x, (h1, c1))
x2, (h2, c2) = self.lstm2(x1, (h2, c2))
x = x1 + x2
x3, (h3, c3) = self.lstm3(x, (h3, c3))
x = x + x3
m = self.proj(x).squeeze(1)
mel.append(m)
return torch.stack(mel, dim=1)
class PreNet(nn.Module):
def __init__(
self,
input_size: int,
hidden_size: int,
output_size: int,
dropout: float = 0.5,
):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_size, hidden_size),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(hidden_size, output_size),
nn.ReLU(),
nn.Dropout(dropout),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.net(x)
def _acoustic(
name: str,
discrete: bool,
upsample: bool,
pretrained: bool = True,
progress: bool = True,
) -> AcousticModel:
acoustic = AcousticModel(discrete, upsample)
if pretrained:
checkpoint = torch.hub.load_state_dict_from_url(URLS[name], progress=progress)
consume_prefix_in_state_dict_if_present(checkpoint["acoustic-model"], "module.")
acoustic.load_state_dict(checkpoint["acoustic-model"])
acoustic.eval()
return acoustic
def hubert_discrete(
pretrained: bool = True,
progress: bool = True,
) -> AcousticModel:
r"""HuBERT-Discrete acoustic model from `"A Comparison of Discrete and Soft Speech Units for Improved Voice Conversion"`.
Args:
pretrained (bool): load pretrained weights into the model
progress (bool): show progress bar when downloading model
"""
return _acoustic(
"hubert-discrete",
discrete=True,
upsample=True,
pretrained=pretrained,
progress=progress,
)
def hubert_soft(
pretrained: bool = True,
progress: bool = True,
) -> AcousticModel:
r"""HuBERT-Soft acoustic model from `"A Comparison of Discrete and Soft Speech Units for Improved Voice Conversion"`.
Args:
pretrained (bool): load pretrained weights into the model
progress (bool): show progress bar when downloading model
"""
return _acoustic(
"hubert-soft",
discrete=False,
upsample=True,
pretrained=pretrained,
progress=progress,
)
@@ -0,0 +1,99 @@
import torch
import torch.nn.functional as F
import matplotlib
import torchaudio.transforms as transforms
matplotlib.use("Agg")
import matplotlib.pylab as plt
class Metric:
def __init__(self):
self.steps = 0
self.value = 0
def update(self, value):
self.steps += 1
self.value += (value - self.value) / self.steps
return self.value
def reset(self):
self.steps = 0
self.value = 0
class LogMelSpectrogram(torch.nn.Module):
def __init__(self):
super().__init__()
self.melspctrogram = transforms.MelSpectrogram(
sample_rate=16000,
n_fft=1024,
win_length=1024,
hop_length=160,
center=False,
power=1.0,
norm="slaney",
onesided=True,
n_mels=128,
mel_scale="slaney",
)
def forward(self, wav):
padding = (1024 - 160) // 2
wav = F.pad(wav, (padding, padding), "reflect")
mel = self.melspctrogram(wav)
logmel = torch.log(torch.clamp(mel, min=1e-5))
return logmel
def save_checkpoint(
checkpoint_dir,
acoustic,
optimizer,
step,
loss,
best,
logger,
):
state = {
"acoustic-model": acoustic.state_dict(),
"optimizer": optimizer.state_dict(),
"step": step,
"loss": loss,
}
checkpoint_dir.mkdir(exist_ok=True, parents=True)
checkpoint_path = checkpoint_dir / f"model-{step}.pt"
torch.save(state, checkpoint_path)
if best:
best_path = checkpoint_dir / "model-best.pt"
torch.save(state, best_path)
logger.info(f"Saved checkpoint: {checkpoint_path.stem}")
def load_checkpoint(
load_path,
acoustic,
optimizer,
rank,
logger,
):
logger.info(f"Loading checkpoint from {load_path}")
checkpoint = torch.load(load_path, map_location={"cuda:0": f"cuda:{rank}"})
acoustic.load_state_dict(checkpoint["acoustic-model"])
if "optimizer" in checkpoint:
optimizer.load_state_dict(checkpoint["optimizer"])
step = checkpoint.get("step", 0)
loss = checkpoint.get("loss", float("inf"))
return step, loss
def plot_spectrogram(spectrogram):
fig, ax = plt.subplots(figsize=(10, 2))
im = ax.imshow(spectrogram, aspect="auto", origin="lower", interpolation="none")
plt.colorbar(im, ax=ax)
fig.canvas.draw()
plt.close()
return fig
@@ -0,0 +1,51 @@
import argparse
from pathlib import Path
import numpy as np
from tqdm import tqdm
import torch
def generate(args):
print("Loading acoustic model checkpoint")
acoustic = torch.hub.load("bshall/acoustic-model:main", f"hubert_{args.model}").cuda()
print(f"Generating from {args.in_dir} -> {args.out_dir}")
for path in tqdm(list(args.in_dir.rglob("*.npy"))):
units = np.load(path)
units_dtype = torch.long if args.model == "discrete" else torch.float
units = torch.tensor(units, dtype=units_dtype).cuda()
with torch.inference_mode():
mel_ = acoustic.generate(units)
mel_ = mel_.transpose(1, 2)
out_path = args.out_dir / path.relative_to(args.in_dir)
out_path.parent.mkdir(exist_ok=True, parents=True)
np.save(out_path.with_suffix(".npy"), mel_.squeeze().cpu().numpy())
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate spectrograms from input speech units (discrete or soft)."
)
parser.add_argument(
"model",
help="available models (HuBERT-Soft or HuBERT-Discrete)",
choices=["soft", "discrete"],
)
parser.add_argument(
"in_dir",
metavar="in-dir",
help="path to the dataset directory.",
type=Path,
)
parser.add_argument(
"out_dir",
metavar="out-dir",
help="path to the output directory.",
type=Path,
)
args = parser.parse_args()
generate(args)
@@ -0,0 +1,3 @@
dependencies = ["torch", "torchaudio"]
from acoustic import hubert_discrete, hubert_soft
@@ -0,0 +1,64 @@
import argparse
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor
from multiprocessing import cpu_count
import numpy as np
from tqdm import tqdm
import torchaudio
from torchaudio.functional import resample
from acoustic.utils import LogMelSpectrogram
melspectrogram = LogMelSpectrogram()
def process_wav(in_path, out_path):
wav, sr = torchaudio.load(in_path)
wav = resample(wav, sr, 16000)
logmel = melspectrogram(wav.unsqueeze(0))
np.save(out_path.with_suffix(".npy"), logmel.squeeze().numpy())
return out_path, logmel.shape[-1]
def preprocess_dataset(args):
args.out_dir.mkdir(parents=True, exist_ok=True)
futures = []
executor = ProcessPoolExecutor(max_workers=cpu_count())
print(f"Extracting features for {args.in_dir}")
for in_path in args.in_dir.rglob("*.wav"):
relative_path = in_path.relative_to(args.in_dir)
out_path = args.out_dir / relative_path.with_suffix("")
out_path.parent.mkdir(parents=True, exist_ok=True)
futures.append(executor.submit(process_wav, in_path, out_path))
results = [future.result() for future in tqdm(futures)]
lengths = {path.stem: length for path, length in results}
frames = sum(lengths.values())
frame_shift_ms = 160 / 16000
hours = frames * frame_shift_ms / 3600
print(f"Wrote {len(lengths)} utterances, {frames} frames ({hours:.2f} hours)")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Extract mel-spectrograms for an audio dataset.")
parser.add_argument(
"in_dir",
metavar="in-dir",
help="path to the dataset directory.",
type=Path,
)
parser.add_argument(
"out_dir",
metavar="out-dir",
help="path to the output directory.",
type=Path,
)
args = parser.parse_args()
preprocess_dataset(args)
@@ -0,0 +1,311 @@
import argparse
import logging
from pathlib import Path
import torch
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
import torch.distributed as dist
from torch.utils.data.distributed import DistributedSampler
import torch.multiprocessing as mp
from torch.nn.parallel import DistributedDataParallel as DDP
from acoustic import AcousticModel
from acoustic.dataset import MelDataset
from acoustic.utils import Metric, save_checkpoint, load_checkpoint, plot_spectrogram
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
########################################################################################
# Define hyperparameters for training:
########################################################################################
BATCH_SIZE = 32
LEARNING_RATE = 4e-4
BETAS = (0.8, 0.99)
WEIGHT_DECAY = 1e-5
STEPS = 80000
LOG_INTERVAL = 5
VALIDATION_INTERVAL = 1000
CHECKPOINT_INTERVAL = 1000
BACKEND = "nccl"
INIT_METHOD = "tcp://localhost:54321"
def train(rank, world_size, args):
dist.init_process_group(
BACKEND,
rank=rank,
world_size=world_size,
init_method=INIT_METHOD,
)
####################################################################################
# Setup logging utilities:
####################################################################################
log_dir = args.checkpoint_dir / "logs"
log_dir.mkdir(exist_ok=True, parents=True)
if rank == 0:
logger.setLevel(logging.INFO)
handler = logging.FileHandler(log_dir / f"{args.checkpoint_dir.stem}.log")
handler.setLevel(logging.INFO)
formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s", datefmt="%m/%d/%Y %I:%M:%S"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
else:
logger.setLevel(logging.ERROR)
writer = SummaryWriter(log_dir) if rank == 0 else None
####################################################################################
# Initialize models and optimizer
####################################################################################
acoustic = AcousticModel().to(rank)
acoustic = DDP(acoustic, device_ids=[rank])
optimizer = optim.AdamW(
acoustic.parameters(),
lr=LEARNING_RATE,
betas=BETAS,
weight_decay=WEIGHT_DECAY,
)
####################################################################################
# Initialize datasets and dataloaders
####################################################################################
train_dataset = MelDataset(
root=args.dataset_dir,
train=True,
discrete=args.discrete,
)
train_sampler = DistributedSampler(train_dataset, drop_last=True)
train_loader = DataLoader(
train_dataset,
batch_size=BATCH_SIZE,
sampler=train_sampler,
collate_fn=train_dataset.pad_collate,
num_workers=8,
pin_memory=True,
shuffle=False,
drop_last=True,
)
validation_dataset = MelDataset(
root=args.dataset_dir,
train=False,
)
validation_loader = DataLoader(
validation_dataset,
batch_size=1,
shuffle=False,
num_workers=8,
pin_memory=True,
)
####################################################################################
# Load checkpoint if args.resume is set
####################################################################################
if args.resume is not None:
global_step, best_loss = load_checkpoint(
load_path=args.resume,
acoustic=acoustic,
optimizer=optimizer,
rank=rank,
logger=logger,
)
else:
global_step, best_loss = 0, float("inf")
# =================================================================================#
# Start training loop
# =================================================================================#
n_epochs = STEPS // len(train_loader) + 1
start_epoch = global_step // len(train_loader) + 1
logger.info("**" * 40)
logger.info(f"PyTorch version: {torch.__version__}")
logger.info(f"CUDA version: {torch.version.cuda}")
logger.info(f"CUDNN version: {torch.backends.cudnn.version()}")
logger.info(f"CUDNN enabled: {torch.backends.cudnn.enabled}")
logger.info(f"CUDNN deterministic: {torch.backends.cudnn.deterministic}")
logger.info(f"CUDNN benchmark: {torch.backends.cudnn.benchmark}")
logger.info(f"# of GPUS: {torch.cuda.device_count()}")
logger.info(f"batch size: {BATCH_SIZE}")
logger.info(f"iterations per epoch: {len(train_loader)}")
logger.info(f"# of epochs: {n_epochs}")
logger.info(f"started at epoch: {start_epoch}")
logger.info("**" * 40 + "\n")
average_loss = Metric()
epoch_loss = Metric()
validation_loss = Metric()
for epoch in range(start_epoch, n_epochs + 1):
train_sampler.set_epoch(epoch)
acoustic.train()
epoch_loss.reset()
for mels, mels_lengths, units, units_lengths in train_loader:
mels, mels_lengths = mels.to(rank), mels_lengths.to(rank)
units, units_lengths = units.to(rank), units_lengths.to(rank)
############################################################################
# Compute training loss
############################################################################
optimizer.zero_grad()
mels_ = acoustic(units, mels[:, :-1, :])
loss = F.l1_loss(mels_, mels[:, 1:, :], reduction="none")
loss = torch.sum(loss, dim=(1, 2)) / (mels_.size(-1) * mels_lengths)
loss = torch.mean(loss)
loss.backward()
optimizer.step()
global_step += 1
############################################################################
# Update and log training metrics
############################################################################
average_loss.update(loss.item())
epoch_loss.update(loss.item())
if rank == 0 and global_step % LOG_INTERVAL == 0:
writer.add_scalar(
"train/loss",
average_loss.value,
global_step,
)
average_loss.reset()
# --------------------------------------------------------------------------#
# Start validation loop
# --------------------------------------------------------------------------#
if global_step % VALIDATION_INTERVAL == 0:
acoustic.eval()
validation_loss.reset()
for i, (mels, units) in enumerate(validation_loader, 1):
mels, units = mels.to(rank), units.to(rank)
with torch.no_grad():
mels_ = acoustic(units, mels[:, :-1, :])
loss = F.l1_loss(mels_, mels[:, 1:, :])
####################################################################
# Update validation metrics and log generated mels
####################################################################
validation_loss.update(loss.item())
if rank == 0:
writer.add_figure(
f"generated/mel_{i}",
plot_spectrogram(
mels_.squeeze().transpose(0, 1).cpu().numpy()
),
global_step,
)
acoustic.train()
############################################################################
# Log validation metrics
############################
if rank == 0:
writer.add_scalar(
"validation/loss",
validation_loss.value,
global_step,
)
logger.info(
f"valid -- epoch: {epoch}, loss: {validation_loss.value:.4f}"
)
new_best = best_loss > validation_loss.value
if new_best or global_step % CHECKPOINT_INTERVAL:
if new_best:
logger.info("-------- new best model found!")
best_loss = validation_loss.value
if rank == 0:
save_checkpoint(
checkpoint_dir=args.checkpoint_dir,
acoustic=acoustic,
optimizer=optimizer,
step=global_step,
loss=validation_loss.value,
best=new_best,
logger=logger,
)
# -----------------------------------------------------------------------------#
# End validation loop
# -----------------------------------------------------------------------------#
####################################################################################
# Log training metrics
####################################################################################
logger.info(f"train -- epoch: {epoch}, loss: {epoch_loss.value:.4f}")
# =================================================================================#
# End training loop
# ==================================================================================#
dist.destroy_process_group()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Train the acoustic model.")
parser.add_argument(
"dataset_dir",
metavar="dataset-dir",
help="path to the data directory.",
type=Path,
)
parser.add_argument(
"checkpoint_dir",
metavar="checkpoint-dir",
help="path to the checkpoint directory.",
type=Path,
)
parser.add_argument(
"--resume",
help="path to the checkpoint to resume from.",
type=Path,
)
parser.add_argument(
"--discrete",
action="store_true",
help="use discrete units.",
)
args = parser.parse_args()
world_size = torch.cuda.device_count()
mp.spawn(
train,
args=(world_size, args),
nprocs=world_size,
join=True,
)
+132
View File
@@ -0,0 +1,132 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# VSCode project settings
.vscode
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Benjamin van Niekerk
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+139
View File
@@ -0,0 +1,139 @@
<p align="center">
<a target="_blank" href="https://colab.research.google.com/github/bshall/soft-vc/blob/main/soft-vc-demo.ipynb">
<img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/>
</a>
</p>
# HiFi-GAN
Training and inference scripts for the vocoder models in [A Comparison of Discrete and Soft Speech Units for Improved Voice Conversion](https://ieeexplore.ieee.org/abstract/document/9746484). For more details see [soft-vc](https://github.com/bshall/soft-vc). Audio samples can be found [here](https://bshall.github.io/soft-vc/). Colab demo can be found [here](https://colab.research.google.com/github/bshall/soft-vc/blob/main/soft-vc-demo.ipynb).
<div align="center">
<img width="100%" alt="Soft-VC"
src="https://raw.githubusercontent.com/bshall/hifigan/main/vocoder.png">
</div>
<div>
<sup>
<strong>Fig 1:</strong> Architecture of the voice conversion system. a) The <strong>discrete</strong> content encoder clusters audio features to produce a sequence of discrete speech units. b) The <strong>soft</strong> content encoder is trained to predict the discrete units. The acoustic model transforms the discrete/soft speech units into a target spectrogram. The vocoder converts the spectrogram into an audio waveform.
</sup>
</div>
## Example Usage
### Programmatic Usage
```python
import torch
import numpy as np
# Load checkpoint
hifigan = torch.hub.load("bshall/hifigan:main", "hifigan_hubert_soft").cuda()
# Load mel-spectrogram
mel = torch.from_numpy(np.load("path/to/mel")).unsqueeze(0).cuda()
# Generate
wav, sr = hifigan.generate(mel)
```
### Script-Based Usage
```
usage: generate.py [-h] {soft,discrete,base} in-dir out-dir
Generate audio for a directory of mel-spectrogams using HiFi-GAN.
positional arguments:
{soft,discrete,base} available models (HuBERT-Soft, HuBERT-Discrete, or
Base).
in-dir path to input directory containing the mel-
spectrograms.
out-dir path to output directory.
optional arguments:
-h, --help show this help message and exit
```
## Training
### Step 1: Dataset Preparation
Download and extract the [LJSpeech](https://keithito.com/LJ-Speech-Dataset/) dataset. The training script expects the following tree structure for the dataset directory:
```
└───wavs
├───dev
│ ├───LJ001-0001.wav
│ ├───...
│ └───LJ050-0278.wav
└───train
├───LJ002-0332.wav
├───...
└───LJ047-0007.wav
```
The `train` and `dev` directories should contain the training and validation splits respectively. The splits used for the paper can be found [here](https://github.com/bshall/hifigan/releases/tag/v0.1).
### Step 2: Resample the Audio
Resample the audio to 16kHz using the `resample.py` script:
```
usage: resample.py [-h] [--sample-rate SAMPLE_RATE] in-dir out-dir
Resample an audio dataset.
positional arguments:
in-dir path to the dataset directory.
out-dir path to the output directory.
optional arguments:
-h, --help show this help message and exit
--sample-rate SAMPLE_RATE
target sample rate (default 16kHz)
```
for example:
```
python reample.py path/to/LJSpeech-1.1/ path/to/LJSpeech-Resampled/
```
### Step 3: Train HifiGAN
```
usage: train.py [-h] [--resume RESUME] [--finetune] dataset-dir checkpoint-dir
Train or finetune HiFi-GAN.
positional arguments:
dataset-dir path to the preprocessed data directory
checkpoint-dir path to the checkpoint directory
optional arguments:
-h, --help show this help message and exit
--resume RESUME path to the checkpoint to resume from
--finetune whether to finetune (note that a resume path must be given)
```
## Links
- [Soft-VC repo](https://github.com/bshall/soft-vc)
- [Soft-VC paper](https://ieeexplore.ieee.org/abstract/document/9746484)
- [HuBERT content encoders](https://github.com/bshall/hubert)
- [Acoustic models](https://github.com/bshall/acoustic-model)
## Citation
If you found this work helpful please consider citing our paper:
```
@inproceedings{
soft-vc-2022,
author={van Niekerk, Benjamin and Carbonneau, Marc-André and Zaïdi, Julian and Baas, Matthew and Seuté, Hugo and Kamper, Herman},
booktitle={ICASSP},
title={A Comparison of Discrete and Soft Speech Units for Improved Voice Conversion},
year={2022}
}
```
## Acknowledgements
This repo is based heavily on [https://github.com/jik876/hifi-gan](https://github.com/jik876/hifi-gan).
@@ -0,0 +1,50 @@
from pathlib import Path
import numpy as np
import argparse
import torch
import torchaudio
from tqdm import tqdm
def generate(args):
print("Loading checkpoint")
model_name = f"hifigan_hubert_{args.model}" if args.model != "base" else "hifigan"
hifigan = torch.hub.load("bshall/hifigan:main", model_name).cuda()
print(f"Generating audio from {args.in_dir}")
for path in tqdm(list(args.in_dir.rglob("*.npy"))):
mel = torch.from_numpy(np.load(path))
mel = mel.unsqueeze(0).cuda()
wav, sr = hifigan.generate(mel)
wav = wav.squeeze(0).cpu()
out_path = args.out_dir / path.relative_to(args.in_dir)
out_path.parent.mkdir(exist_ok=True, parents=True)
torchaudio.save(out_path.with_suffix(".wav"), wav, sr)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate audio for a directory of mel-spectrogams using HiFi-GAN."
)
parser.add_argument(
"model",
help="available models (HuBERT-Soft, HuBERT-Discrete, or Base).",
choices=["soft", "discrete", "base"],
)
parser.add_argument(
"in_dir",
metavar="in-dir",
help="path to input directory containing the mel-spectrograms.",
type=Path,
)
parser.add_argument(
"out_dir",
metavar="out-dir",
help="path to output directory.",
type=Path,
)
args = parser.parse_args()
generate(args)
@@ -0,0 +1 @@
from .generator import hifigan, hifigan_hubert_discrete, hifigan_hubert_soft
@@ -0,0 +1,126 @@
from pathlib import Path
import math
import random
import numpy as np
import torch
import torch.nn.functional as F
from torch.utils.data import Dataset
import torchaudio
import torchaudio.transforms as transforms
class LogMelSpectrogram(torch.nn.Module):
def __init__(self):
super().__init__()
self.melspctrogram = transforms.MelSpectrogram(
sample_rate=16000,
n_fft=1024,
win_length=1024,
hop_length=160,
center=False,
power=1.0,
norm="slaney",
onesided=True,
n_mels=128,
mel_scale="slaney",
)
def forward(self, wav):
wav = F.pad(wav, ((1024 - 160) // 2, (1024 - 160) // 2), "reflect")
mel = self.melspctrogram(wav)
logmel = torch.log(torch.clamp(mel, min=1e-5))
return logmel
class MelDataset(Dataset):
def __init__(
self,
root: Path,
segment_length: int,
sample_rate: int,
hop_length: int,
train: bool = True,
finetune: bool = False,
):
self.wavs_dir = root / "wavs"
self.mels_dir = root / "mels"
self.data_dir = self.wavs_dir if not finetune else self.mels_dir
self.segment_length = segment_length
self.sample_rate = sample_rate
self.hop_length = hop_length
self.train = train
self.finetune = finetune
suffix = ".wav" if not finetune else ".npy"
pattern = f"train/**/*{suffix}" if train else "dev/**/*{suffix}"
self.metadata = [
path.relative_to(self.data_dir).with_suffix("")
for path in self.data_dir.rglob(pattern)
]
self.logmel = LogMelSpectrogram()
def __len__(self):
return len(self.metadata)
def __getitem__(self, index):
path = self.metadata[index]
wav_path = self.wavs_dir / path
info = torchaudio.info(wav_path.with_suffix(".wav"))
if info.sample_rate != self.sample_rate:
raise ValueError(
f"Sample rate {info.sample_rate} doesn't match target of {self.sample_rate}"
)
if self.finetune:
mel_path = self.mels_dir / path
src_logmel = torch.from_numpy(np.load(mel_path.with_suffix(".npy")))
src_logmel = src_logmel.unsqueeze(0)
mel_frames_per_segment = math.ceil(self.segment_length / self.hop_length)
mel_diff = src_logmel.size(-1) - mel_frames_per_segment if self.train else 0
mel_offset = random.randint(0, max(mel_diff, 0))
frame_offset = self.hop_length * mel_offset
else:
frame_diff = info.num_frames - self.segment_length
frame_offset = random.randint(0, max(frame_diff, 0))
wav, _ = torchaudio.load(
filepath=wav_path.with_suffix(".wav"),
frame_offset=frame_offset if self.train else 0,
num_frames=self.segment_length if self.train else -1,
)
if wav.size(-1) < self.segment_length:
wav = F.pad(wav, (0, self.segment_length - wav.size(-1)))
if not self.finetune and self.train:
gain = random.random() * (0.99 - 0.4) + 0.4
flip = -1 if random.random() > 0.5 else 1
wav = flip * gain * wav / wav.abs().max()
tgt_logmel = self.logmel(wav.unsqueeze(0)).squeeze(0)
if self.finetune:
if self.train:
src_logmel = src_logmel[
:, :, mel_offset : mel_offset + mel_frames_per_segment
]
if src_logmel.size(-1) < mel_frames_per_segment:
src_logmel = F.pad(
src_logmel,
(0, mel_frames_per_segment - src_logmel.size(-1)),
"constant",
src_logmel.min(),
)
else:
src_logmel = tgt_logmel.clone()
return wav, src_logmel, tgt_logmel
@@ -0,0 +1,262 @@
# adopted from https://github.com/jik876/hifi-gan/blob/master/models.py
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Tuple, List
from hifigan.utils import get_padding
LRELU_SLOPE = 0.1
class PeriodDiscriminator(torch.nn.Module):
"""HiFiGAN Period Discriminator"""
def __init__(
self,
period: int,
kernel_size: int = 5,
stride: int = 3,
use_spectral_norm: bool = False,
) -> None:
super().__init__()
self.period = period
norm_f = nn.utils.spectral_norm if use_spectral_norm else nn.utils.weight_norm
self.convs = nn.ModuleList(
[
norm_f(
nn.Conv2d(
1,
32,
(kernel_size, 1),
(stride, 1),
padding=(get_padding(5, 1), 0),
)
),
norm_f(
nn.Conv2d(
32,
128,
(kernel_size, 1),
(stride, 1),
padding=(get_padding(5, 1), 0),
)
),
norm_f(
nn.Conv2d(
128,
512,
(kernel_size, 1),
(stride, 1),
padding=(get_padding(5, 1), 0),
)
),
norm_f(
nn.Conv2d(
512,
1024,
(kernel_size, 1),
(stride, 1),
padding=(get_padding(5, 1), 0),
)
),
norm_f(nn.Conv2d(1024, 1024, (kernel_size, 1), 1, padding=(2, 0))),
]
)
self.conv_post = norm_f(nn.Conv2d(1024, 1, (3, 1), 1, padding=(1, 0)))
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, List[torch.Tensor]]:
"""
Args:
x (Tensor): input waveform.
Returns:
[Tensor]: discriminator scores per sample in the batch.
[List[Tensor]]: list of features from each convolutional layer.
"""
feat = []
# 1d to 2d
b, c, t = x.shape
if t % self.period != 0: # pad first
n_pad = self.period - (t % self.period)
x = F.pad(x, (0, n_pad), "reflect")
t = t + n_pad
x = x.view(b, c, t // self.period, self.period)
for l in self.convs:
x = l(x)
x = F.leaky_relu(x, LRELU_SLOPE)
feat.append(x)
x = self.conv_post(x)
feat.append(x)
x = torch.flatten(x, 1, -1)
return x, feat
class MultiPeriodDiscriminator(torch.nn.Module):
"""HiFiGAN Multi-Period Discriminator (MPD)"""
def __init__(self):
super().__init__()
self.discriminators = nn.ModuleList(
[
PeriodDiscriminator(2),
PeriodDiscriminator(3),
PeriodDiscriminator(5),
PeriodDiscriminator(7),
PeriodDiscriminator(11),
]
)
def forward(
self, x: torch.Tensor
) -> Tuple[List[torch.Tensor], List[List[torch.Tensor]]]:
"""
Args:
x (Tensor): input waveform.
Returns:
[List[Tensor]]: list of scores from each discriminator.
[List[List[Tensor]]]: list of features from each discriminator's convolutional layers.
"""
scores = []
feats = []
for _, d in enumerate(self.discriminators):
score, feat = d(x)
scores.append(score)
feats.append(feat)
return scores, feats
class ScaleDiscriminator(torch.nn.Module):
"""HiFiGAN Scale Discriminator."""
def __init__(self, use_spectral_norm: bool = False) -> None:
super().__init__()
norm_f = nn.utils.spectral_norm if use_spectral_norm else nn.utils.weight_norm
self.convs = nn.ModuleList(
[
norm_f(nn.Conv1d(1, 128, 15, 1, padding=7)),
norm_f(nn.Conv1d(128, 128, 41, 2, groups=4, padding=20)),
norm_f(nn.Conv1d(128, 256, 41, 2, groups=16, padding=20)),
norm_f(nn.Conv1d(256, 512, 41, 4, groups=16, padding=20)),
norm_f(nn.Conv1d(512, 1024, 41, 4, groups=16, padding=20)),
norm_f(nn.Conv1d(1024, 1024, 41, 1, groups=16, padding=20)),
norm_f(nn.Conv1d(1024, 1024, 5, 1, padding=2)),
]
)
self.conv_post = norm_f(nn.Conv1d(1024, 1, 3, 1, padding=1))
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, List[torch.Tensor]]:
"""
Args:
x (Tensor): input waveform.
Returns:
Tensor: discriminator scores.
List[Tensor]: list of features from the convolutional layers.
"""
feat = []
for l in self.convs:
x = l(x)
x = F.leaky_relu(x, LRELU_SLOPE)
feat.append(x)
x = self.conv_post(x)
feat.append(x)
x = torch.flatten(x, 1, -1)
return x, feat
class MultiScaleDiscriminator(torch.nn.Module):
"""HiFiGAN Multi-Scale Discriminator."""
def __init__(self):
super().__init__()
self.discriminators = nn.ModuleList(
[
ScaleDiscriminator(use_spectral_norm=True),
ScaleDiscriminator(),
ScaleDiscriminator(),
]
)
self.meanpools = nn.ModuleList(
[nn.AvgPool1d(4, 2, padding=2), nn.AvgPool1d(4, 2, padding=2)]
)
def forward(
self, x: torch.Tensor
) -> Tuple[List[torch.Tensor], List[List[torch.Tensor]]]:
"""
Args:
x (Tensor): input waveform.
Returns:
List[Tensor]: discriminator scores.
List[List[Tensor]]: list of features from each discriminator's convolutional layers.
"""
scores = []
feats = []
for i, d in enumerate(self.discriminators):
if i != 0:
x = self.meanpools[i - 1](x)
score, feat = d(x)
scores.append(score)
feats.append(feat)
return scores, feats
class HifiganDiscriminator(nn.Module):
"""HiFiGAN discriminator"""
def __init__(self):
super().__init__()
self.mpd = MultiPeriodDiscriminator()
self.msd = MultiScaleDiscriminator()
def forward(
self, x: torch.Tensor
) -> Tuple[List[torch.Tensor], List[List[torch.Tensor]]]:
"""
Args:
x (Tensor): input waveform.
Returns:
List[Tensor]: discriminator scores.
List[List[Tensor]]: list of features from from each discriminator's convolutional layers.
"""
scores, feats = self.mpd(x)
scores_, feats_ = self.msd(x)
return scores + scores_, feats + feats_
def feature_loss(
features_real: List[List[torch.Tensor]], features_generate: List[List[torch.Tensor]]
) -> float:
loss = 0
for r, g in zip(features_real, features_generate):
for rl, gl in zip(r, g):
loss += torch.mean(torch.abs(rl - gl))
return loss * 2
def discriminator_loss(real, generated):
loss = 0
real_losses = []
generated_losses = []
for r, g in zip(real, generated):
r_loss = torch.mean((1 - r) ** 2)
g_loss = torch.mean(g ** 2)
loss += r_loss + g_loss
real_losses.append(r_loss.item())
generated_losses.append(g_loss.item())
return loss, real_losses, generated_losses
def generator_loss(discriminator_outputs):
loss = 0
generator_losses = []
for x in discriminator_outputs:
l = torch.mean((1 - x) ** 2)
generator_losses.append(l)
loss += l
return loss, generator_losses
@@ -0,0 +1,282 @@
# adapted from https://github.com/jik876/hifi-gan/blob/master/models.py
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.utils import remove_weight_norm, weight_norm
from torch.nn.modules.utils import consume_prefix_in_state_dict_if_present
from typing import Tuple
from hifigan.utils import get_padding
URLS = {
"hifigan": "https://github.com/bshall/hifigan/releases/download/v0.1/hifigan-67926ec6.pt",
"hifigan-hubert-soft": "https://github.com/bshall/hifigan/releases/download/v0.1/hifigan-hubert-discrete-bbad3043.pt",
"hifigan-hubert-discrete": "https://github.com/bshall/hifigan/releases/download/v0.1/hifigan-hubert-soft-65f03469.pt",
}
LRELU_SLOPE = 0.1
class HifiganGenerator(torch.nn.Module):
def __init__(
self,
in_channels: int = 128,
resblock_dilation_sizes: Tuple[Tuple[int, ...], ...] = (
(1, 3, 5),
(1, 3, 5),
(1, 3, 5),
),
resblock_kernel_sizes: Tuple[int, ...] = (3, 7, 11),
upsample_kernel_sizes: Tuple[int, ...] = (20, 8, 4, 4),
upsample_initial_channel: int = 512,
upsample_factors: int = (10, 4, 2, 2),
inference_padding: int = 5,
sample_rate: int = 16000,
) -> None:
r"""HiFiGAN Generator
Args:
in_channels (int): number of input channels.
resblock_dilation_sizes (Tuple[Tuple[int, ...], ...]): list of dilation values in each layer of a `ResBlock`.
resblock_kernel_sizes (Tuple[int, ...]): list of kernel sizes for each `ResBlock`.
upsample_kernel_sizes (Tuple[int, ...]): list of kernel sizes for each transposed convolution.
upsample_initial_channel (int): number of channels for the first upsampling layer. This is divided by 2
for each consecutive upsampling layer.
upsample_factors (Tuple[int, ...]): upsampling factors (stride) for each upsampling layer.
inference_padding (int): constant padding applied to the input at inference time.
sample_rate (int): sample rate of the generated audio.
"""
super().__init__()
self.inference_padding = inference_padding
self.num_kernels = len(resblock_kernel_sizes)
self.num_upsamples = len(upsample_factors)
self.sample_rate = sample_rate
# initial upsampling layers
self.conv_pre = weight_norm(
nn.Conv1d(in_channels, upsample_initial_channel, 7, 1, padding=3)
)
# upsampling layers
self.ups = nn.ModuleList()
for i, (u, k) in enumerate(zip(upsample_factors, upsample_kernel_sizes)):
self.ups.append(
weight_norm(
nn.ConvTranspose1d(
upsample_initial_channel // (2 ** i),
upsample_initial_channel // (2 ** (i + 1)),
k,
u,
padding=(k - u) // 2,
)
)
)
# MRF blocks
self.resblocks = nn.ModuleList()
for i in range(len(self.ups)):
ch = upsample_initial_channel // (2 ** (i + 1))
for _, (k, d) in enumerate(
zip(resblock_kernel_sizes, resblock_dilation_sizes)
):
self.resblocks.append(ResBlock1(ch, k, d))
# post convolution layer
self.conv_post = weight_norm(nn.Conv1d(ch, 1, 7, 1, padding=3))
def forward(self, x: torch.Tensor) -> torch.Tensor:
o = self.conv_pre(x)
for i in range(self.num_upsamples):
o = F.leaky_relu(o, LRELU_SLOPE)
o = self.ups[i](o)
z_sum = None
for j in range(self.num_kernels):
if z_sum is None:
z_sum = self.resblocks[i * self.num_kernels + j](o)
else:
z_sum += self.resblocks[i * self.num_kernels + j](o)
o = z_sum / self.num_kernels
o = F.leaky_relu(o)
o = self.conv_post(o)
o = torch.tanh(o)
return o
@torch.no_grad()
def generate(self, x: torch.Tensor) -> torch.Tensor:
x = F.pad(x, (self.inference_padding, self.inference_padding), "replicate")
return self(x), self.sample_rate
def remove_weight_norm(self):
print("Removing weight norm...")
for l in self.ups:
remove_weight_norm(l)
for l in self.resblocks:
l.remove_weight_norm()
remove_weight_norm(self.conv_pre)
remove_weight_norm(self.conv_post)
class ResBlock1(torch.nn.Module):
def __init__(
self, channels: int, kernel_size: int = 3, dilation: Tuple[int, ...] = (1, 3, 5)
) -> None:
super().__init__()
self.convs1 = nn.ModuleList(
[
weight_norm(
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=dilation[0],
padding=get_padding(kernel_size, dilation[0]),
)
),
weight_norm(
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=dilation[1],
padding=get_padding(kernel_size, dilation[1]),
)
),
weight_norm(
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=dilation[2],
padding=get_padding(kernel_size, dilation[2]),
)
),
]
)
self.convs2 = nn.ModuleList(
[
weight_norm(
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=1,
padding=get_padding(kernel_size, 1),
)
),
weight_norm(
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=1,
padding=get_padding(kernel_size, 1),
)
),
weight_norm(
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=1,
padding=get_padding(kernel_size, 1),
)
),
]
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
for c1, c2 in zip(self.convs1, self.convs2):
xt = F.leaky_relu(x, LRELU_SLOPE)
xt = c1(xt)
xt = F.leaky_relu(xt, LRELU_SLOPE)
xt = c2(xt)
x = xt + x
return x
def remove_weight_norm(self):
for l in self.convs1:
remove_weight_norm(l)
for l in self.convs2:
remove_weight_norm(l)
class ResBlock2(torch.nn.Module):
def __init__(
self, channels: int, kernel_size: int = 3, dilation: Tuple[int, ...] = (1, 3)
) -> None:
super().__init__()
self.convs = nn.ModuleList(
[
weight_norm(
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=dilation[0],
padding=get_padding(kernel_size, dilation[0]),
)
),
weight_norm(
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=dilation[1],
padding=get_padding(kernel_size, dilation[1]),
)
),
]
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
for c in self.convs:
xt = F.leaky_relu(x, LRELU_SLOPE)
xt = c(xt)
x = xt + x
return x
def remove_weight_norm(self):
for l in self.convs:
remove_weight_norm(l)
def _hifigan(
name: str,
pretrained: bool = True,
progress: bool = True,
map_location=None,
) -> HifiganGenerator:
hifigan = HifiganGenerator()
if pretrained:
checkpoint = torch.hub.load_state_dict_from_url(
URLS[name], map_location=map_location, progress=progress
)
consume_prefix_in_state_dict_if_present(checkpoint, "module.")
hifigan.load_state_dict(checkpoint)
hifigan.eval()
hifigan.remove_weight_norm()
return hifigan
def hifigan(
pretrained: bool = True, progress: bool = True, map_location=None
) -> HifiganGenerator:
return _hifigan("hifigan", pretrained, progress, map_location)
def hifigan_hubert_soft(
pretrained: bool = True, progress: bool = True, map_location=None
) -> HifiganGenerator:
return _hifigan("hifigan-hubert-soft", pretrained, progress, map_location=None)
def hifigan_hubert_discrete(
pretrained: bool = True, progress: bool = True, map_location=None
) -> HifiganGenerator:
return _hifigan("hifigan-hubert-discrete", pretrained, progress, map_location=None)
@@ -0,0 +1,84 @@
import torch
import matplotlib
matplotlib.use("Agg")
import matplotlib.pylab as plt
def get_padding(k, d):
return int((k * d - d) / 2)
def plot_spectrogram(spectrogram):
fig, ax = plt.subplots(figsize=(10, 2))
im = ax.imshow(spectrogram, aspect="auto", origin="lower", interpolation="none")
plt.colorbar(im, ax=ax)
fig.canvas.draw()
plt.close()
return fig
def save_checkpoint(
checkpoint_dir,
generator,
discriminator,
optimizer_generator,
optimizer_discriminator,
scheduler_generator,
scheduler_discriminator,
step,
loss,
best,
logger,
):
state = {
"generator": {
"model": generator.state_dict(),
"optimizer": optimizer_generator.state_dict(),
"scheduler": scheduler_generator.state_dict(),
},
"discriminator": {
"model": discriminator.state_dict(),
"optimizer": optimizer_discriminator.state_dict(),
"scheduler": scheduler_discriminator.state_dict(),
},
"step": step,
"loss": loss,
}
checkpoint_dir.mkdir(exist_ok=True, parents=True)
checkpoint_path = checkpoint_dir / f"model-{step}.pt"
torch.save(state, checkpoint_path)
if best:
best_path = checkpoint_dir / "model-best.pt"
torch.save(state, best_path)
logger.info(f"Saved checkpoint: {checkpoint_path.stem}")
def load_checkpoint(
load_path,
generator,
discriminator,
optimizer_generator,
optimizer_discriminator,
scheduler_generator,
scheduler_discriminator,
rank,
logger,
finetune=False,
):
logger.info(f"Loading checkpoint from {load_path}")
checkpoint = torch.load(load_path, map_location={"cuda:0": f"cuda:{rank}"})
generator.load_state_dict(checkpoint["generator"]["model"])
discriminator.load_state_dict(checkpoint["discriminator"]["model"])
if not finetune:
optimizer_generator.load_state_dict(checkpoint["generator"]["optimizer"])
scheduler_generator.load_state_dict(checkpoint["generator"]["scheduler"])
optimizer_discriminator.load_state_dict(
checkpoint["discriminator"]["optimizer"]
)
scheduler_discriminator.load_state_dict(
checkpoint["discriminator"]["scheduler"]
)
return checkpoint["step"], checkpoint["loss"]
@@ -0,0 +1,3 @@
dependencies = ["torch", "torchaudio"]
from hifigan import hifigan, hifigan_hubert_discrete, hifigan_hubert_soft
@@ -0,0 +1,4 @@
tensorboard==2.7.0
torch==1.9.1
torchaudio==0.9.1
tqdm==4.62.3
@@ -0,0 +1,56 @@
import argparse
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor
from multiprocessing import cpu_count
import torchaudio
from torchaudio.functional import resample
from tqdm import tqdm
def process_wav(in_path, out_path, sample_rate):
wav, sr = torchaudio.load(in_path)
wav = resample(wav, sr, sample_rate)
torchaudio.save(out_path, wav, sample_rate)
return out_path, wav.size(-1) / sample_rate
def preprocess_dataset(args):
args.out_dir.mkdir(parents=True, exist_ok=True)
futures = []
executor = ProcessPoolExecutor(max_workers=cpu_count())
print(f"Resampling audio in {args.in_dir}")
for in_path in args.in_dir.rglob("*.wav"):
relative_path = in_path.relative_to(args.in_dir)
out_path = args.out_dir / relative_path
out_path.parent.mkdir(parents=True, exist_ok=True)
futures.append(
executor.submit(process_wav, in_path, out_path, args.sample_rate)
)
results = [future.result() for future in tqdm(futures)]
lengths = {path.stem: length for path, length in results}
seconds = sum(lengths.values())
hours = seconds / 3600
print(f"Wrote {len(lengths)} utterances ({hours:.2f} hours)")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Resample an audio dataset.")
parser.add_argument(
"in_dir", metavar="in-dir", help="path to the dataset directory.", type=Path
)
parser.add_argument(
"out_dir", metavar="out-dir", help="path to the output directory.", type=Path
)
parser.add_argument(
"--sample-rate",
help="target sample rate (default 16kHz)",
type=int,
default=16000,
)
args = parser.parse_args()
preprocess_dataset(args)
+347
View File
@@ -0,0 +1,347 @@
import argparse
import logging
from pathlib import Path
import torch
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
import torch.distributed as dist
from torch.utils.data.distributed import DistributedSampler
import torch.multiprocessing as mp
from torch.nn.parallel import DistributedDataParallel as DDP
from hifigan.generator import HifiganGenerator
from hifigan.discriminator import (
HifiganDiscriminator,
feature_loss,
discriminator_loss,
generator_loss,
)
from hifigan.dataset import MelDataset, LogMelSpectrogram
from hifigan.utils import load_checkpoint, save_checkpoint, plot_spectrogram
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
BATCH_SIZE = 8
SEGMENT_LENGTH = 8320
HOP_LENGTH = 160
SAMPLE_RATE = 16000
BASE_LEARNING_RATE = 2e-4
FINETUNE_LEARNING_RATE = 1e-4
BETAS = (0.8, 0.99)
LEARNING_RATE_DECAY = 0.999
WEIGHT_DECAY = 1e-5
EPOCHS = 3100
LOG_INTERVAL = 5
VALIDATION_INTERVAL = 1000
NUM_GENERATED_EXAMPLES = 10
CHECKPOINT_INTERVAL = 5000
def train_model(rank, world_size, args):
dist.init_process_group(
"nccl",
rank=rank,
world_size=world_size,
init_method="tcp://localhost:54321",
)
log_dir = args.checkpoint_dir / "logs"
log_dir.mkdir(exist_ok=True, parents=True)
if rank == 0:
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler(log_dir / f"{args.checkpoint_dir.stem}.log")
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s", datefmt="%m/%d/%Y %I:%M:%S"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
else:
logger.setLevel(logging.ERROR)
writer = SummaryWriter(log_dir) if rank == 0 else None
generator = HifiganGenerator().to(rank)
discriminator = HifiganDiscriminator().to(rank)
generator = DDP(generator, device_ids=[rank])
discriminator = DDP(discriminator, device_ids=[rank])
optimizer_generator = optim.AdamW(
generator.parameters(),
lr=BASE_LEARNING_RATE if not args.finetune else FINETUNE_LEARNING_RATE,
betas=BETAS,
weight_decay=WEIGHT_DECAY,
)
optimizer_discriminator = optim.AdamW(
discriminator.parameters(),
lr=BASE_LEARNING_RATE if not args.finetune else FINETUNE_LEARNING_RATE,
betas=BETAS,
weight_decay=WEIGHT_DECAY,
)
scheduler_generator = optim.lr_scheduler.ExponentialLR(
optimizer_generator, gamma=LEARNING_RATE_DECAY
)
scheduler_discriminator = optim.lr_scheduler.ExponentialLR(
optimizer_discriminator, gamma=LEARNING_RATE_DECAY
)
train_dataset = MelDataset(
root=args.dataset_dir,
segment_length=SEGMENT_LENGTH,
sample_rate=SAMPLE_RATE,
hop_length=HOP_LENGTH,
train=True,
finetune=args.finetune,
)
train_sampler = DistributedSampler(train_dataset, drop_last=True)
train_loader = DataLoader(
train_dataset,
batch_size=BATCH_SIZE,
sampler=train_sampler,
num_workers=8,
pin_memory=True,
shuffle=False,
drop_last=True,
)
validation_dataset = MelDataset(
root=args.dataset_dir,
segment_length=SEGMENT_LENGTH,
sample_rate=SAMPLE_RATE,
hop_length=HOP_LENGTH,
train=False,
finetune=args.finetune,
)
validation_loader = DataLoader(
validation_dataset,
batch_size=1,
shuffle=False,
num_workers=8,
pin_memory=True,
)
melspectrogram = LogMelSpectrogram().to(rank)
if args.resume is not None:
global_step, best_loss = load_checkpoint(
load_path=args.resume,
generator=generator,
discriminator=discriminator,
optimizer_generator=optimizer_generator,
optimizer_discriminator=optimizer_discriminator,
scheduler_generator=scheduler_generator,
scheduler_discriminator=scheduler_discriminator,
rank=rank,
logger=logger,
finetune=args.finetune,
)
else:
global_step, best_loss = 0, float("inf")
if args.finetune:
global_step, best_loss = 0, float("inf")
n_epochs = EPOCHS
start_epoch = global_step // len(train_loader) + 1
logger.info("**" * 40)
logger.info(f"batch size: {BATCH_SIZE}")
logger.info(f"iterations per epoch: {len(train_loader)}")
logger.info(f"total of epochs: {n_epochs}")
logger.info(f"started at epoch: {start_epoch}")
logger.info("**" * 40 + "\n")
for epoch in range(start_epoch, n_epochs + 1):
train_sampler.set_epoch(epoch)
generator.train()
discriminator.train()
average_loss_mel = average_loss_discriminator = average_loss_generator = 0
for i, (wavs, mels, tgts) in enumerate(train_loader, 1):
wavs, mels, tgts = wavs.to(rank), mels.to(rank), tgts.to(rank)
# Discriminator
optimizer_discriminator.zero_grad()
wavs_ = generator(mels.squeeze(1))
mels_ = melspectrogram(wavs_)
scores, _ = discriminator(wavs)
scores_, _ = discriminator(wavs_.detach())
loss_discriminator, _, _ = discriminator_loss(scores, scores_)
loss_discriminator.backward()
optimizer_discriminator.step()
# Generator
optimizer_generator.zero_grad()
scores, features = discriminator(wavs)
scores_, features_ = discriminator(wavs_)
loss_mel = F.l1_loss(mels_, tgts)
loss_features = feature_loss(features, features_)
loss_generator_adversarial, _ = generator_loss(scores_)
loss_generator = 45 * loss_mel + loss_features + loss_generator_adversarial
loss_generator.backward()
optimizer_generator.step()
global_step += 1
average_loss_mel += (loss_mel.item() - average_loss_mel) / i
average_loss_discriminator += (
loss_discriminator.item() - average_loss_discriminator
) / i
average_loss_generator += (
loss_generator.item() - average_loss_generator
) / i
if rank == 0:
if global_step % LOG_INTERVAL == 0:
writer.add_scalar(
"train/loss_mel",
loss_mel.item(),
global_step,
)
writer.add_scalar(
"train/loss_generator",
loss_generator.item(),
global_step,
)
writer.add_scalar(
"train/loss_discriminator",
loss_discriminator.item(),
global_step,
)
if global_step % VALIDATION_INTERVAL == 0:
generator.eval()
average_validation_loss = 0
for j, (wavs, mels, tgts) in enumerate(validation_loader, 1):
wavs, mels, tgts = wavs.to(rank), mels.to(rank), tgts.to(rank)
with torch.no_grad():
wavs_ = generator(mels.squeeze(1))
mels_ = melspectrogram(wavs_)
length = min(mels_.size(-1), tgts.size(-1))
loss_mel = F.l1_loss(mels_[..., :length], tgts[..., :length])
average_validation_loss += (
loss_mel.item() - average_validation_loss
) / j
if rank == 0:
if j <= NUM_GENERATED_EXAMPLES:
writer.add_audio(
f"generated/wav_{j}",
wavs_.squeeze(0),
global_step,
sample_rate=16000,
)
writer.add_figure(
f"generated/mel_{j}",
plot_spectrogram(mels_.squeeze().cpu().numpy()),
global_step,
)
generator.train()
discriminator.train()
if rank == 0:
writer.add_scalar(
"validation/mel_loss", average_validation_loss, global_step
)
logger.info(
f"valid -- epoch: {epoch}, mel loss: {average_validation_loss:.4f}"
)
new_best = best_loss > average_validation_loss
if new_best or global_step % CHECKPOINT_INTERVAL == 0:
if new_best:
logger.info("-------- new best model found!")
best_loss = average_validation_loss
if rank == 0:
save_checkpoint(
checkpoint_dir=args.checkpoint_dir,
generator=generator,
discriminator=discriminator,
optimizer_generator=optimizer_generator,
optimizer_discriminator=optimizer_discriminator,
scheduler_generator=scheduler_generator,
scheduler_discriminator=scheduler_discriminator,
step=global_step,
loss=average_validation_loss,
best=new_best,
logger=logger,
)
scheduler_discriminator.step()
scheduler_generator.step()
logger.info(
f"train -- epoch: {epoch}, mel loss: {average_loss_mel:.4f}, generator loss: {average_loss_generator:.4f}, discriminator loss: {average_loss_discriminator:.4f}"
)
dist.destroy_process_group()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Train or finetune HiFi-GAN.")
parser.add_argument(
"dataset_dir",
metavar="dataset-dir",
help="path to the preprocessed data directory",
type=Path,
)
parser.add_argument(
"checkpoint_dir",
metavar="checkpoint-dir",
help="path to the checkpoint directory",
type=Path,
)
parser.add_argument(
"--resume",
help="path to the checkpoint to resume from",
type=Path,
)
parser.add_argument(
"--finetune",
help="whether to finetune (note that a resume path must be given)",
action="store_true",
)
args = parser.parse_args()
# display training setup info
logger.info(f"PyTorch version: {torch.__version__}")
logger.info(f"CUDA version: {torch.version.cuda}")
logger.info(f"CUDNN version: {torch.backends.cudnn.version()}")
logger.info(f"CUDNN enabled: {torch.backends.cudnn.enabled}")
logger.info(f"CUDNN deterministic: {torch.backends.cudnn.deterministic}")
logger.info(f"CUDNN benchmark: {torch.backends.cudnn.benchmark}")
logger.info(f"# of GPUS: {torch.cuda.device_count()}")
# clear handlers
logger.handlers.clear()
world_size = torch.cuda.device_count()
mp.spawn(
train_model,
args=(world_size, args),
nprocs=world_size,
join=True,
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

+132
View File
@@ -0,0 +1,132 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# VSCode project settings
.vscode
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Benjamin van Niekerk
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+163
View File
@@ -0,0 +1,163 @@
<p align="center">
<a target="_blank" href="https://colab.research.google.com/github/bshall/soft-vc/blob/main/soft-vc-demo.ipynb">
<img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/>
</a>
</p>
# HuBERT
Training and inference scripts for the HuBERT content encoders in [A Comparison of Discrete and Soft Speech Units for Improved Voice Conversion](https://ieeexplore.ieee.org/abstract/document/9746484).
For more details see [soft-vc](https://github.com/bshall/soft-vc). Audio samples can be found [here](https://bshall.github.io/soft-vc/). Colab demo can be found [here](https://colab.research.google.com/github/bshall/soft-vc/blob/main/soft-vc-demo.ipynb).
<div align="center">
<img width="100%" alt="Soft-VC"
src="https://raw.githubusercontent.com/bshall/hubert/main/content-encoder.png">
</div>
<div>
<sup>
<strong>Fig 1:</strong> Architecture of the voice conversion system. a) The <strong>discrete</strong> content encoder clusters audio features to produce a sequence of discrete speech units. b) The <strong>soft</strong> content encoder is trained to predict the discrete units. The acoustic model transforms the discrete/soft speech units into a target spectrogram. The vocoder converts the spectrogram into an audio waveform.
</sup>
</div>
## Example Usage
### Programmatic Usage
```python
import torch, torchaudio
# Load checkpoint (either hubert_soft or hubert_discrete)
hubert = torch.hub.load("bshall/hubert:main", "hubert_soft").cuda()
# Load audio
wav, sr = torchaudio.load("path/to/wav")
assert sr == 16000
wav = wav.unsqueeze(0).cuda()
# Extract speech units
units = hubert.units(x)
```
### Script-Based Usage
```
usage: encode.py [-h] [--extension EXTENSION] {soft,discrete} in-dir out-dir
Encode an audio dataset.
positional arguments:
{soft,discrete} available models (HuBERT-Soft or HuBERT-Discrete)
in-dir path to the dataset directory.
out-dir path to the output directory.
optional arguments:
-h, --help show this help message and exit
--extension EXTENSION
extension of the audio files (defaults to .flac).
```
## Training
### Step 1: Dataset Preparation
Download and extract the [LibriSpeech](https://www.openslr.org/12) corpus. The training script expects the following tree structure for the dataset directory:
```
│ lengths.json
│
└───wavs
├───dev-*
│ ├───84
│ ├───...
│ └───8842
└───train-*
├───19
├───...
└───8975
```
The `train-*` and `dev-*` directories should contain the training and validation splits respectively. Note that there can be multiple `train` and `dev` folders e.g., `train-clean-100`, `train-other-500`, etc. Finally, the `lengths.json` file should contain key-value pairs with the file path and number of samples:
```json
{
"dev-clean/1272/128104/1272-128104-0000": 93680,
"dev-clean/1272/128104/1272-128104-0001": 77040,
}
```
### Step 2: Extract Discrete Speech Units
Encode LibriSpeech using the HuBERT-Discrete model and `encode.py` script:
```
usage: encode.py [-h] [--extension EXTENSION] {soft,discrete} in-dir out-dir
Encode an audio dataset.
positional arguments:
{soft,discrete} available models (HuBERT-Soft or HuBERT-Discrete)
in-dir path to the dataset directory.
out-dir path to the output directory.
optional arguments:
-h, --help show this help message and exit
--extension EXTENSION
extension of the audio files (defaults to .flac).
```
for example:
```
python encode.py discrete path/to/LibriSpeech/wavs path/to/LibriSpeech/units
```
At this point the directory tree should look like:
```
│ lengths.json
│
├───discrete
│ ├───...
└───wavs
├───...
```
### Step 3: Train the HuBERT-Soft Content Encoder
```
usage: train.py [-h] [--resume RESUME] [--warmstart] [--mask] [--alpha ALPHA] dataset-dir checkpoint-dir
Train HuBERT soft content encoder.
positional arguments:
dataset-dir path to the data directory.
checkpoint-dir path to the checkpoint directory.
optional arguments:
-h, --help show this help message and exit
--resume RESUME path to the checkpoint to resume from.
--warmstart whether to initialize from the fairseq HuBERT checkpoint.
--mask whether to use input masking.
--alpha ALPHA weight for the masked loss.
```
## Links
- [Soft-VC repo](https://github.com/bshall/soft-vc)
- [Soft-VC paper](https://ieeexplore.ieee.org/abstract/document/9746484)
- [Official HuBERT repo](https://github.com/pytorch/fairseq)
- [HuBERT paper](https://arxiv.org/abs/2106.07447)
## Citation
If you found this work helpful please consider citing our paper:
```
@inproceedings{
soft-vc-2022,
author={van Niekerk, Benjamin and Carbonneau, Marc-André and Zaïdi, Julian and Baas, Matthew and Seuté, Hugo and Kamper, Herman},
booktitle={ICASSP},
title={A Comparison of Discrete and Soft Speech Units for Improved Voice Conversion},
year={2022}
}
```
+66
View File
@@ -0,0 +1,66 @@
from pathlib import Path
import logging
import argparse
import torch
import numpy as np
from sklearn.cluster import KMeans
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def cluster(args):
with open(args.subset) as file:
subset = [line.strip() for line in file]
logger.info(f"Loading features from {args.in_dir}")
features = []
for path in subset:
in_path = args.in_dir / path
features.append(np.load(in_path.with_suffix(".npy")))
features = np.concatenate(features, axis=0)
logger.info(f"Clustering features of shape: {features.shape}")
kmeans = KMeans(n_clusters=args.n_clusters).fit(features)
checkpoint_path = args.checkpoint_dir / f"kmeans_{args.n_clusters}.pt"
checkpoint_path.parent.mkdir(exist_ok=True, parents=True)
torch.save(
checkpoint_path,
{
"n_features_in_": kmeans.n_features_in_,
"_n_threads": kmeans._n_threads,
"cluster_centers_": kmeans.cluster_centers_,
},
)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Cluster speech features features.")
parser.add_argument(
"in_dir",
metavar="in-dir",
help="path to the encoded dataset",
type=Path,
)
parser.add_argument(
"subset",
matavar="subset",
help="path to the .txt file containing the list of files to cluster",
type=Path,
)
parser.add_argument(
"checkpoint_dir",
metavar="checkpoint-dir",
help="path to the checkpoint directory",
type=Path,
)
parser.add_argument(
"--n-clusters",
help="number of clusters",
type=int,
default=100,
)
args = parser.parse_args()
cluster(args)
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

+56
View File
@@ -0,0 +1,56 @@
import argparse
import logging
import numpy as np
from pathlib import Path
from tqdm import tqdm
import torch
import torchaudio
from torchaudio.functional import resample
def encode_dataset(args):
print(f"Loading hubert checkpoint")
hubert = torch.hub.load("bshall/hubert:main", f"hubert_{args.model}").cuda()
print(f"Encoding dataset at {args.in_dir}")
for in_path in tqdm(list(args.in_dir.rglob(f"*{args.extension}"))):
wav, sr = torchaudio.load(in_path)
wav = resample(wav, sr, 16000)
wav = wav.unsqueeze(0).cuda()
with torch.inference_mode():
units = hubert.units(wav)
out_path = args.out_dir / in_path.relative_to(args.in_dir)
out_path.parent.mkdir(parents=True, exist_ok=True)
np.save(out_path.with_suffix(".npy"), units.squeeze().cpu().numpy())
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Encode an audio dataset.")
parser.add_argument(
"model",
help="available models (HuBERT-Soft or HuBERT-Discrete)",
choices=["soft", "discrete"],
)
parser.add_argument(
"in_dir",
metavar="in-dir",
help="path to the dataset directory.",
type=Path,
)
parser.add_argument(
"out_dir",
metavar="out-dir",
help="path to the output directory.",
type=Path,
)
parser.add_argument(
"--extension",
help="extension of the audio files (defaults to .flac).",
default=".flac",
type=str,
)
args = parser.parse_args()
encode_dataset(args)
@@ -0,0 +1,3 @@
dependencies = ["torch", "torchaudio", "sklearn"]
from hubert import hubert_discrete, hubert_soft, kmeans100
@@ -0,0 +1,8 @@
from .model import (
Hubert,
HubertDiscrete,
HubertSoft,
hubert_discrete,
hubert_soft,
kmeans100,
)
@@ -0,0 +1,91 @@
import random
from pathlib import Path
import numpy as np
import json
import torch
import torch.nn.functional as F
from torch.utils.data import Dataset
import torchaudio
class AcousticUnitsDataset(Dataset):
def __init__(
self,
root: Path,
sample_rate: int = 16000,
label_rate: int = 50,
min_samples: int = 32000,
max_samples: int = 250000,
train: bool = True,
):
self.wavs_dir = root / "wavs"
self.units_dir = root / "units"
with open(root / "lengths.json") as file:
self.lenghts = json.load(file)
pattern = "train-*/**/*.flac" if train else "dev-*/**/*.flac"
metadata = (
(path, path.relative_to(self.wavs_dir).with_suffix("").as_posix())
for path in self.wavs_dir.rglob(pattern)
)
metadata = ((path, key) for path, key in metadata if key in self.lenghts)
self.metadata = [
path for path, key in metadata if self.lenghts[key] > min_samples
]
self.sample_rate = sample_rate
self.label_rate = label_rate
self.min_samples = min_samples
self.max_samples = max_samples
self.train = train
def __len__(self):
return len(self.metadata)
def __getitem__(self, index):
wav_path = self.metadata[index]
units_path = self.units_dir / wav_path.relative_to(self.wavs_dir)
wav, _ = torchaudio.load(wav_path)
wav = F.pad(wav, ((400 - 320) // 2, (400 - 320) // 2))
codes = np.load(units_path.with_suffix(".npy"))
return wav, torch.from_numpy(codes).long()
def collate(self, batch):
wavs, codes = zip(*batch)
wavs, codes = list(wavs), list(codes)
wav_lengths = [wav.size(-1) for wav in wavs]
code_lengths = [code.size(-1) for code in codes]
wav_frames = min(self.max_samples, *wav_lengths)
collated_wavs, wav_offsets = [], []
for wav in wavs:
wav_diff = wav.size(-1) - wav_frames
wav_offset = random.randint(0, wav_diff)
wav = wav[:, wav_offset : wav_offset + wav_frames]
collated_wavs.append(wav)
wav_offsets.append(wav_offset)
rate = self.label_rate / self.sample_rate
code_offsets = [round(wav_offset * rate) for wav_offset in wav_offsets]
code_frames = round(wav_frames * rate)
remaining_code_frames = [
length - offset for length, offset in zip(code_lengths, code_offsets)
]
code_frames = min(code_frames, *remaining_code_frames)
collated_codes = []
for code, code_offset in zip(codes, code_offsets):
code = code[code_offset : code_offset + code_frames]
collated_codes.append(code)
wavs = torch.stack(collated_wavs, dim=0)
codes = torch.stack(collated_codes, dim=0)
return wavs, codes
@@ -0,0 +1,293 @@
import copy
from typing import Optional, Tuple
import random
from sklearn.cluster import KMeans
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.modules.utils import consume_prefix_in_state_dict_if_present
URLS = {
"hubert-discrete": "https://github.com/bshall/hubert/releases/download/v0.1/hubert-discrete-e9416457.pt",
"hubert-soft": "https://github.com/bshall/hubert/releases/download/v0.1/hubert-soft-0d54a1f4.pt",
"kmeans100": "https://github.com/bshall/hubert/releases/download/v0.1/kmeans100-50f36a95.pt",
}
class Hubert(nn.Module):
def __init__(self, num_label_embeddings: int = 100, mask: bool = True):
super().__init__()
self._mask = mask
self.feature_extractor = FeatureExtractor()
self.feature_projection = FeatureProjection()
self.positional_embedding = PositionalConvEmbedding()
self.norm = nn.LayerNorm(768)
self.dropout = nn.Dropout(0.1)
self.encoder = TransformerEncoder(
nn.TransformerEncoderLayer(
768, 12, 3072, activation="gelu", batch_first=True
),
12,
)
self.proj = nn.Linear(768, 256)
self.masked_spec_embed = nn.Parameter(torch.FloatTensor(768).uniform_())
self.label_embedding = nn.Embedding(num_label_embeddings, 256)
def mask(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
mask = None
if self.training and self._mask:
mask = _compute_mask((x.size(0), x.size(1)), 0.8, 10, x.device, 2)
x[mask] = self.masked_spec_embed.to(x.dtype)
return x, mask
def encode(
self, x: torch.Tensor, layer: Optional[int] = None
) -> Tuple[torch.Tensor, torch.Tensor]:
x = self.feature_extractor(x)
x = self.feature_projection(x.transpose(1, 2))
x, mask = self.mask(x)
x = x + self.positional_embedding(x)
x = self.dropout(self.norm(x))
x = self.encoder(x, output_layer=layer)
return x, mask
def logits(self, x: torch.Tensor) -> torch.Tensor:
logits = torch.cosine_similarity(
x.unsqueeze(2),
self.label_embedding.weight.unsqueeze(0).unsqueeze(0),
dim=-1,
)
return logits / 0.1
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
x, mask = self.encode(x)
x = self.proj(x)
logits = self.logits(x)
return logits, mask
class HubertSoft(Hubert):
def __init__(self):
super().__init__()
@torch.inference_mode()
def units(self, wav: torch.Tensor) -> torch.Tensor:
wav = F.pad(wav, ((400 - 320) // 2, (400 - 320) // 2))
x, _ = self.encode(wav)
return self.proj(x)
class HubertDiscrete(Hubert):
def __init__(self, kmeans):
super().__init__(504)
self.kmeans = kmeans
@torch.inference_mode()
def units(self, wav: torch.Tensor) -> torch.LongTensor:
wav = F.pad(wav, ((400 - 320) // 2, (400 - 320) // 2))
x, _ = self.encode(wav, layer=7)
x = self.kmeans.predict(x.squeeze().cpu().numpy())
return torch.tensor(x, dtype=torch.long, device=wav.device)
class FeatureExtractor(nn.Module):
def __init__(self):
super().__init__()
self.conv0 = nn.Conv1d(1, 512, 10, 5, bias=False)
self.norm0 = nn.GroupNorm(512, 512)
self.conv1 = nn.Conv1d(512, 512, 3, 2, bias=False)
self.conv2 = nn.Conv1d(512, 512, 3, 2, bias=False)
self.conv3 = nn.Conv1d(512, 512, 3, 2, bias=False)
self.conv4 = nn.Conv1d(512, 512, 3, 2, bias=False)
self.conv5 = nn.Conv1d(512, 512, 2, 2, bias=False)
self.conv6 = nn.Conv1d(512, 512, 2, 2, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = F.gelu(self.norm0(self.conv0(x)))
x = F.gelu(self.conv1(x))
x = F.gelu(self.conv2(x))
x = F.gelu(self.conv3(x))
x = F.gelu(self.conv4(x))
x = F.gelu(self.conv5(x))
x = F.gelu(self.conv6(x))
return x
class FeatureProjection(nn.Module):
def __init__(self):
super().__init__()
self.norm = nn.LayerNorm(512)
self.projection = nn.Linear(512, 768)
self.dropout = nn.Dropout(0.1)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.norm(x)
x = self.projection(x)
x = self.dropout(x)
return x
class PositionalConvEmbedding(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Conv1d(
768,
768,
kernel_size=128,
padding=128 // 2,
groups=16,
)
self.conv = nn.utils.weight_norm(self.conv, name="weight", dim=2)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.conv(x.transpose(1, 2))
x = F.gelu(x[:, :, :-1])
return x.transpose(1, 2)
class TransformerEncoder(nn.Module):
def __init__(
self, encoder_layer: nn.TransformerEncoderLayer, num_layers: int
) -> None:
super(TransformerEncoder, self).__init__()
self.layers = nn.ModuleList(
[copy.deepcopy(encoder_layer) for _ in range(num_layers)]
)
self.num_layers = num_layers
def forward(
self,
src: torch.Tensor,
mask: torch.Tensor = None,
src_key_padding_mask: torch.Tensor = None,
output_layer: Optional[int] = None,
) -> torch.Tensor:
output = src
for layer in self.layers[:output_layer]:
output = layer(
output, src_mask=mask, src_key_padding_mask=src_key_padding_mask
)
return output
def _compute_mask(
shape: Tuple[int, int],
mask_prob: float,
mask_length: int,
device: torch.device,
min_masks: int = 0,
) -> torch.Tensor:
batch_size, sequence_length = shape
if mask_length < 1:
raise ValueError("`mask_length` has to be bigger than 0.")
if mask_length > sequence_length:
raise ValueError(
f"`mask_length` has to be smaller than `sequence_length`, but got `mask_length`: {mask_length} and `sequence_length`: {sequence_length}`"
)
# compute number of masked spans in batch
num_masked_spans = int(mask_prob * sequence_length / mask_length + random.random())
num_masked_spans = max(num_masked_spans, min_masks)
# make sure num masked indices <= sequence_length
if num_masked_spans * mask_length > sequence_length:
num_masked_spans = sequence_length // mask_length
# SpecAugment mask to fill
mask = torch.zeros((batch_size, sequence_length), device=device, dtype=torch.bool)
# uniform distribution to sample from, make sure that offset samples are < sequence_length
uniform_dist = torch.ones(
(batch_size, sequence_length - (mask_length - 1)), device=device
)
# get random indices to mask
mask_indices = torch.multinomial(uniform_dist, num_masked_spans)
# expand masked indices to masked spans
mask_indices = (
mask_indices.unsqueeze(dim=-1)
.expand((batch_size, num_masked_spans, mask_length))
.reshape(batch_size, num_masked_spans * mask_length)
)
offsets = (
torch.arange(mask_length, device=device)[None, None, :]
.expand((batch_size, num_masked_spans, mask_length))
.reshape(batch_size, num_masked_spans * mask_length)
)
mask_idxs = mask_indices + offsets
# scatter indices to mask
mask = mask.scatter(1, mask_idxs, True)
return mask
def hubert_discrete(
pretrained: bool = True,
progress: bool = True,
) -> HubertDiscrete:
r"""HuBERT-Discrete from `"A Comparison of Discrete and Soft Speech Units for Improved Voice Conversion"`.
Args:
pretrained (bool): load pretrained weights into the model
progress (bool): show progress bar when downloading model
"""
kmeans = kmeans100(pretrained=pretrained, progress=progress)
hubert = HubertDiscrete(kmeans)
if pretrained:
checkpoint = torch.hub.load_state_dict_from_url(
URLS["hubert-discrete"], progress=progress
)
consume_prefix_in_state_dict_if_present(checkpoint, "module.")
hubert.load_state_dict(checkpoint)
hubert.eval()
return hubert
def hubert_soft(
pretrained: bool = True,
progress: bool = True,
) -> HubertSoft:
r"""HuBERT-Soft from `"A Comparison of Discrete and Soft Speech Units for Improved Voice Conversion"`.
Args:
pretrained (bool): load pretrained weights into the model
progress (bool): show progress bar when downloading model
"""
hubert = HubertSoft()
if pretrained:
checkpoint = torch.hub.load_state_dict_from_url(
URLS["hubert-soft"], progress=progress
)
consume_prefix_in_state_dict_if_present(checkpoint, "module.")
hubert.load_state_dict(checkpoint)
hubert.eval()
return hubert
def _kmeans(
num_clusters: int, pretrained: bool = True, progress: bool = True
) -> KMeans:
kmeans = KMeans(num_clusters)
if pretrained:
checkpoint = torch.hub.load_state_dict_from_url(
URLS[f"kmeans{num_clusters}"], progress=progress
)
kmeans.__dict__["n_features_in_"] = checkpoint["n_features_in_"]
kmeans.__dict__["_n_threads"] = checkpoint["_n_threads"]
kmeans.__dict__["cluster_centers_"] = checkpoint["cluster_centers_"].numpy()
return kmeans
def kmeans100(pretrained: bool = True, progress: bool = True) -> KMeans:
r"""
k-means checkpoint for HuBERT-Discrete with 100 clusters.
Args:
pretrained (bool): load pretrained weights into the model
progress (bool): show progress bar when downloading model
"""
return _kmeans(100, pretrained, progress)
@@ -0,0 +1,58 @@
import torch
class Metric:
def __init__(self):
self.steps = 0
self.value = 0
def update(self, value):
self.steps += 1
self.value += (value - self.value) / self.steps
return self.value
def reset(self):
self.steps = 0
self.value = 0
def save_checkpoint(
checkpoint_dir,
hubert,
optimizer,
scaler,
step,
loss,
best,
logger,
):
state = {
"hubert": hubert.state_dict(),
"optimizer": optimizer.state_dict(),
"scaler": scaler.state_dict(),
"step": step,
"loss": loss,
}
checkpoint_dir.mkdir(exist_ok=True, parents=True)
checkpoint_path = checkpoint_dir / f"model-{step}.pt"
torch.save(state, checkpoint_path)
if best:
best_path = checkpoint_dir / "model-best.pt"
torch.save(state, best_path)
logger.info(f"Saved checkpoint: {checkpoint_path.stem}")
def load_checkpoint(
load_path,
hubert,
optimizer,
scaler,
rank,
logger,
):
logger.info(f"Loading checkpoint from {load_path}")
checkpoint = torch.load(load_path, map_location={"cuda:0": f"cuda:{rank}"})
hubert.load_state_dict(checkpoint["hubert"])
scaler.load_state_dict(checkpoint["scaler"])
optimizer.load_state_dict(checkpoint["optimizer"])
return checkpoint["step"], checkpoint["loss"]
+453
View File
@@ -0,0 +1,453 @@
import argparse
import logging
from pathlib import Path
import torch
import torch.cuda.amp as amp
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
import torch.distributed as dist
from torch.utils.data.distributed import DistributedSampler
import torch.multiprocessing as mp
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.nn.modules.utils import consume_prefix_in_state_dict_if_present
from hubert.model import Hubert, URLS
from hubert.dataset import AcousticUnitsDataset
from hubert.utils import Metric, save_checkpoint, load_checkpoint
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
########################################################################################
# Define hyperparameters for training:
########################################################################################
BATCH_SIZE = 64
LEARNING_RATE = 2e-5
BETAS = (0.9, 0.98)
EPS = 1e-06
WEIGHT_DECAY = 1e-2
MAX_NORM = 10
STEPS = 25000
LOG_INTERVAL = 5
VALIDATION_INTERVAL = 1000
CHECKPOINT_INTERVAL = 1000
BACKEND = "nccl"
INIT_METHOD = "tcp://localhost:54321"
def train(rank, world_size, args):
dist.init_process_group(
BACKEND,
rank=rank,
world_size=world_size,
init_method=INIT_METHOD,
)
####################################################################################
# Setup logging utilities:
####################################################################################
log_dir = args.checkpoint_dir / "logs"
log_dir.mkdir(exist_ok=True, parents=True)
if rank == 0:
logger.setLevel(logging.INFO)
handler = logging.FileHandler(log_dir / f"{args.checkpoint_dir.stem}.log")
handler.setLevel(logging.INFO)
formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s", datefmt="%m/%d/%Y %I:%M:%S"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
else:
logger.setLevel(logging.ERROR)
writer = SummaryWriter(log_dir) if rank == 0 else None
####################################################################################
# Initialize models
####################################################################################
hubert = Hubert(mask=args.mask).to(rank)
if args.warmstart:
checkpoint = torch.hub.load_state_dict_from_url(
URLS["hubert-discrete"], map_location={"cuda:0": f"cuda:{rank}"}
)
consume_prefix_in_state_dict_if_present(checkpoint, "module.")
hubert.load_state_dict(checkpoint, strict=False)
hubert = DDP(hubert, device_ids=[rank])
####################################################################################
# Initialze optimizer and grad scaler
####################################################################################
optimizer = optim.AdamW(
hubert.parameters(),
lr=LEARNING_RATE,
betas=BETAS,
eps=EPS,
weight_decay=WEIGHT_DECAY,
)
scaler = amp.GradScaler()
####################################################################################
# Initialize datasets and dataloaders
####################################################################################
train_dataset = AcousticUnitsDataset(
root=args.dataset_dir,
train=True,
)
train_sampler = DistributedSampler(train_dataset, drop_last=True)
train_loader = DataLoader(
train_dataset,
collate_fn=train_dataset.collate,
batch_size=BATCH_SIZE,
sampler=train_sampler,
num_workers=8,
pin_memory=True,
shuffle=False,
drop_last=True,
)
validation_dataset = AcousticUnitsDataset(
root=args.dataset_dir,
train=False,
)
validation_loader = DataLoader(
validation_dataset,
batch_size=1,
shuffle=False,
num_workers=8,
pin_memory=True,
)
####################################################################################
# Load checkpoint if args.resume is set
####################################################################################
if args.resume is not None:
global_step, best_loss = load_checkpoint(
load_path=args.resume,
hubert=hubert,
optimizer=optimizer,
scaler=scaler,
rank=rank,
logger=logger,
)
else:
global_step, best_loss = 0, float("inf")
# =================================================================================#
# Start training loop
# =================================================================================#
n_epochs = STEPS // len(train_loader) + 1
start_epoch = global_step // len(train_loader) + 1
logger.info("**" * 40)
logger.info(f"PyTorch version: {torch.__version__}")
logger.info(f"CUDA version: {torch.version.cuda}")
logger.info(f"CUDNN version: {torch.backends.cudnn.version()}")
logger.info(f"CUDNN enabled: {torch.backends.cudnn.enabled}")
logger.info(f"CUDNN deterministic: {torch.backends.cudnn.deterministic}")
logger.info(f"CUDNN benchmark: {torch.backends.cudnn.benchmark}")
logger.info(f"# of GPUS: {torch.cuda.device_count()}")
logger.info(f"batch size: {BATCH_SIZE}")
logger.info(f"iterations per epoch: {len(train_loader)}")
logger.info(f"# of epochs: {n_epochs}")
logger.info(f"started at epoch: {start_epoch}")
logger.info("**" * 40 + "\n")
if args.mask:
average_masked_loss = Metric()
average_unmasked_loss = Metric()
average_masked_accuracy = Metric()
average_unmasked_accuracy = Metric()
epoch_masked_loss = Metric()
epoch_unmasked_loss = Metric()
epoch_masked_accuracy = Metric()
epoch_unmasked_accuracy = Metric()
else:
average_loss = Metric()
average_accuracy = Metric()
epoch_loss = Metric()
epoch_accuracy = Metric()
validation_loss = Metric()
validation_accuracy = Metric()
for epoch in range(start_epoch, n_epochs + 1):
train_sampler.set_epoch(epoch)
hubert.train()
if args.mask:
epoch_masked_loss.reset()
epoch_unmasked_loss.reset()
epoch_masked_accuracy.reset()
epoch_unmasked_accuracy.reset()
else:
epoch_loss.reset()
epoch_accuracy.reset()
for wavs, codes in train_loader:
global_step += 1
wavs, codes = wavs.to(rank), codes.to(rank)
############################################################################
# Compute training loss
############################################################################
optimizer.zero_grad()
with amp.autocast():
logits, mask = hubert(wavs)
length = min(
mask.size(-1) if args.mask else float("inf"), codes.size(-1)
)
logits = logits[:, :length, :]
codes = codes[:, :length]
if args.mask:
mask = mask[:, :length]
if args.mask:
masked_loss = F.cross_entropy(logits[mask], codes[mask])
unmasked_loss = F.cross_entropy(logits[~mask], codes[~mask])
loss = args.alpha * masked_loss + (1 - args.alpha) * unmasked_loss
else:
loss = F.cross_entropy(logits.transpose(1, 2), codes)
scaler.scale(loss).backward()
scaler.unscale_(optimizer)
nn.utils.clip_grad_norm_(hubert.parameters(), MAX_NORM)
scaler.step(optimizer)
scaler.update()
if args.mask:
masked_accuracy = logits[mask].argmax(dim=-1) == codes[mask]
masked_accuracy = torch.mean(masked_accuracy.float())
unmasked_accuracy = logits[~mask].argmax(dim=-1) == codes[~mask]
unmasked_accuracy = torch.mean(unmasked_accuracy.float())
else:
accuracy = logits.argmax(dim=-1) == codes
accuracy = torch.mean(accuracy.float())
############################################################################
# Update and log training metrics
############################################################################
if args.mask:
average_masked_loss.update(masked_loss.item())
average_unmasked_loss.update(unmasked_loss.item())
average_masked_accuracy.update(masked_accuracy.item())
average_unmasked_accuracy.update(unmasked_accuracy.item())
epoch_masked_loss.update(masked_loss.item())
epoch_unmasked_loss.update(unmasked_loss.item())
epoch_masked_accuracy.update(masked_accuracy.item())
epoch_unmasked_accuracy.update(unmasked_accuracy.item())
else:
average_loss.update(loss.item())
average_accuracy.update(accuracy.item())
epoch_loss.update(loss.item())
epoch_accuracy.update(accuracy.item())
if rank == 0 and global_step % LOG_INTERVAL == 0:
if args.mask:
writer.add_scalar(
"train/masked_loss",
average_masked_loss.value,
global_step,
)
writer.add_scalar(
"train/unmasked_loss",
average_unmasked_loss.value,
global_step,
)
writer.add_scalar(
"train/masked_accuracy",
average_masked_accuracy.value * 100,
global_step,
)
writer.add_scalar(
"train/unmasked_accuracy",
average_unmasked_accuracy.value * 100,
global_step,
)
average_masked_loss.reset()
average_unmasked_loss.reset()
average_masked_accuracy.reset()
average_unmasked_accuracy.reset()
else:
writer.add_scalar(
"train/loss",
average_loss.value,
global_step,
)
writer.add_scalar(
"train/accuracy",
average_accuracy.value,
global_step,
)
average_loss.reset()
average_accuracy.reset()
# --------------------------------------------------------------------------#
# Start validation loop
# --------------------------------------------------------------------------#
if global_step % VALIDATION_INTERVAL == 0:
hubert.eval()
validation_loss.reset()
validation_accuracy.reset()
for wavs, codes in validation_loader:
wavs, codes = wavs.to(rank), codes.to(rank)
with torch.no_grad():
logits, _ = hubert(wavs)
logits = logits.transpose(1, 2)
loss = F.cross_entropy(logits, codes)
accuracy = logits.argmax(dim=1) == codes
accuracy = torch.mean(accuracy.float())
####################################################################
# Update validation metrics
####################################################################
validation_loss.update(loss.item())
validation_accuracy.update(accuracy.item())
hubert.train()
############################################################################
# Log validation metrics
############################################################################
if rank == 0:
writer.add_scalar(
"validation/unit_loss",
validation_loss.value,
global_step,
)
writer.add_scalar(
"validation/unit_accuracy",
validation_accuracy.value * 100,
global_step,
)
logger.info(
f"valid -- epoch: {epoch}, loss: {validation_loss.value:.4f}, accuracy: {validation_accuracy.value * 100:.2f}"
)
############################################################################
# Save model checkpoint
############################################################################
new_best = best_loss > validation_loss.value
if new_best or global_step % CHECKPOINT_INTERVAL == 0:
if new_best:
logger.info("-------- new best model found!")
best_loss = validation_loss.value
if rank == 0:
save_checkpoint(
checkpoint_dir=args.checkpoint_dir,
hubert=hubert,
optimizer=optimizer,
scaler=scaler,
step=global_step,
loss=validation_loss.value,
best=new_best,
logger=logger,
)
# -----------------------------------------------------------------------------#
# End validation loop
# -----------------------------------------------------------------------------#
####################################################################################
# Log training metrics
####################################################################################
logger.info(
f"""
train -- epoch: {epoch}, masked loss: {epoch_masked_loss.value:.4f}, unmasked loss: {epoch_unmasked_loss.value:.4f},
masked accuracy: {epoch_masked_accuracy.value * 100:.2f}, umasked accuracy: {epoch_unmasked_accuracy.value * 100:.2f}
"""
)
# ==================================================================================#
# End training loop
# ==================================================================================#
dist.destroy_process_group()
def train_hubert(args):
world_size = torch.cuda.device_count()
mp.spawn(
train,
args=(world_size, args),
nprocs=world_size,
join=True,
)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Train HuBERT soft content encoder.")
parser.add_argument(
"dataset_dir",
metavar="dataset-dir",
help="path to the data directory.",
type=Path,
)
parser.add_argument(
"checkpoint_dir",
metavar="checkpoint-dir",
help="path to the checkpoint directory.",
type=Path,
)
parser.add_argument(
"--resume",
help="path to the checkpoint to resume from.",
type=Path,
)
parser.add_argument(
"--warmstart",
help="whether to initialize from the fairseq HuBERT checkpoint.",
action="store_true",
)
parser.add_argument(
"--mask",
help="whether to use input masking.",
action="store_true",
)
parser.add_argument(
"--alpha",
help="weight for the masked loss.",
default=1,
type=float,
)
args = parser.parse_args()
world_size = torch.cuda.device_count()
mp.spawn(
train,
args=(world_size, args),
nprocs=world_size,
join=True,
)
@@ -0,0 +1,508 @@
# -*- coding: utf-8 -*-
"""SoftVCTraceAndOnnx.ipynb
Automatically generated by Colaboratory.
Original file is located at
https://colab.research.google.com/drive/1g0YsQQY8fzXHmZUWAMzQO60c6OUlyKBl
"""
!pip3 install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu116
!pip3 install sklearn
!pip3 install soundfile
!pip3 install matplotlib
!git clone https://github.com/bshall/hubert
!git clone https://github.com/bshall/acoustic-model
!git clone https://github.com/bshall/hifigan
!nvidia-smi
from google.colab import drive
drive.mount('/content/drive')
# Torch Script Inference Hubert
import copy
from typing import Optional, Tuple
import random
from sklearn.cluster import KMeans
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.modules.utils import consume_prefix_in_state_dict_if_present
import torch.nn.utils as utils
from torch.nn.utils.weight_norm import WeightNorm
from typing import Dict, Any
from collections import OrderedDict
def remove_weight_norm(module):
module_list = [mod for mod in module.children()]
if len(module_list) == 0:
for k, hook in module._forward_pre_hooks.items():
if isinstance(hook, WeightNorm):
hook.remove(module)
del module._forward_pre_hooks[k]
else:
for mod in module_list:
remove_weight_norm(mod)
def modify_model(state_dict:Dict[str,Any]):
new_state_dict = OrderedDict()
for k in state_dict['acoustic-model'].keys():
value = state_dict['acoustic-model'][k]
name = k[7:] # remove `module.` because they used ddp training to save
new_state_dict[name] = value
return new_state_dict
# @torch.jit.export
# def generate(self, x: torch.Tensor) -> torch.Tensor:
# x = self.encoder(x)
# return self.decoder.generate(x)
class AcousticModelScripted(nn.Module):
def __init__(self, discrete: bool = False, upsample: bool = True):
super().__init__()
self.encoder = Encoder(discrete, upsample)
self.decoder = Decoder()
def forward(self, x: torch.Tensor, mels: torch.Tensor) -> torch.Tensor:
x = self.encoder(x)
return self.decoder(x, mels)
@torch.jit.export
def generate(self, x: torch.Tensor) -> torch.Tensor:
x = self.encoder(x)
return self.decoder.generate(x)
class Encoder(nn.Module):
def __init__(self, discrete: bool = False, upsample: bool = True):
super().__init__()
self.embedding = nn.Embedding(100 + 1, 256) if discrete else None
self.prenet = PreNet(256, 256, 256)
self.convs = nn.Sequential(
nn.Conv1d(256, 512, 5, 1, 2),
nn.ReLU(),
nn.InstanceNorm1d(512),
nn.ConvTranspose1d(512, 512, 4, 2, 1) if upsample else nn.Identity(),
nn.Conv1d(512, 512, 5, 1, 2),
nn.ReLU(),
nn.InstanceNorm1d(512),
nn.Conv1d(512, 512, 5, 1, 2),
nn.ReLU(),
nn.InstanceNorm1d(512),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.embedding is not None:
x = self.embedding(x)
x = self.prenet(x)
x = self.convs(x.transpose(1, 2))
return x.transpose(1, 2)
class Decoder(nn.Module):
def __init__(self):
super().__init__()
self.prenet = PreNet(128, 256, 256)
self.lstm1 = nn.LSTM(512 + 256, 768, batch_first=True)
self.lstm2 = nn.LSTM(768, 768, batch_first=True)
self.lstm3 = nn.LSTM(768, 768, batch_first=True)
self.proj = nn.Linear(768, 128, bias=False)
def forward(self, x: torch.Tensor, mels: torch.Tensor) -> torch.Tensor:
mels = self.prenet(mels)
x, _ = self.lstm1(torch.cat((x, mels), dim=-1))
res = x
x, _ = self.lstm2(x)
x = res + x
res = x
x, _ = self.lstm3(x)
x = res + x
return self.proj(x)
@torch.inference_mode()
def generate(self, xs: torch.Tensor) -> torch.Tensor:
m = torch.zeros(xs.size(0), 128, device=xs.device)
h1 = torch.zeros(1, xs.size(0), 768, device=xs.device)
c1 = torch.zeros(1, xs.size(0), 768, device=xs.device)
h2 = torch.zeros(1, xs.size(0), 768, device=xs.device)
c2 = torch.zeros(1, xs.size(0), 768, device=xs.device)
h3 = torch.zeros(1, xs.size(0), 768, device=xs.device)
c3 = torch.zeros(1, xs.size(0), 768, device=xs.device)
mel = []
for x in torch.unbind(xs, dim=1):
m = self.prenet(m)
x = torch.cat((x, m), dim=1).unsqueeze(1)
x1, (h1, c1) = self.lstm1(x, (h1, c1))
x2, (h2, c2) = self.lstm2(x1, (h2, c2))
x = x1 + x2
x3, (h3, c3) = self.lstm3(x, (h3, c3))
x = x + x3
m = self.proj(x).squeeze(1)
mel.append(m)
return torch.stack(mel, dim=1)
class PreNet(nn.Module):
def __init__(
self,
input_size: int,
hidden_size: int,
output_size: int,
dropout: float = 0.5,
):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_size, hidden_size),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(hidden_size, output_size),
nn.ReLU(),
nn.Dropout(dropout),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.net(x)
import IPython.display as display
import torch, torchaudio
import torch.nn as nn
import sys
import soundfile as sf
from time import time
import torch.jit
sys.path.insert(0, './hubert/hubert')
from model import HubertSoft
sys.path.insert(0, './acoustic-model/')
from acoustic.model import AcousticModel
#@title select input wav
file_name = "/content/drive/MyDrive/VCModels/Source/brandon1.wav"#@param { type: "string"}
source, sr = torchaudio.load(f"{file_name}")
source = torchaudio.functional.resample(source, sr, 16000)
source = source.unsqueeze(0).cuda()
print(f"Wav Shape and Type {source.shape} {source.dtype}")
if source.shape[1] == 2: # remove channel
source = torch.mean(source, dim=0).unsqueeze(0)
print(f"Removing Channel to mono Wav Shape and Type {source.shape} {source.dtype}")
display.Audio(source.squeeze().cpu(), rate=16000)
import datetime
hubert_soft = torch.hub.load("bshall/hubert:main", "hubert_soft").cuda()
hubert_soft.eval()
script_acoustic = True #@param {type: "boolean"}
print(script_acoustic)
# Load the acoustic model (either hubert_soft or hubert_discrete)
input_acoustic_model_file = "/content/drive/MyDrive/VCModels/Padme/model-24000.pt" #@param {type: "string"}
acoustic_dict = torch.load(input_acoustic_model_file) # this used ddp ## REPLACE THIS! <-- !
if script_acoustic == True:
acoustic = AcousticModelScripted().cuda()
else:
acoustic = AcousticModel().cuda()
acoustic.eval()
new_state_dict = modify_model(state_dict=acoustic_dict)
acoustic.load_state_dict(new_state_dict)
import importlib.util
import sys
spec = importlib.util.spec_from_file_location("hifigan.utils", "/content/hifigan/hifigan/utils.py")
u = importlib.util.module_from_spec(spec)
sys.modules["hifigan.utils"] = u
spec.loader.exec_module(u)
spec = importlib.util.spec_from_file_location("hifigan.generator", "/content/hifigan/hifigan/generator.py")
g = importlib.util.module_from_spec(spec)
sys.modules["hifigan.generator"] = g
spec.loader.exec_module(g)
new_state_dict = modify_model(state_dict=acoustic_dict)
acoustic.load_state_dict(new_state_dict)
input_hifigan_checkpoint_path = "/content/drive/MyDrive/VCModels/Hifigan/Padme.pt" #@param {type: "string"}
if input_hifigan_checkpoint_path:
checkpoint = torch.load(input_hifigan_checkpoint_path)["generator"]["model"]
hifigan = g.HifiganGenerator()
consume_prefix_in_state_dict_if_present(checkpoint, "module.")
hifigan.load_state_dict(checkpoint)
hifigan.remove_weight_norm()
else:
hifigan = torch.hub.load("bshall/hifigan:main", "hifigan_hubert_soft")
hifigan.cuda()
hifigan.eval()
#utils.remove_weight_norm(hubert_soft.positional_embedding.conv)
# Convert to the target speaker
units = None
mel = None
unit_start_time = 0.0
unit_end_time = 0.0
acoustic_start_time = 0.0
acoustic_end_time = 0.0
hifi_start_time = 0.0
hifi_end_time = 0.0
with torch.inference_mode():
unit_start_time = datetime.datetime.now()
units = hubert_soft.units(source)
print(f"Units and Type {units.shape} {units.dtype}")
unit_end_time = datetime.datetime.now()
hubert_soft_script = torch.jit.trace_module(hubert_soft,{"units":source})
torch.jit.save(hubert_soft_script, 'hubert_soft.jit')
print(f"Unit Generation Time {(unit_end_time - unit_start_time).total_seconds() * 1000}")
print("Hubert Soft Exported")
acoustic_start_time = datetime.datetime.now()
mel = acoustic.generate(units).transpose(1, 2)
print(f"Mel and Type {mel.shape} {mel.dtype}")
acoustic_end_time = datetime.datetime.now()
print(f"Acoustic Time {(acoustic_end_time - acoustic_start_time).total_seconds() * 1000}")
if script_acoustic == True:
output_acoustic_scripted = '/content/drive/MyDrive/VCModels/FinishedModels/Padme.recast' #@param {type: "string"}
print("Exporting Scripted")
acoustic_model_script = torch.jit.script(acoustic)
torch.jit.save(acoustic_model_script, output_acoustic_scripted)
else:
print("Exporting Traced")
acoustic_model_script = torch.jit.trace_module(acoustic,{"generate":units})
output_acoustic_traced = '' #@param {type: "string"}
torch.jit.save(acoustic_model_script, output_acoustic_traced)
print("Acoustic Model Exported")
hifi_start_time = datetime.datetime.now()
target = hifigan(mel)
print(f"Wavform and Type {target.shape} {target.dtype}")
hifi_end_time = datetime.datetime.now()
print(f"MEL {target.shape}")
print(f"Hifi Gan Time {(hifi_end_time - hifi_start_time).total_seconds() * 1000}")
hifi_gan_script = torch.jit.trace_module(hifigan,{"forward":mel})
output_hifigan_jit = '/content/drive/MyDrive/VCModels/FinishedModels/Padme-hifigan-jit.pt' #@param {type: "string"}
torch.jit.save(hifi_gan_script, output_hifigan_jit)
print("Hifi Gan Model Exported")
import datetime
jit_unit_start_time = 0.0
jit_unit_end_time = 0.0
jit_unit_final = 0.0
jit_acoustic_start_time = 0.0
jit_acoustic_end_time = 0.0
jit_acoustic_final = 0.0
jit_hifi_start_time = 0.0
jit_hifi_end_time = 0.0
jit_hifi_final = 0.0
jit_model = torch.jit.load('hubert_soft.jit').cuda()
jit_model.eval()
with torch.no_grad():
jit_unit_start_time = datetime.datetime.now()
jit_tensor = jit_model.units(source)
jit_unit_end_time = datetime.datetime.now()
jit_unit_final = jit_unit_end_time - jit_unit_start_time
print(jit_unit_final.total_seconds() * 1000)
print(jit_tensor.shape)
assert torch.allclose(jit_tensor,units)
print(script_acoustic)
if script_acoustic == True:
acoustic_jit_model = torch.jit.load(output_acoustic_scripted).cuda()
print("Using Scripted Model")
else:
acoustic_jit_model = torch.jit.load(output_acoustic_traced).cuda()
acoustic_jit_model.eval()
with torch.no_grad():
if script_acoustic == True:
print("Using Scripted Model")
jit_acoustic_start_time = datetime.datetime.now()
jit_mel = acoustic_jit_model.generate(jit_tensor)
else:
jit_acoustic_start_time = datetime.datetime.now()
jit_mel = acoustic_jit_model.generate(jit_tensor)
jit_acoustic_end_time = datetime.datetime.now()
jit_acoustic_final = jit_acoustic_end_time - jit_acoustic_start_time
print(jit_acoustic_final.total_seconds() * 1000)
jit_mel = jit_mel.transpose(1,2)
print(jit_mel.shape)
assert torch.allclose(mel,jit_mel)
hifi_gan = torch.jit.load(output_hifigan_jit).cuda()
hifi_gan.eval()
with torch.no_grad():
jit_hifi_start_time = datetime.datetime.now()
jit_target = hifi_gan(jit_mel)
jit_hifi_end_time = datetime.datetime.now()
jit_hifi_final = jit_hifi_end_time - jit_hifi_start_time
print(jit_hifi_final.total_seconds() * 1000)
assert torch.allclose(target,jit_target)
sf.write(f'./sample_jit.wav', jit_target.squeeze().detach().cpu().numpy(), samplerate=16000)
sf.write(f'./sample.wav', target.squeeze().cpu().detach().numpy(), samplerate=16000)
#uploaded = files.upload()
import datetime
jit_unit_start_time = 0.0
jit_unit_end_time = 0.0
jit_unit_final = 0.0
jit_acoustic_start_time = 0.0
jit_acoustic_end_time = 0.0
jit_acoustic_final = 0.0
jit_hifi_start_time = 0.0
jit_hifi_end_time = 0.0
jit_hifi_final = 0.0
source, sr = torchaudio.load(f"{file_name}")
source = torchaudio.functional.resample(source, sr, 16000)
source = source.unsqueeze(0).cuda()
print(f"Wav Shape and Type {source.shape} {source.dtype}")
if source.shape[1] == 2: # remove channel
source = torch.mean(source, dim=1).unsqueeze(0)
print(f"Removing Channel to mono Wav Shape and Type {source.shape} {source.dtype}")
display.Audio(source.squeeze().cpu(), rate=16000)
jit_model = torch.jit.load('hubert_soft.jit').cuda()
jit_model.eval()
if script_acoustic == True:
acoustic_jit_model = torch.jit.load(output_acoustic_scripted).cuda()
print("Loading Scripted Model")
else:
acoustic_jit_model = torch.jit.load(output_acoustic_traced).cuda()
print("Loading Tracing Model")
acoustic_jit_model.eval()
hifi_gan = torch.jit.load(output_hifigan_jit).cuda()
hifi_gan.eval()
print("Loaded Models")
# Warm up the model to get the real time
for i in range(0,5):
with torch.no_grad():
jit_unit_start_time = datetime.datetime.now()
jit_tensor = jit_model.units(source)
jit_unit_end_time = datetime.datetime.now()
if script_acoustic == True:
print("Using Scripted Model TT2")
jit_acoustic_start_time = datetime.datetime.now()
jit_mel = acoustic_jit_model.generate(jit_tensor)
else:
print("Using Traced Model TT2")
jit_acoustic_start_time = datetime.datetime.now()
jit_mel = acoustic_jit_model.generate(jit_tensor)
jit_mel = jit_mel.transpose(1, 2)
jit_acoustic_end_time = datetime.datetime.now()
jit_hifi_start_time = datetime.datetime.now()
jit_target = hifi_gan(jit_mel)
jit_hifi_end_time = datetime.datetime.now()
jit_unit_final = jit_unit_end_time - jit_unit_start_time
jit_acoustic_final = jit_acoustic_end_time - jit_acoustic_start_time
jit_hifi_final = jit_hifi_end_time - jit_hifi_start_time
print(f"16 Khz Wav Shape {source.shape}")
print(f"Jit Tensor Shape for Unit Inputs: {jit_tensor.shape}")
print(f"Hubert Runtime : {jit_unit_final.total_seconds() * 1000}")
print(f"Tactron 2 Runtime Mel Output Shape: {jit_acoustic_final.total_seconds() * 1000}")
print(f"Jit Tensor Shape for Mel output: {jit_mel.shape}")
print(f"Hifi Runtime Mel Output Shape: {jit_hifi_final.total_seconds() * 1000}")
print(f"Jit Tensor Shape for Mel output: {jit_target.shape}")
sf.write(f'./{file_name}_jit.wav', jit_target.squeeze().detach().cpu().numpy(), samplerate=16000)
display.Audio(jit_target.squeeze().cpu(), rate=16000)
# silero vad
import datetime
source, sr = torchaudio.load(f"{file_name}")
source = torchaudio.functional.resample(source, sr, 16000)
print(source.shape)
input_vad_file = "/content/drive/MyDrive/tts/softvc-checkpoints/silero_vad.jit" #@param {type: "string"}
class VADIterator:
# You will only need to verify the 100 ms chunks to see this checks a minute of audio
# sound_sample_size run vad in 30 ms, 60 ms or 100 ms
def __init__(self,threshold: float = 0.5, sampling_rate: int = 16000,sound_sample_size:float = 100):
self.silero_jit = torch.jit.load(input_vad_file).cuda()
self.silero_jit.eval()
self.sampling_rate = sampling_rate
self.sound_sample_size = sound_sample_size
jit_silero_start_time = 0.0
jit_silero_end_time = 0.0
jit_silero_final = 0.0
def operating_frame_size(self,sampling_rate:int,ms_of_audio:float):
return int((ms_of_audio * sampling_rate) / 1000) # number of frames
def check_voice_activity(self,source_sample) -> float:
probs_over_time = []
# formula we know that we can get 1600 operating frames / 16000 sr * 1000 = 100 ms of audio from 1600 frames.
# this gives you the operating frames given the audio size you want.
op_frame_size = self.operating_frame_size(sampling_rate=self.sampling_rate,ms_of_audio=self.sound_sample_size)
print(op_frame_size)
iters = int(source_sample.shape[-1] / op_frame_size) # get the chunks
#print(iters)
for i in range(0,iters): # get sound_sample_size ms chunks
#print(window_size)
chunk = source_sample[i * op_frame_size:(i + 1) * op_frame_size]
with torch.no_grad():# slide window across using VAD 100ms
jit_silero_start_time = datetime.datetime.now()
try:
print(f"Current {i}")
prob = self.silero_jit(chunk.cuda(),sr=self.sampling_rate).item() # input shape is [1*] just an array of samples. 1 = false
jit_silero_end_time = datetime.datetime.now()
print(f"Probability: {prob}")
jit_silero_final = jit_silero_end_time - jit_silero_start_time
#print(f"Silero VAD Time: {jit_silero_final.total_seconds() * 1000}")
probs_over_time.append(prob)
except Exception as e:
print(e)
return probs_over_time
iterator = VADIterator(sound_sample_size=100)
source_sample = source_sample=source.squeeze().squeeze()
print(source_sample.shape)
probs = iterator.check_voice_activity(source_sample=source_sample)
#print(probs)