Audio-to-Audio
audio
speech
voice-conversion
GaboChoropan's picture
Upload 221 files
d4c5383 verified
# %% [markdown]
# ## Settings
# %%
import argparse
import gc
import json
import math
import os
import shutil
import warnings
from collections import defaultdict
from copy import deepcopy
from fractions import Fraction
from functools import partial
from pathlib import Path
from pprint import pprint
from random import Random
from typing import BinaryIO, Literal, Optional, Union
import numpy as np
import pyworld
import torch
import torch.nn as nn
import torchaudio
from torch.nn import functional as F
from torch.nn.utils import remove_weight_norm, weight_norm
from torch.utils.tensorboard import SummaryWriter
from tqdm.auto import tqdm
assert "soundfile" in torchaudio.list_audio_backends()
# モジュールのバージョンではない
PARAPHERNALIA_VERSION = "2.0.0-alpha.2"
def is_notebook() -> bool:
return "get_ipython" in globals()
def repo_root() -> Path:
d = Path.cwd() / "dummy" if is_notebook() else Path(__file__)
assert d.is_absolute(), d
for d in d.parents:
if (d / ".git").is_dir():
return d
raise RuntimeError("Repository root is not found.")
# ハイパーパラメータ
# 学習データや出力ディレクトリなど、学習ごとに変わるようなものはここに含めない
dict_default_hparams = {
# train
"learning_rate": 1e-4,
"min_learning_rate": 5e-6,
"adam_betas": [0.8, 0.99],
"adam_eps": 1e-6,
"batch_size": 8,
"grad_weight_mel": 1.0, # grad_weight は比が同じなら同じ意味になるはず
"grad_weight_adv": 1.0,
"grad_weight_fm": 1.0,
"grad_balancer_ema_decay": 0.995,
"use_amp": True,
"num_workers": 16,
"n_steps": 20000,
"warmup_steps": 10000,
"in_sample_rate": 16000, # 変更不可
"out_sample_rate": 24000, # 変更不可
"wav_length": 4 * 24000, # 4s
"segment_length": 100, # 1s
# data
"phone_extractor_file": "assets/pretrained/003b_checkpoint_03000000.pt",
"pitch_estimator_file": "assets/pretrained/008_1_checkpoint_00300000.pt",
"in_ir_wav_dir": "assets/ir",
"in_noise_wav_dir": "assets/noise",
"in_test_wav_dir": "assets/test",
"pretrained_file": "assets/pretrained/040c_checkpoint_libritts_r_200_02300000.pt", # None も可
# model
"hidden_channels": 256, # ファインチューン時変更不可、変更した場合は推論側の対応必要
"san": False, # ファインチューン時変更不可
}
if __name__ == "__main__":
# スクリプト内部のデフォルト設定と assets/default_config.json が同期されているか確認
default_config_file = repo_root() / "assets/default_config.json"
if default_config_file.is_file():
with open(default_config_file, encoding="utf-8") as f:
default_config: dict = json.load(f)
for key, value in dict_default_hparams.items():
if key not in default_config:
warnings.warn(f"{key} not found in default_config.json.")
else:
if value != default_config[key]:
warnings.warn(
f"{key} differs between default_config.json ({default_config[key]}) and internal default hparams ({value})."
)
del default_config[key]
for key in default_config:
warnings.warn(f"{key} found in default_config.json is unknown.")
else:
warnings.warn("dafualt_config.json not found.")
def prepare_training_configs_for_experiment() -> tuple[dict, Path, Path, bool]:
import ipynbname
from IPython import get_ipython
h = deepcopy(dict_default_hparams)
in_wav_dataset_dir = repo_root() / "../../data/processed/libritts_r_200"
try:
notebook_name = ipynbname.name()
except FileNotFoundError:
notebook_name = Path(get_ipython().user_ns["__vsc_ipynb_file__"]).name
out_dir = repo_root() / "notebooks" / notebook_name.split(".")[0].split("_")[0]
resume = False
return h, in_wav_dataset_dir, out_dir, resume
def prepare_training_configs() -> tuple[dict, Path, Path, bool]:
# data_dir, out_dir は config ファイルでもコマンドライン引数でも指定でき、
# コマンドライン引数が優先される。
# 各種ファイルパスを相対パスで指定した場合、config ファイルでは
# リポジトリルートからの相対パスとなるが、コマンドライン引数では
# カレントディレクトリからの相対パスとなる。
parser = argparse.ArgumentParser()
# fmt: off
parser.add_argument("-d", "--data_dir", type=Path, help="directory containing the training data")
parser.add_argument("-o", "--out_dir", type=Path, help="output directory")
parser.add_argument("-r", "--resume", action="store_true", help="resume training")
parser.add_argument("-c", "--config", type=Path, help="path to the config file")
# fmt: on
args = parser.parse_args()
# config
if args.config is None:
h = deepcopy(dict_default_hparams)
else:
with open(args.config, encoding="utf-8") as f:
h = json.load(f)
for key in dict_default_hparams.keys():
if key not in h:
h[key] = dict_default_hparams[key]
warnings.warn(
f"{key} is not specified in the config file. Using the default value."
)
# data_dir
if args.data_dir is not None:
in_wav_dataset_dir = args.data_dir
elif "data_dir" in h:
in_wav_dataset_dir = repo_root() / Path(h["data_dir"])
del h["data_dir"]
else:
raise ValueError(
"data_dir must be specified. "
"For example `python3 beatrice_trainer -d my_training_data_dir -o my_output_dir`."
)
# out_dir
if args.out_dir is not None:
out_dir = args.out_dir
elif "out_dir" in h:
out_dir = repo_root() / Path(h["out_dir"])
del h["out_dir"]
else:
raise ValueError(
"out_dir must be specified. "
"For example `python3 beatrice_trainer -d my_training_data_dir -o my_output_dir`."
)
for key in list(h.keys()):
if key not in dict_default_hparams:
warnings.warn(f"`{key}` specified in the config file will be ignored.")
del h[key]
# resume
resume = args.resume
return h, in_wav_dataset_dir, out_dir, resume
class AttrDict(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__dict__ = self
# %% [markdown]
# ## Phone Extractor
# %%
def dump_params(params: torch.Tensor, f: BinaryIO):
if params is None:
return
if params.dtype == torch.bfloat16:
f.write(
params.detach()
.clone()
.float()
.view(torch.short)
.numpy()
.ravel()[1::2]
.tobytes()
)
else:
f.write(params.detach().numpy().ravel().tobytes())
f.flush()
def dump_layer(layer: nn.Module, f: BinaryIO):
dump = partial(dump_params, f=f)
if hasattr(layer, "dump"):
layer.dump(f)
elif isinstance(layer, (nn.Linear, nn.Conv1d, nn.LayerNorm)):
dump(layer.weight)
dump(layer.bias)
elif isinstance(layer, nn.ConvTranspose1d):
dump(layer.weight.transpose(0, 1))
dump(layer.bias)
elif isinstance(layer, nn.GRU):
dump(layer.weight_ih_l0)
dump(layer.bias_ih_l0)
dump(layer.weight_hh_l0)
dump(layer.bias_hh_l0)
for i in range(1, 99999):
if not hasattr(layer, f"weight_ih_l{i}"):
break
dump(getattr(layer, f"weight_ih_l{i}"))
dump(getattr(layer, f"bias_ih_l{i}"))
dump(getattr(layer, f"weight_hh_l{i}"))
dump(getattr(layer, f"bias_hh_l{i}"))
elif isinstance(layer, nn.Embedding):
dump(layer.weight)
elif isinstance(layer, nn.ModuleList):
for l in layer:
dump_layer(l, f)
else:
assert False, layer
class CausalConv1d(nn.Conv1d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int,
stride: int = 1,
dilation: int = 1,
groups: int = 1,
bias: bool = True,
delay: int = 0,
):
padding = (kernel_size - 1) * dilation - delay
self.trim = (kernel_size - 1) * dilation - 2 * delay
if self.trim < 0:
raise ValueError
super().__init__(
in_channels,
out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
)
def forward(self, input: torch.Tensor) -> torch.Tensor:
result = super().forward(input)
if self.trim == 0:
return result
else:
return result[:, :, : -self.trim]
class ConvNeXtBlock(nn.Module):
def __init__(
self,
channels: int,
intermediate_channels: int,
layer_scale_init_value: float,
kernel_size: int = 7,
use_weight_norm: bool = False,
):
super().__init__()
self.use_weight_norm = use_weight_norm
self.dwconv = CausalConv1d(
channels, channels, kernel_size=kernel_size, groups=channels
)
self.norm = nn.LayerNorm(channels)
self.pwconv1 = nn.Linear(channels, intermediate_channels)
self.pwconv2 = nn.Linear(intermediate_channels, channels)
self.gamma = nn.Parameter(torch.full((channels,), layer_scale_init_value))
if use_weight_norm:
self.norm = nn.Identity()
self.dwconv = weight_norm(self.dwconv)
self.pwconv1 = weight_norm(self.pwconv1)
self.pwconv2 = weight_norm(self.pwconv2)
def forward(self, x: torch.Tensor) -> torch.Tensor:
identity = x
x = self.dwconv(x)
x = x.transpose(1, 2)
x = self.norm(x)
x = self.pwconv1(x)
x = F.gelu(x, approximate="tanh")
x = self.pwconv2(x)
x *= self.gamma
x = x.transpose(1, 2)
x += identity
return x
def remove_weight_norm(self):
if self.use_weight_norm:
remove_weight_norm(self.dwconv)
remove_weight_norm(self.pwconv1)
remove_weight_norm(self.pwconv2)
def merge_weights(self):
if not self.use_weight_norm:
self.pwconv1.bias.data += (
self.norm.bias.data[None, :] * self.pwconv1.weight.data
).sum(1)
self.pwconv1.weight.data *= self.norm.weight.data[None, :]
self.norm.bias.data[:] = 0.0
self.norm.weight.data[:] = 1.0
self.pwconv2.weight.data *= self.gamma.data[:, None]
self.pwconv2.bias.data *= self.gamma.data
self.gamma.data[:] = 1.0
def dump(self, f: Union[BinaryIO, str, bytes, os.PathLike]):
if isinstance(f, (str, bytes, os.PathLike)):
with open(f, "wb") as f:
self.dump(f)
return
if not hasattr(f, "write"):
raise TypeError
dump_layer(self.dwconv, f)
dump_layer(self.pwconv1, f)
dump_layer(self.pwconv2, f)
class ConvNeXtStack(nn.Module):
def __init__(
self,
in_channels: int,
channels: int,
intermediate_channels: int,
n_blocks: int,
delay: int,
embed_kernel_size: int,
kernel_size: int,
use_weight_norm: bool = False,
):
super().__init__()
assert delay * 2 + 1 <= embed_kernel_size
self.use_weight_norm = use_weight_norm
self.embed = CausalConv1d(in_channels, channels, embed_kernel_size, delay=delay)
self.norm = nn.LayerNorm(channels)
self.convnext = nn.ModuleList(
[
ConvNeXtBlock(
channels=channels,
intermediate_channels=intermediate_channels,
layer_scale_init_value=1.0 / n_blocks,
kernel_size=kernel_size,
use_weight_norm=use_weight_norm,
)
for _ in range(n_blocks)
]
)
self.final_layer_norm = nn.LayerNorm(channels)
if use_weight_norm:
self.embed = weight_norm(self.embed)
self.norm = nn.Identity()
self.final_layer_norm = nn.Identity()
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, (nn.Conv1d, nn.Linear)):
nn.init.trunc_normal_(m.weight, std=0.02)
nn.init.constant_(m.bias, 0)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.embed(x)
x = self.norm(x.transpose(1, 2)).transpose(1, 2)
for conv_block in self.convnext:
x = conv_block(x)
x = self.final_layer_norm(x.transpose(1, 2)).transpose(1, 2)
return x
def remove_weight_norm(self):
if self.use_weight_norm:
remove_weight_norm(self.embed)
for conv_block in self.convnext:
conv_block.remove_weight_norm()
def merge_weights(self):
for conv_block in self.convnext:
conv_block.merge_weights()
def dump(self, f: Union[BinaryIO, str, bytes, os.PathLike]):
if isinstance(f, (str, bytes, os.PathLike)):
with open(f, "wb") as f:
self.dump(f)
return
if not hasattr(f, "write"):
raise TypeError
dump_layer(self.embed, f)
if not self.use_weight_norm:
dump_layer(self.norm, f)
dump_layer(self.convnext, f)
if not self.use_weight_norm:
dump_layer(self.final_layer_norm, f)
class FeatureExtractor(nn.Module):
def __init__(self, hidden_channels: int):
super().__init__()
# fmt: off
self.conv0 = weight_norm(nn.Conv1d(1, hidden_channels // 8, 10, 5, bias=False))
self.conv1 = weight_norm(nn.Conv1d(hidden_channels // 8, hidden_channels // 4, 3, 2, bias=False))
self.conv2 = weight_norm(nn.Conv1d(hidden_channels // 4, hidden_channels // 2, 3, 2, bias=False))
self.conv3 = weight_norm(nn.Conv1d(hidden_channels // 2, hidden_channels, 3, 2, bias=False))
self.conv4 = weight_norm(nn.Conv1d(hidden_channels, hidden_channels, 3, 2, bias=False))
self.conv5 = weight_norm(nn.Conv1d(hidden_channels, hidden_channels, 2, 2, bias=False))
# fmt: on
def forward(self, x: torch.Tensor) -> torch.Tensor:
# x: [batch_size, 1, wav_length]
wav_length = x.size(2)
if wav_length % 160 != 0:
warnings.warn("wav_length % 160 != 0")
x = F.pad(x, (40, 40))
x = F.gelu(self.conv0(x), approximate="tanh")
x = F.gelu(self.conv1(x), approximate="tanh")
x = F.gelu(self.conv2(x), approximate="tanh")
x = F.gelu(self.conv3(x), approximate="tanh")
x = F.gelu(self.conv4(x), approximate="tanh")
x = F.gelu(self.conv5(x), approximate="tanh")
# [batch_size, hidden_channels, wav_length / 160]
return x
def remove_weight_norm(self):
remove_weight_norm(self.conv0)
remove_weight_norm(self.conv1)
remove_weight_norm(self.conv2)
remove_weight_norm(self.conv3)
remove_weight_norm(self.conv4)
remove_weight_norm(self.conv5)
def dump(self, f: Union[BinaryIO, str, bytes, os.PathLike]):
if isinstance(f, (str, bytes, os.PathLike)):
with open(f, "wb") as f:
self.dump(f)
return
if not hasattr(f, "write"):
raise TypeError
dump_layer(self.conv0, f)
dump_layer(self.conv1, f)
dump_layer(self.conv2, f)
dump_layer(self.conv3, f)
dump_layer(self.conv4, f)
dump_layer(self.conv5, f)
class FeatureProjection(nn.Module):
def __init__(self, in_channels: int, out_channels: int):
super().__init__()
self.norm = nn.LayerNorm(in_channels)
self.projection = nn.Conv1d(in_channels, out_channels, 1)
self.dropout = nn.Dropout(0.1)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# [batch_size, channels, length]
x = self.norm(x.transpose(1, 2)).transpose(1, 2)
x = self.projection(x)
x = self.dropout(x)
return x
def merge_weights(self):
self.projection.bias.data += (
(self.norm.bias.data[None, :, None] * self.projection.weight.data)
.sum(1)
.squeeze(1)
)
self.projection.weight.data *= self.norm.weight.data[None, :, None]
self.norm.bias.data[:] = 0.0
self.norm.weight.data[:] = 1.0
def dump(self, f: Union[BinaryIO, str, bytes, os.PathLike]):
if isinstance(f, (str, bytes, os.PathLike)):
with open(f, "wb") as f:
self.dump(f)
return
if not hasattr(f, "write"):
raise TypeError
dump_layer(self.projection, f)
class PhoneExtractor(nn.Module):
def __init__(
self,
phone_channels: int = 256,
hidden_channels: int = 256,
backbone_embed_kernel_size: int = 7,
kernel_size: int = 17,
n_blocks: int = 8,
):
super().__init__()
self.feature_extractor = FeatureExtractor(hidden_channels)
self.feature_projection = FeatureProjection(hidden_channels, hidden_channels)
self.n_speaker_encoder_layers = 3
self.speaker_encoder = nn.GRU(
hidden_channels,
hidden_channels,
self.n_speaker_encoder_layers,
batch_first=True,
)
for i in range(self.n_speaker_encoder_layers):
for input_char in "ih":
self.speaker_encoder = weight_norm(
self.speaker_encoder, f"weight_{input_char}h_l{i}"
)
self.backbone = ConvNeXtStack(
in_channels=hidden_channels,
channels=hidden_channels,
intermediate_channels=hidden_channels * 3,
n_blocks=n_blocks,
delay=0,
embed_kernel_size=backbone_embed_kernel_size,
kernel_size=kernel_size,
)
self.head = weight_norm(nn.Conv1d(hidden_channels, phone_channels, 1))
def forward(
self, x: torch.Tensor, return_stats: bool = True
) -> Union[torch.Tensor, tuple[torch.Tensor, dict[str, float]]]:
# x: [batch_size, 1, wav_length]
stats = {}
# [batch_size, 1, wav_length] -> [batch_size, feature_extractor_hidden_channels, length]
x = self.feature_extractor(x)
if return_stats:
stats["feature_norm"] = x.detach().norm(dim=1).mean()
# [batch_size, feature_extractor_hidden_channels, length] -> [batch_size, hidden_channels, length]
x = self.feature_projection(x)
# [batch_size, hidden_channels, length] -> [batch_size, length, hidden_channels]
g, _ = self.speaker_encoder(x.transpose(1, 2))
if self.training:
batch_size, length, _ = g.size()
shuffle_sizes_for_each_data = torch.randint(
0, 50, (batch_size,), device=g.device
)
max_indices = torch.arange(length, device=g.device)[None, :, None]
min_indices = (
max_indices - shuffle_sizes_for_each_data[:, None, None]
).clamp_(min=0)
with torch.cuda.amp.autocast(False):
indices = (
torch.rand(g.size(), device=g.device)
* (max_indices - min_indices + 1)
).long() + min_indices
assert indices.min() >= 0, indices.min()
assert indices.max() < length, (indices.max(), length)
g = g.gather(1, indices)
# [batch_size, length, hidden_channels] -> [batch_size, hidden_channels, length]
g = g.transpose(1, 2).contiguous()
# [batch_size, hidden_channels, length]
x = self.backbone(x + g)
# [batch_size, hidden_channels, length] -> [batch_size, phone_channels, length]
phone = self.head(F.gelu(x, approximate="tanh"))
results = [phone]
if return_stats:
stats["code_norm"] = phone.detach().norm(dim=1).mean().item()
results.append(stats)
if len(results) == 1:
return results[0]
return tuple(results)
@torch.inference_mode()
def units(self, x: torch.Tensor) -> torch.Tensor:
# x: [batch_size, 1, wav_length]
# [batch_size, 1, wav_length] -> [batch_size, phone_channels, length]
phone = self.forward(x, return_stats=False)
# [batch_size, phone_channels, length] -> [batch_size, length, phone_channels]
phone = phone.transpose(1, 2)
# [batch_size, length, phone_channels]
return phone
def remove_weight_norm(self):
self.feature_extractor.remove_weight_norm()
for i in range(self.n_speaker_encoder_layers):
for input_char in "ih":
remove_weight_norm(self.speaker_encoder, f"weight_{input_char}h_l{i}")
remove_weight_norm(self.head)
def merge_weights(self):
self.feature_projection.merge_weights()
self.backbone.merge_weights()
def dump(self, f: Union[BinaryIO, str, bytes, os.PathLike]):
if isinstance(f, (str, bytes, os.PathLike)):
with open(f, "wb") as f:
self.dump(f)
return
if not hasattr(f, "write"):
raise TypeError
dump_layer(self.feature_extractor, f)
dump_layer(self.feature_projection, f)
dump_layer(self.speaker_encoder, f)
dump_layer(self.backbone, f)
dump_layer(self.head, f)
# %% [markdown]
# ## Pitch Estimator
# %%
def extract_pitch_features(
y: torch.Tensor, # [..., wav_length]
hop_length: int = 160, # 10ms
win_length: int = 560, # 35ms
max_corr_period: int = 256, # 16ms, 62.5Hz (16000 / 256)
corr_win_length: int = 304, # 19ms
instfreq_features_cutoff_bin: int = 64, # 1828Hz (16000 * 64 / 560)
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
assert max_corr_period + corr_win_length == win_length
# パディングする
padding_length = (win_length - hop_length) // 2
y = F.pad(y, (padding_length, padding_length))
# フレームにする
# [..., win_length, n_frames]
y_frames = y.unfold(-1, win_length, hop_length).transpose_(-2, -1)
# 複素スペクトログラム
# Complex[..., (win_length // 2 + 1), n_frames]
spec: torch.Tensor = torch.fft.rfft(y_frames, n=win_length, dim=-2)
# Complex[..., instfreq_features_cutoff_bin, n_frames]
spec = spec[..., :instfreq_features_cutoff_bin, :]
# 対数パワースペクトログラム
log_power_spec = spec.abs().add_(1e-5).log10_()
# 瞬時位相の時間差分
# 時刻 0 の値は 0
delta_spec = spec[..., :, 1:] * spec[..., :, :-1].conj()
delta_spec /= delta_spec.abs().add_(1e-5)
delta_spec = torch.cat(
[torch.zeros_like(delta_spec[..., :, :1]), delta_spec], dim=-1
)
# [..., instfreq_features_cutoff_bin * 3, n_frames]
instfreq_features = torch.cat(
[log_power_spec, delta_spec.real, delta_spec.imag], dim=-2
)
# 自己相関
# 余裕があったら LPC 残差にするのも試したい
# 元々これに 2.0 / corr_win_length を掛けて使おうと思っていたが、
# この値は振幅の 2 乗に比例していて、NN に入力するために良い感じに分散を
# 標準化する方法が思いつかなかったのでやめた
flipped_y_frames = y_frames.flip((-2,))
a = torch.fft.rfft(flipped_y_frames, n=win_length, dim=-2)
b = torch.fft.rfft(y_frames[..., -corr_win_length:, :], n=win_length, dim=-2)
# [..., max_corr_period, n_frames]
corr = torch.fft.irfft(a * b, n=win_length, dim=-2)[..., corr_win_length:, :]
# エネルギー項
energy = flipped_y_frames.square_().cumsum_(-2)
energy0 = energy[..., corr_win_length - 1 : corr_win_length, :]
energy = energy[..., corr_win_length:, :] - energy[..., :-corr_win_length, :]
# Difference function
corr_diff = (energy0 + energy).sub_(corr.mul_(2.0))
assert corr_diff.min() >= -1e-3, corr_diff.min()
corr_diff.clamp_(min=0.0) # 計算誤差対策
# 標準化
corr_diff *= 2.0 / corr_win_length
corr_diff.sqrt_()
# 変換モデルへの入力用のエネルギー
energy = (
y_frames.mul_(
torch.signal.windows.cosine(win_length, device=y.device)[..., None]
)
.square_()
.sum(-2, keepdim=True)
)
energy.clamp_(min=1e-3).log10_() # >= -3, 振幅 1 の正弦波なら大体 2.15
energy *= 0.5 # >= -1.5, 振幅 1 の正弦波なら大体 1.07, 1 の差は振幅で 20dB の差
return (
instfreq_features, # [..., instfreq_features_cutoff_bin * 3, n_frames]
corr_diff, # [..., max_corr_period, n_frames]
energy, # [..., 1, n_frames]
)
class PitchEstimator(nn.Module):
def __init__(
self,
input_instfreq_channels: int = 192,
input_corr_channels: int = 256,
pitch_channels: int = 384,
channels: int = 192,
intermediate_channels: int = 192 * 3,
n_blocks: int = 6,
delay: int = 1, # 10ms, 特徴抽出と合わせると 22.5ms
embed_kernel_size: int = 3,
kernel_size: int = 33,
bins_per_octave: int = 96,
):
super().__init__()
self.bins_per_octave = bins_per_octave
self.instfreq_embed_0 = nn.Conv1d(input_instfreq_channels, channels, 1)
self.instfreq_embed_1 = nn.Conv1d(channels, channels, 1)
self.corr_embed_0 = nn.Conv1d(input_corr_channels, channels, 1)
self.corr_embed_1 = nn.Conv1d(channels, channels, 1)
self.backbone = ConvNeXtStack(
channels,
channels,
intermediate_channels,
n_blocks,
delay,
embed_kernel_size,
kernel_size,
)
self.head = nn.Conv1d(channels, pitch_channels, 1)
def forward(self, wav: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
# wav: [batch_size, 1, wav_length]
# [batch_size, input_instfreq_channels, length],
# [batch_size, input_corr_channels, length]
with torch.cuda.amp.autocast(False):
instfreq_features, corr_diff, energy = extract_pitch_features(
wav.squeeze(1),
hop_length=160,
win_length=560,
max_corr_period=256,
corr_win_length=304,
instfreq_features_cutoff_bin=64,
)
instfreq_features = F.gelu(
self.instfreq_embed_0(instfreq_features), approximate="tanh"
)
instfreq_features = self.instfreq_embed_1(instfreq_features)
corr_diff = F.gelu(self.corr_embed_0(corr_diff), approximate="tanh")
corr_diff = self.corr_embed_1(corr_diff)
# [batch_size, channels, length]
x = instfreq_features + corr_diff # ここ活性化関数忘れてる
x = self.backbone(x)
# [batch_size, pitch_channels, length]
x = self.head(x)
return x, energy
def sample_pitch(
self, pitch: torch.Tensor, band_width: int = 48, return_features: bool = False
) -> Union[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
# pitch: [batch_size, pitch_channels, length]
# 返されるピッチの値には 0 は含まれない
batch_size, pitch_channels, length = pitch.size()
pitch = pitch.softmax(1)
if return_features:
unvoiced_proba = pitch[:, :1, :].clone()
pitch[:, 0, :] = -100.0
pitch = (
pitch.transpose(1, 2)
.contiguous()
.view(batch_size * length, 1, pitch_channels)
)
band_pitch = F.conv1d(
pitch,
torch.ones((1, 1, 1), device=pitch.device).expand(1, 1, band_width),
)
# [batch_size * length, 1, pitch_channels - band_width + 1] -> Long[batch_size * length, 1]
quantized_band_pitch = band_pitch.argmax(2)
if return_features:
# [batch_size * length, 1]
band_proba = band_pitch.gather(2, quantized_band_pitch[:, :, None])
# [batch_size * length, 1]
half_pitch_band_proba = band_pitch.gather(
2,
(quantized_band_pitch - self.bins_per_octave).clamp_(min=1)[:, :, None],
)
half_pitch_band_proba[quantized_band_pitch <= self.bins_per_octave] = 0.0
half_pitch_proba = (half_pitch_band_proba / (band_proba + 1e-6)).view(
batch_size, 1, length
)
# [batch_size * length, 1]
double_pitch_band_proba = band_pitch.gather(
2,
(quantized_band_pitch + self.bins_per_octave).clamp_(
max=pitch_channels - band_width
)[:, :, None],
)
double_pitch_band_proba[
quantized_band_pitch
> pitch_channels - band_width - self.bins_per_octave
] = 0.0
double_pitch_proba = (double_pitch_band_proba / (band_proba + 1e-6)).view(
batch_size, 1, length
)
# Long[1, pitch_channels]
mask = torch.arange(pitch_channels, device=pitch.device)[None, :]
# bool[batch_size * length, pitch_channels]
mask = (quantized_band_pitch <= mask) & (
mask < quantized_band_pitch + band_width
)
# Long[batch_size, length]
quantized_pitch = (pitch.squeeze(1) * mask).argmax(1).view(batch_size, length)
if return_features:
features = torch.cat(
[unvoiced_proba, half_pitch_proba, double_pitch_proba], dim=1
)
# Long[batch_size, length], [batch_size, 3, length]
return quantized_pitch, features
else:
return quantized_pitch
def merge_weights(self):
self.backbone.merge_weights()
def dump(self, f: Union[BinaryIO, str, bytes, os.PathLike]):
if isinstance(f, (str, bytes, os.PathLike)):
with open(f, "wb") as f:
self.dump(f)
return
if not hasattr(f, "write"):
raise TypeError
dump_layer(self.instfreq_embed_0, f)
dump_layer(self.instfreq_embed_1, f)
dump_layer(self.corr_embed_0, f)
dump_layer(self.corr_embed_1, f)
dump_layer(self.backbone, f)
dump_layer(self.head, f)
# %% [markdown]
# ## Vocoder
# %%
def overlap_add(
ir: torch.Tensor,
pitch: torch.Tensor,
hop_length: int = 240,
delay: int = 0,
) -> torch.Tensor:
# print("ir, pitch: ", ir.dtype, pitch.dtype)
batch_size, ir_length, length = ir.size()
assert pitch.size() == (batch_size, length * hop_length)
assert 0 <= delay < ir_length, (delay, ir_length)
# 位相は [0, 1) で表す
normalized_freq = pitch / 24000.0
# 初期位相をランダムに設定
normalized_freq[:, 0] = torch.rand(batch_size, device=pitch.device)
with torch.cuda.amp.autocast(enabled=False):
phase = (normalized_freq.double().cumsum_(1) % 1.0).float()
# 重ねる箇所を求める
# [n_pitchmarks], [n_pitchmarks]
indices0, indices1 = torch.nonzero(phase[:, :-1] > phase[:, 1:], as_tuple=True)
# 重ねる箇所の小数部分 (位相の遅れ) を求める
numer = 1.0 - phase[indices0, indices1]
# [n_pitchmarks]
fractional_part = numer / (numer + phase[indices0, indices1 + 1])
# 重ねる値を求める
# [n_pitchmarks, ir_length]
values = ir[indices0, :, indices1 // hop_length]
# 位相を遅らせる
# values が時間領域と仮定
# Complex[n_pitchmarks, ir_length / 2 + 1]
values = torch.fft.rfft(values, n=ir_length, dim=1)
# 位相遅れの量
# [n_pitchmarks, ir_length / 2 + 1]
delay_phase = (
torch.arange(ir_length // 2 + 1, device=pitch.device, dtype=torch.float32)[
None, :
]
/ -ir_length
* fractional_part[:, None]
)
# Complex[n_pitchmarks, ir_length / 2 + 1]
delay_phase = torch.polar(torch.ones_like(delay_phase), delay_phase * math.tau)
# values *= delay_phase
values = values * delay_phase
# [n_pitchmarks, ir_length]
values = torch.fft.irfft(values, n=ir_length, dim=1)
# 加算する値をサンプル単位にばらす
# [n_pitchmarks * ir_length]
values = values.ravel()
# Long[n_pitchmarks * ir_length]
indices0 = indices0[:, None].expand(-1, ir_length).ravel()
# Long[n_pitchmarks * ir_length]
indices1 = (
indices1[:, None] + torch.arange(ir_length, device=pitch.device)
).ravel()
# overlap-add する
overlap_added_signal = torch.zeros(
(batch_size, length * hop_length + ir_length), device=pitch.device
)
# print("overlap_added_signal, values: ", overlap_added_signal.dtype, values.dtype)
overlap_added_signal.index_put_((indices0, indices1), values, accumulate=True)
overlap_added_signal = overlap_added_signal[:, delay : -ir_length + delay]
# sinc 重ねたものと ir を畳み込んだ方が FFT の回数減らせた気がする
return overlap_added_signal
def generate_noise(aperiodicity: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
# aperiodicity: [batch_size, hop_length, length]
batch_size, hop_length, length = aperiodicity.size()
excitation = torch.rand(
batch_size, (length + 1) * hop_length, device=aperiodicity.device
)
excitation -= 0.5
n_fft = 2 * hop_length
# 矩形窓で分析
# Complex[batch_size, hop_length + 1, length]
noise = torch.stft(
excitation,
n_fft=n_fft,
hop_length=hop_length,
window=torch.ones(n_fft, device=excitation.device),
center=False,
return_complex=True,
)
assert noise.size(2) == aperiodicity.size(2), (
noise.size(),
aperiodicity.size(),
)
noise[:, 0, :] = 0.0
noise[:, 1:, :] *= aperiodicity
# ハン窓で合成
# torch.istft は最適合成窓が使われるので使えないことに注意
# [batch_size, 2 * hop_length, length]
noise = torch.fft.irfft(noise, n=2 * hop_length, dim=1)
noise *= torch.hann_window(2 * hop_length, device=noise.device)[None, :, None]
# [batch_size, (length + 1) * hop_length]
noise = F.fold(
noise,
(1, (length + 1) * hop_length),
(1, 2 * hop_length),
stride=(1, hop_length),
).squeeze_((1, 2))
noise = noise[:, hop_length // 2 : -hop_length // 2]
excitation = excitation[:, hop_length // 2 : -hop_length // 2]
return noise, excitation # [batch_size, length * hop_length]
class GradientEqualizerFunction(torch.autograd.Function):
"""ノルムが小さいほど勾配が大きくなってしまうのを補正する"""
@staticmethod
def forward(ctx, x: torch.Tensor) -> torch.Tensor:
# x: [batch_size, 1, length]
rms = x.square().mean(dim=2, keepdim=True).sqrt_()
ctx.save_for_backward(rms)
return x
@staticmethod
def backward(ctx, dx: torch.Tensor) -> torch.Tensor:
# dx: [batch_size, 1, length]
(rms,) = ctx.saved_tensors
dx = dx * (math.sqrt(2.0) * rms + 0.1)
return dx
class PseudoDDSPVocoder(nn.Module):
def __init__(
self,
channels: int,
hop_length: int = 240,
n_pre_blocks: int = 4,
):
super().__init__()
self.hop_length = hop_length
self.prenet = ConvNeXtStack(
in_channels=channels,
channels=channels,
intermediate_channels=channels * 3,
n_blocks=n_pre_blocks,
delay=2, # 20ms 遅延
embed_kernel_size=7,
kernel_size=33,
)
self.ir_generator = ConvNeXtStack(
in_channels=channels,
channels=channels,
intermediate_channels=channels * 3,
n_blocks=2,
delay=0,
embed_kernel_size=3,
kernel_size=33,
use_weight_norm=True,
)
self.ir_generator_post = weight_norm(nn.Conv1d(channels, 512, 1, bias=False))
self.aperiodicity_generator = ConvNeXtStack(
in_channels=channels,
channels=channels,
intermediate_channels=channels * 3,
n_blocks=2,
delay=0,
embed_kernel_size=3,
kernel_size=33,
use_weight_norm=True,
)
self.aperiodicity_generator_post = weight_norm(
nn.Conv1d(channels, hop_length, 1, bias=False)
)
def forward(
self, x: torch.Tensor, pitch: torch.Tensor
) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
# x: [batch_size, channels, length]
# pitch: [batch_size, length]
x = self.prenet(x)
ir = self.ir_generator(x)
ir = F.elu(ir, inplace=True)
# [batch_size, 512, length]
ir = self.ir_generator_post(ir)
# 最近傍補間
# [batch_size, length * hop_length]
pitch = torch.repeat_interleave(pitch, self.hop_length, dim=1)
# [batch_size, length * hop_length]
periodic_signal = overlap_add(ir, pitch, self.hop_length, delay=120)
aperiodicity = self.aperiodicity_generator(x)
aperiodicity = F.elu(aperiodicity, inplace=True)
# [batch_size, hop_length, length]
aperiodicity = self.aperiodicity_generator_post(aperiodicity)
# [batch_size, length * hop_length], [batch_size, length * hop_length]
aperiodic_signal, noise_excitation = generate_noise(aperiodicity)
# [batch_size, 1, length * hop_length]
y_g_hat = (periodic_signal + aperiodic_signal)[:, None, :]
y_g_hat = GradientEqualizerFunction.apply(y_g_hat)
return y_g_hat, {
"periodic_signal": periodic_signal.detach(),
"aperiodic_signal": aperiodic_signal.detach(),
"noise_excitation": noise_excitation.detach(),
}
def remove_weight_norm(self):
self.prenet.remove_weight_norm()
self.ir_generator.remove_weight_norm()
remove_weight_norm(self.ir_generator_post)
self.aperiodicity_generator.remove_weight_norm()
remove_weight_norm(self.aperiodicity_generator_post)
def merge_weights(self):
self.prenet.merge_weights()
self.ir_generator.merge_weights()
self.aperiodicity_generator.merge_weights()
def dump(self, f: Union[BinaryIO, str, bytes, os.PathLike]):
if isinstance(f, (str, bytes, os.PathLike)):
with open(f, "wb") as f:
self.dump(f)
return
if not hasattr(f, "write"):
raise TypeError
dump_layer(self.prenet, f)
dump_layer(self.ir_generator, f)
dump_layer(self.ir_generator_post, f)
dump_layer(self.aperiodicity_generator, f)
dump_layer(self.aperiodicity_generator_post, f)
def slice_segments(
x: torch.Tensor, start_indices: torch.Tensor, segment_length: int
) -> torch.Tensor:
batch_size, channels, _ = x.size()
# [batch_size, 1, segment_size]
indices = start_indices[:, None, None] + torch.arange(
segment_length, device=start_indices.device
)
# [batch_size, channels, segment_size]
indices = indices.expand(batch_size, channels, segment_length)
return x.gather(2, indices)
class ConverterNetwork(nn.Module):
def __init__(
self,
phone_extractor: PhoneExtractor,
pitch_estimator: PitchEstimator,
n_speakers: int,
hidden_channels: int,
):
super().__init__()
self.frozen_modules = {
"phone_extractor": phone_extractor.eval().requires_grad_(False),
"pitch_estimator": pitch_estimator.eval().requires_grad_(False),
}
self.embed_phone = nn.Conv1d(256, hidden_channels, 1)
self.embed_quantized_pitch = nn.Embedding(384, hidden_channels)
phase = (
torch.arange(384, dtype=torch.float)[:, None]
* (
torch.arange(0, hidden_channels, 2, dtype=torch.float)
* (-math.log(10000.0) / hidden_channels)
).exp_()
)
self.embed_quantized_pitch.weight.data[:, 0::2] = phase.sin()
self.embed_quantized_pitch.weight.data[:, 1::2] = phase.cos_()
self.embed_quantized_pitch.weight.requires_grad_(False)
self.embed_pitch_features = nn.Conv1d(4, hidden_channels, 1)
self.embed_speaker = nn.Embedding(n_speakers, hidden_channels)
self.embed_formant_shift = nn.Embedding(9, hidden_channels)
self.vocoder = PseudoDDSPVocoder(
channels=hidden_channels,
hop_length=240,
n_pre_blocks=4,
)
self.melspectrogram = torchaudio.transforms.MelSpectrogram(
sample_rate=24000,
n_fft=1024,
win_length=720,
hop_length=128,
n_mels=80,
power=2, # 不安定さの原因になっているかも
norm="slaney",
mel_scale="slaney",
)
def _get_resampler(
self, orig_freq, new_freq, device, cache={}
) -> torchaudio.transforms.Resample:
key = orig_freq, new_freq
if key in cache:
return cache[key]
resampler = torchaudio.transforms.Resample(orig_freq, new_freq).to(device)
cache[key] = resampler
return resampler
def forward(
self,
x: torch.Tensor,
target_speaker_id: torch.Tensor,
formant_shift_semitone: torch.Tensor,
pitch_shift_semitone: Optional[torch.Tensor] = None,
slice_start_indices: Optional[torch.Tensor] = None,
slice_segment_length: Optional[int] = None,
return_stats: bool = False,
) -> Union[torch.Tensor, tuple[torch.Tensor, dict[str, float]]]:
# x: [batch_size, 1, wav_length]
# target_speaker_id: Long[batch_size]
# formant_shift_semitone: [batch_size]
# pitch_shift_semitone: [batch_size]
# slice_start_indices: [batch_size]
batch_size, _, _ = x.size()
with torch.inference_mode():
phone_extractor: PhoneExtractor = self.frozen_modules["phone_extractor"]
pitch_estimator: PitchEstimator = self.frozen_modules["pitch_estimator"]
# [batch_size, 1, wav_length] -> [batch_size, phone_channels, length]
phone = phone_extractor.units(x).transpose(1, 2)
# [batch_size, 1, wav_length] -> [batch_size, pitch_channels, length], [batch_size, 1, length]
pitch, energy = pitch_estimator(x)
# augmentation
if self.training:
# [batch_size, pitch_channels - 1]
weights = pitch.softmax(1)[:, 1:, :].mean(2)
# [batch_size]
mean_pitch = (
weights * torch.arange(1, 384, device=weights.device)
).sum(1) / weights.sum(1)
mean_pitch = mean_pitch.round_().long()
target_pitch = torch.randint_like(mean_pitch, 64, 257)
shift = target_pitch - mean_pitch
shift_ratio = (
2.0 ** (shift.float() / pitch_estimator.bins_per_octave)
).tolist()
shift = []
interval_length = 100 # 1s
interval_zeros = torch.zeros(
(1, 1, interval_length * 160), device=x.device
)
concatenated_shifted_x = []
offsets = [0]
for i in range(batch_size):
shift_ratio_i = shift_ratio[i]
shift_ratio_fraction_i = Fraction.from_float(
shift_ratio_i
).limit_denominator(30)
shift_numer_i = shift_ratio_fraction_i.numerator
shift_denom_i = shift_ratio_fraction_i.denominator
shift_ratio_i = shift_numer_i / shift_denom_i
shift_i = int(
round(
math.log2(shift_ratio_i) * pitch_estimator.bins_per_octave
)
)
shift.append(shift_i)
shift_ratio[i] = shift_ratio_i
# [1, 1, wav_length / shift_ratio]
with torch.cuda.amp.autocast(False):
shifted_x_i = self._get_resampler(
shift_numer_i, shift_denom_i, x.device
)(x[i])[None]
if shifted_x_i.size(2) % 160 != 0:
shifted_x_i = F.pad(
shifted_x_i,
(0, 160 - shifted_x_i.size(2) % 160),
mode="reflect",
)
assert shifted_x_i.size(2) % 160 == 0
offsets.append(
offsets[-1] + interval_length + shifted_x_i.size(2) // 160
)
concatenated_shifted_x.extend([interval_zeros, shifted_x_i])
if offsets[-1] % 256 != 0:
# 長さが同じ方が何かのキャッシュが効いて早くなるようなので
# 適当に 256 の倍数になるようにパディングして長さのパターン数を減らす
concatenated_shifted_x.append(
torch.zeros(
(1, 1, (256 - offsets[-1] % 256) * 160), device=x.device
)
)
# [batch_size, 1, sum(wav_length) + batch_size * 16000]
concatenated_shifted_x = torch.cat(concatenated_shifted_x, dim=2)
assert concatenated_shifted_x.size(2) % (256 * 160) == 0
# [1, pitch_channels, length / shift_ratio], [1, 1, length / shift_ratio]
concatenated_pitch, concatenated_energy = pitch_estimator(
concatenated_shifted_x
)
for i in range(batch_size):
shift_i = shift[i]
shift_ratio_i = shift_ratio[i]
left = offsets[i] + interval_length
right = offsets[i + 1]
pitch_i = concatenated_pitch[:, :, left:right]
energy_i = concatenated_energy[:, :, left:right]
pitch_i = F.interpolate(
pitch_i,
scale_factor=shift_ratio_i,
mode="linear",
align_corners=False,
)
energy_i = F.interpolate(
energy_i,
scale_factor=shift_ratio_i,
mode="linear",
align_corners=False,
)
assert pitch_i.size(2) == energy_i.size(2)
assert abs(pitch_i.size(2) - pitch.size(2)) <= 10
length = min(pitch_i.size(2), pitch.size(2))
if shift_i > 0:
pitch[i : i + 1, :1, :length] = pitch_i[:, :1, :length]
pitch[i : i + 1, 1:-shift_i, :length] = pitch_i[
:, 1 + shift_i :, :length
]
pitch[i : i + 1, -shift_i:, :length] = -10.0
elif shift_i < 0:
pitch[i : i + 1, :1, :length] = pitch_i[:, :1, :length]
pitch[i : i + 1, 1 : 1 - shift_i, :length] = -10.0
pitch[i : i + 1, 1 - shift_i :, :length] = pitch_i[
:, 1:shift_i, :length
]
energy[i : i + 1, :, :length] = energy_i[:, :, :length]
# [batch_size, pitch_channels, length] -> Long[batch_size, length], [batch_size, 3, length]
quantized_pitch, pitch_features = pitch_estimator.sample_pitch(
pitch, return_features=True
)
if pitch_shift_semitone is not None:
quantized_pitch = torch.where(
quantized_pitch == 0,
quantized_pitch,
(
quantized_pitch
+ (
pitch_shift_semitone[:, None]
* (pitch_estimator.bins_per_octave / 12)
)
.round_()
.long()
).clamp_(1, 383),
)
pitch = 55.0 * 2.0 ** (
quantized_pitch.float() / pitch_estimator.bins_per_octave
)
# phone が 2.5ms 先読みしているのに対して、
# energy は 12.5ms, pitch_features は 22.5ms 先読みしているので、
# ずらして phone に合わせる
energy = F.pad(energy[:, :, :-1], (1, 0), mode="reflect")
quantized_pitch = F.pad(quantized_pitch[:, :-2], (2, 0), mode="reflect")
pitch_features = F.pad(pitch_features[:, :, :-2], (2, 0), mode="reflect")
# [batch_size, 1, length], [batch_size, 3, length] -> [batch_size, 4, length]
pitch_features = torch.cat([energy, pitch_features], dim=1)
formant_shift_indices = (
((formant_shift_semitone + 2.0) * 2.0).round_().long()
)
phone = phone.clone()
quantized_pitch = quantized_pitch.clone()
pitch_features = pitch_features.clone()
formant_shift_indices = formant_shift_indices.clone()
pitch = pitch.clone()
# [batch_sise, hidden_channels, length]
x = (
self.embed_phone(phone)
+ self.embed_quantized_pitch(quantized_pitch).transpose(1, 2)
+ self.embed_pitch_features(pitch_features)
+ (
self.embed_speaker(target_speaker_id)[:, :, None]
+ self.embed_formant_shift(formant_shift_indices)[:, :, None]
)
)
if slice_start_indices is not None:
assert slice_segment_length is not None
# [batch_size, hidden_channels, length] -> [batch_size, hidden_channels, segment_length]
x = slice_segments(x, slice_start_indices, slice_segment_length)
x = F.silu(x, inplace=True)
# [batch_size, hidden_channels, segment_length] -> [batch_size, 1, segment_length * 240]
y_g_hat, stats = self.vocoder(x, pitch)
if return_stats:
return y_g_hat, stats
else:
return y_g_hat
def _normalize_melsp(self, x):
return x.log().mul(0.5).clamp_(min=math.log(1e-5))
def forward_and_compute_loss(
self,
noisy_wavs_16k: torch.Tensor,
target_speaker_id: torch.Tensor,
formant_shift_semitone: torch.Tensor,
slice_start_indices: torch.Tensor,
slice_segment_length: int,
y_all: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
# noisy_wavs_16k: [batch_size, 1, wav_length]
# target_speaker_id: Long[batch_size]
# formant_shift_semitone: [batch_size]
# slice_start_indices: [batch_size]
# slice_segment_length: int
# y_all: [batch_size, 1, wav_length]
# [batch_size, 1, wav_length] -> [batch_size, 1, wav_length * 240]
y_hat_all, stats = self(
noisy_wavs_16k,
target_speaker_id,
formant_shift_semitone,
return_stats=True,
)
with torch.cuda.amp.autocast(False):
melsp_periodic_signal = self.melspectrogram(
stats["periodic_signal"].float()
)
melsp_aperiodic_signal = self.melspectrogram(
stats["aperiodic_signal"].float()
)
melsp_noise_excitation = self.melspectrogram(
stats["noise_excitation"].float()
)
# [1, n_mels, 1]
# 1/6 ... [-0.5, 0.5] の一様乱数の平均パワー
# 3/8 ... ハン窓をかけた時のパワー減衰
# 0.5 ... 謎
reference_melsp = self.melspectrogram.mel_scale(
torch.full(
(1, self.melspectrogram.n_fft // 2 + 1, 1),
(1 / 6) * (3 / 8) * 0.5 * self.melspectrogram.win_length,
device=noisy_wavs_16k.device,
)
)
aperiodic_ratio = melsp_aperiodic_signal / (
melsp_periodic_signal + melsp_aperiodic_signal + 1e-5
)
compensation_ratio = reference_melsp / (melsp_noise_excitation + 1e-5)
melsp_y_hat = self.melspectrogram(y_hat_all.float().squeeze(1))
melsp_y_hat = melsp_y_hat * (
(1.0 - aperiodic_ratio) + aperiodic_ratio * compensation_ratio
)
y_hat_mel = self._normalize_melsp(melsp_y_hat)
# [batch_size, 1, wav_length] -> [batch_size, 1, wav_length * 240]
y_hat = slice_segments(
y_hat_all, slice_start_indices * 240, slice_segment_length * 240
)
y_mel = self._normalize_melsp(self.melspectrogram(y_all.squeeze(1)))
# [batch_size, 1, wav_length] -> [batch_size, 1, wav_length * 240]
y = slice_segments(
y_all, slice_start_indices * 240, slice_segment_length * 240
)
loss_mel = F.l1_loss(y_hat_mel, y_mel)
return y, y_hat, y_hat_all, loss_mel
def remove_weight_norm(self):
self.vocoder.remove_weight_norm()
def merge_weights(self):
self.vocoder.merge_weights()
def dump(self, f: Union[BinaryIO, str, bytes, os.PathLike]):
if isinstance(f, (str, bytes, os.PathLike)):
with open(f, "wb") as f:
self.dump(f)
return
if not hasattr(f, "write"):
raise TypeError
dump_layer(self.embed_phone, f)
dump_layer(self.embed_quantized_pitch, f)
dump_layer(self.embed_pitch_features, f)
dump_layer(self.vocoder, f)
# Discriminator
def _normalize(tensor: torch.Tensor, dim: int) -> torch.Tensor:
denom = tensor.norm(p=2.0, dim=dim, keepdim=True).clamp_min(1e-6)
return tensor / denom
class SANConv2d(nn.Conv2d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int,
stride: int = 1,
padding: int = 0,
dilation: int = 1,
bias: bool = True,
padding_mode="zeros",
device=None,
dtype=None,
):
super().__init__(
in_channels,
out_channels,
kernel_size,
stride,
padding=padding,
dilation=dilation,
groups=1,
bias=bias,
padding_mode=padding_mode,
device=device,
dtype=dtype,
)
scale = self.weight.norm(p=2.0, dim=[1, 2, 3], keepdim=True).clamp_min(1e-6)
self.weight = nn.parameter.Parameter(self.weight / scale.expand_as(self.weight))
self.scale = nn.parameter.Parameter(scale.view(out_channels))
if bias:
self.bias = nn.parameter.Parameter(
torch.zeros(in_channels, device=device, dtype=dtype)
)
else:
self.register_parameter("bias", None)
def forward(
self, input: torch.Tensor, flg_san_train: bool = False
) -> Union[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
if self.bias is not None:
input = input + self.bias.view(self.in_channels, 1, 1)
normalized_weight = self._get_normalized_weight()
scale = self.scale.view(self.out_channels, 1, 1)
if flg_san_train:
out_fun = F.conv2d(
input,
normalized_weight.detach(),
None,
self.stride,
self.padding,
self.dilation,
self.groups,
)
out_dir = F.conv2d(
input.detach(),
normalized_weight,
None,
self.stride,
self.padding,
self.dilation,
self.groups,
)
out = out_fun * scale, out_dir * scale.detach()
else:
out = F.conv2d(
input,
normalized_weight,
None,
self.stride,
self.padding,
self.dilation,
self.groups,
)
out = out * scale
return out
@torch.no_grad()
def normalize_weight(self):
self.weight.data = self._get_normalized_weight()
def _get_normalized_weight(self) -> torch.Tensor:
return _normalize(self.weight, dim=[1, 2, 3])
def get_padding(kernel_size: int, dilation: int = 1) -> int:
return (kernel_size * dilation - dilation) // 2
class DiscriminatorP(nn.Module):
def __init__(
self, period: int, kernel_size: int = 5, stride: int = 3, san: bool = False
):
super().__init__()
self.period = period
self.san = san
# fmt: off
self.convs = nn.ModuleList([
weight_norm(nn.Conv2d(1, 32, (kernel_size, 1), (stride, 1), (get_padding(kernel_size, 1), 0))),
weight_norm(nn.Conv2d(32, 128, (kernel_size, 1), (stride, 1), (get_padding(kernel_size, 1), 0))),
weight_norm(nn.Conv2d(128, 512, (kernel_size, 1), (stride, 1), (get_padding(kernel_size, 1), 0))),
weight_norm(nn.Conv2d(512, 1024, (kernel_size, 1), (stride, 1), (get_padding(kernel_size, 1), 0))),
weight_norm(nn.Conv2d(1024, 1024, (kernel_size, 1), 1, (get_padding(kernel_size, 1), 0))),
])
# fmt: on
if san:
self.conv_post = SANConv2d(1024, 1, (3, 1), 1, (1, 0))
else:
self.conv_post = weight_norm(nn.Conv2d(1024, 1, (3, 1), 1, (1, 0)))
def forward(
self, x: torch.Tensor, flg_san_train: bool = False
) -> tuple[
Union[torch.Tensor, tuple[torch.Tensor, torch.Tensor]], list[torch.Tensor]
]:
fmap = []
b, c, t = x.shape
if t % self.period != 0:
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.silu(x, inplace=True)
fmap.append(x)
if self.san:
x = self.conv_post(x, flg_san_train=flg_san_train)
else:
x = self.conv_post(x)
if flg_san_train:
x_fun, x_dir = x
fmap.append(x_fun)
x_fun = torch.flatten(x_fun, 1, -1)
x_dir = torch.flatten(x_dir, 1, -1)
x = x_fun, x_dir
else:
fmap.append(x)
x = torch.flatten(x, 1, -1)
return x, fmap
class DiscriminatorR(nn.Module):
def __init__(self, resolution: int, san: bool = False):
super().__init__()
self.resolution = resolution
self.san = san
assert len(self.resolution) == 3
self.convs = nn.ModuleList(
[
weight_norm(nn.Conv2d(1, 32, (3, 9), padding=(1, 4))),
weight_norm(nn.Conv2d(32, 32, (3, 9), (1, 2), (1, 4))),
weight_norm(nn.Conv2d(32, 32, (3, 9), (1, 2), (1, 4))),
weight_norm(nn.Conv2d(32, 32, (3, 9), (1, 2), (1, 4))),
weight_norm(nn.Conv2d(32, 32, (3, 3), padding=(1, 1))),
]
)
if san:
self.conv_post = SANConv2d(32, 1, (3, 3), padding=(1, 1))
else:
self.conv_post = weight_norm(nn.Conv2d(32, 1, (3, 3), padding=(1, 1)))
def forward(
self, x: torch.Tensor, flg_san_train: bool = False
) -> tuple[
Union[torch.Tensor, tuple[torch.Tensor, torch.Tensor]], list[torch.Tensor]
]:
fmap = []
x = self._spectrogram(x)
x.unsqueeze_(1)
for l in self.convs:
x = l(x)
x = F.silu(x, inplace=True)
fmap.append(x)
if self.san:
x = self.conv_post(x, flg_san_train=flg_san_train)
else:
x = self.conv_post(x)
if flg_san_train:
x_fun, x_dir = x
fmap.append(x_fun)
x_fun = torch.flatten(x_fun, 1, -1)
x_dir = torch.flatten(x_dir, 1, -1)
x = x_fun, x_dir
else:
fmap.append(x)
x = torch.flatten(x, 1, -1)
return x, fmap
def _spectrogram(self, x: torch.Tensor) -> torch.Tensor:
n_fft, hop_length, win_length = self.resolution
x = F.pad(
x, ((n_fft - hop_length) // 2, (n_fft - hop_length) // 2), mode="reflect"
)
x.squeeze_(1)
with torch.cuda.amp.autocast(False):
mag = torch.stft(
x.float(),
n_fft=n_fft,
hop_length=hop_length,
win_length=win_length,
window=torch.ones(win_length, device=x.device),
center=False,
return_complex=True,
).abs()
return mag
class MultiPeriodDiscriminator(nn.Module):
def __init__(self, san: bool = False):
super().__init__()
resolutions = [[1024, 120, 600], [2048, 240, 1200], [512, 50, 240]]
periods = [2, 3, 5, 7, 11]
self.discriminators = nn.ModuleList(
[DiscriminatorR(r, san=san) for r in resolutions]
+ [DiscriminatorP(p, san=san) for p in periods]
)
self.discriminator_names = [f"R_{n}_{h}_{w}" for n, h, w in resolutions] + [
f"P_{p}" for p in periods
]
self.san = san
def forward(
self, y: torch.Tensor, y_hat: torch.Tensor, flg_san_train: bool = False
) -> tuple[
list[Union[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]],
list[Union[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]],
list[list[torch.Tensor]],
list[list[torch.Tensor]],
]:
batch_size = y.size(0)
concatenated_y_y_hat = torch.cat([y, y_hat])
y_d_rs = []
y_d_gs = []
fmap_rs = []
fmap_gs = []
for d in self.discriminators:
if flg_san_train:
(y_d_fun, y_d_dir), fmap = d(
concatenated_y_y_hat, flg_san_train=flg_san_train
)
y_d_r_fun, y_d_g_fun = torch.split(y_d_fun, batch_size)
y_d_r_dir, y_d_g_dir = torch.split(y_d_dir, batch_size)
y_d_r = y_d_r_fun, y_d_r_dir
y_d_g = y_d_g_fun, y_d_g_dir
else:
y_d, fmap = d(concatenated_y_y_hat, flg_san_train=flg_san_train)
y_d_r, y_d_g = torch.split(y_d, batch_size)
fmap_r = []
fmap_g = []
for fm in fmap:
fm_r, fm_g = torch.split(fm, batch_size)
fmap_r.append(fm_r)
fmap_g.append(fm_g)
y_d_rs.append(y_d_r)
y_d_gs.append(y_d_g)
fmap_rs.append(fmap_r)
fmap_gs.append(fmap_g)
return y_d_rs, y_d_gs, fmap_rs, fmap_gs
def forward_and_compute_discriminator_loss(
self, y: torch.Tensor, y_hat: torch.Tensor
) -> tuple[torch.Tensor, dict[str, float]]:
y_d_rs, y_d_gs, _, _ = self(y, y_hat, flg_san_train=self.san)
loss = 0.0
stats = {}
assert len(y_d_gs) == len(y_d_rs) == len(self.discriminators)
for dr, dg, name in zip(y_d_rs, y_d_gs, self.discriminator_names):
if self.san:
dr_fun, dr_dir = map(lambda x: x.float(), dr)
dg_fun, dg_dir = map(lambda x: x.float(), dg)
r_loss_fun = F.softplus(1.0 - dr_fun).square().mean()
g_loss_fun = F.softplus(dg_fun).square().mean()
r_loss_dir = F.softplus(1.0 - dr_dir).square().mean()
g_loss_dir = -F.softplus(1.0 - dg_dir).square().mean()
r_loss = r_loss_fun + r_loss_dir
g_loss = g_loss_fun + g_loss_dir
else:
dr = dr.float()
dg = dg.float()
r_loss = (1.0 - dr).square().mean()
g_loss = dg.square().mean()
stats[f"{name}_dr_loss"] = r_loss.item()
stats[f"{name}_dg_loss"] = g_loss.item()
loss += r_loss + g_loss
return loss, stats
def forward_and_compute_generator_loss(
self, y: torch.Tensor, y_hat: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor, dict[str, float]]:
_, y_d_gs, fmap_rs, fmap_gs = self(y, y_hat, flg_san_train=False)
stats = {}
# adversarial loss
adv_loss = 0.0
for dg, name in zip(y_d_gs, self.discriminator_names):
dg = dg.float()
if self.san:
g_loss = F.softplus(1.0 - dg).square().mean()
else:
g_loss = (1.0 - dg).square().mean()
stats[f"{name}_gg_loss"] = g_loss.item()
adv_loss += g_loss
# feature mathcing loss
fm_loss = 0.0
for fr, fg in zip(fmap_rs, fmap_gs):
for r, g in zip(fr, fg):
fm_loss += (r.detach() - g).abs().mean()
return adv_loss, fm_loss, stats
# %% [markdown]
# ## Utilities
# %%
class GradBalancer:
"""Adapted from https://github.com/facebookresearch/encodec/blob/main/encodec/balancer.py"""
def __init__(
self,
weights: dict[str, float],
rescale_grads: bool = True,
total_norm: float = 1.0,
ema_decay: float = 0.999,
per_batch_item: bool = True,
):
self.weights = weights
self.per_batch_item = per_batch_item
self.total_norm = total_norm
self.ema_decay = ema_decay
self.rescale_grads = rescale_grads
self.ema_total: dict[str, float] = defaultdict(float)
self.ema_fix: dict[str, float] = defaultdict(float)
def backward(
self,
losses: dict[str, torch.Tensor],
input: torch.Tensor,
scaler: Optional[torch.cuda.amp.GradScaler] = None,
skip_update_ema: bool = False,
) -> dict[str, float]:
stats = {}
if skip_update_ema:
assert len(losses) == len(self.ema_total)
ema_norms = {k: tot / self.ema_fix[k] for k, tot in self.ema_total.items()}
else:
# 各 loss に対して d loss / d input とそのノルムを計算する
norms = {}
grads = {}
for name, loss in losses.items():
if scaler is not None:
loss = scaler.scale(loss)
(grad,) = torch.autograd.grad(loss, [input], retain_graph=True)
if not grad.isfinite().all():
input.backward(grad)
return {}
grad = grad.detach() / (1.0 if scaler is None else scaler.get_scale())
if self.per_batch_item:
dims = tuple(range(1, grad.dim()))
ema_norm = grad.norm(dim=dims).mean()
else:
ema_norm = grad.norm()
norms[name] = float(ema_norm)
grads[name] = grad
# ノルムの移動平均を計算する
for key, value in norms.items():
self.ema_total[key] = self.ema_total[key] * self.ema_decay + value
self.ema_fix[key] = self.ema_fix[key] * self.ema_decay + 1.0
ema_norms = {k: tot / self.ema_fix[k] for k, tot in self.ema_total.items()}
# ログを取る
total_ema_norm = sum(ema_norms.values())
for k, ema_norm in ema_norms.items():
stats[f"grad_norm_value_{k}"] = ema_norm
stats[f"grad_norm_ratio_{k}"] = ema_norm / (total_ema_norm + 1e-12)
# loss の係数の比率を計算する
if self.rescale_grads:
total_weights = sum([self.weights[k] for k in ema_norms])
ratios = {k: w / total_weights for k, w in self.weights.items()}
# 勾配を修正する
loss = 0.0
for name, ema_norm in ema_norms.items():
if self.rescale_grads:
scale = ratios[name] * self.total_norm / (ema_norm + 1e-12)
else:
scale = self.weights[name]
loss += (losses if skip_update_ema else grads)[name] * scale
if scaler is not None:
loss = scaler.scale(loss)
if skip_update_ema:
loss.backward()
else:
input.backward(loss)
return stats
def state_dict(self):
return {
"ema_total": self.ema_total,
"ema_fix": self.ema_fix,
}
def load_state_dict(self, state_dict):
self.ema_total = state_dict["ema_total"]
self.ema_fix = state_dict["ema_fix"]
class QualityTester(nn.Module):
def __init__(self):
super().__init__()
self.utmos = torch.hub.load(
"tarepan/SpeechMOS:v1.0.0", "utmos22_strong", trust_repo=True
).eval()
@torch.inference_mode()
def compute_mos(self, wav: torch.Tensor) -> dict[str, list[float]]:
res = {"utmos": self.utmos(wav, sr=16000).tolist()}
return res
def test(
self, converted_wav: torch.Tensor, source_wav: torch.Tensor
) -> dict[str, list[float]]:
# [batch_size, wav_length]
res = {}
res.update(self.compute_mos(converted_wav))
return res
def test_many(
self, converted_wavs: list[torch.Tensor], source_wavs: list[torch.Tensor]
) -> tuple[dict[str, float], dict[str, list[float]]]:
# list[batch_size, wav_length]
results = defaultdict(list)
assert len(converted_wavs) == len(source_wavs)
for converted_wav, source_wav in zip(converted_wavs, source_wavs):
res = self.test(converted_wav, source_wav)
for metric_name, value in res.items():
results[metric_name].extend(value)
return {
metric_name: sum(values) / len(values)
for metric_name, values in results.items()
}, results
def compute_grad_norm(
model: nn.Module, return_stats: bool = False
) -> Union[float, dict[str, float]]:
total_norm = 0.0
stats = {}
for name, p in model.named_parameters():
if p.grad is None:
continue
param_norm = p.grad.data.norm().item()
if not math.isfinite(param_norm):
param_norm = p.grad.data.float().norm().item()
total_norm += param_norm * param_norm
if return_stats:
stats[f"grad_norm_{name}"] = param_norm
total_norm = math.sqrt(total_norm)
if return_stats:
return total_norm, stats
else:
return total_norm
def compute_mean_f0(
files: list[Path], method: Literal["dio", "harvest"] = "dio"
) -> float:
sum_log_f0 = 0.0
n_frames = 0
for file in files:
wav, sr = torchaudio.load(file, backend="soundfile")
if method == "dio":
f0, _ = pyworld.dio(wav.ravel().numpy().astype(np.float64), sr)
elif method == "harvest":
f0, _ = pyworld.harvest(wav.ravel().numpy().astype(np.float64), sr)
else:
raise ValueError(f"Invalid method: {method}")
f0 = f0[f0 > 0]
sum_log_f0 += float(np.log(f0).sum())
n_frames += len(f0)
if n_frames == 0:
return math.nan
mean_log_f0 = sum_log_f0 / n_frames
return math.exp(mean_log_f0)
# %% [markdown]
# ## Dataset
# %%
def get_resampler(
sr_before: int, sr_after: int, device="cpu", cache={}
) -> torchaudio.transforms.Resample:
if not isinstance(device, str):
device = str(device)
if (sr_before, sr_after, device) not in cache:
cache[(sr_before, sr_after, device)] = torchaudio.transforms.Resample(
sr_before, sr_after
).to(device)
return cache[(sr_before, sr_after, device)]
def convolve(signal: torch.Tensor, ir: torch.Tensor) -> torch.Tensor:
n = 1 << (signal.size(-1) + ir.size(-1) - 2).bit_length()
res = torch.fft.irfft(torch.fft.rfft(signal, n=n) * torch.fft.rfft(ir, n=n), n=n)
return res[..., : signal.size(-1)]
def random_filter(audio: torch.Tensor) -> torch.Tensor:
assert audio.ndim == 2
ab = torch.rand(audio.size(0), 6) * 0.75 - 0.375
a, b = ab[:, :3], ab[:, 3:]
a[:, 0] = 1.0
b[:, 0] = 1.0
audio = torchaudio.functional.lfilter(audio, a, b, clamp=False)
return audio
def get_noise(
n_samples: int, sample_rate: float, files: list[Union[str, bytes, os.PathLike]]
) -> torch.Tensor:
resample_augmentation_candidates = [0.9, 0.95, 1.0, 1.05, 1.1]
wavs = []
current_length = 0
while current_length < n_samples:
idx_files = torch.randint(0, len(files), ())
file = files[idx_files]
wav, sr = torchaudio.load(file, backend="soundfile")
assert wav.size(0) == 1
augmented_sample_rate = int(
round(
sample_rate
* resample_augmentation_candidates[
torch.randint(0, len(resample_augmentation_candidates), ())
]
)
)
resampler = get_resampler(sr, augmented_sample_rate)
wav = resampler(wav)
wav = random_filter(wav)
wav *= 0.99 / (wav.abs().max() + 1e-5)
wavs.append(wav)
current_length += wav.size(1)
start = torch.randint(0, current_length - n_samples + 1, ())
wav = torch.cat(wavs, dim=1)[:, start : start + n_samples]
assert wav.size() == (1, n_samples), wav.size()
return wav
def get_butterworth_lpf(
cutoff_freq: int, sample_rate: int, cache={}
) -> tuple[torch.Tensor, torch.Tensor]:
if (cutoff_freq, sample_rate) not in cache:
q = math.sqrt(0.5)
omega = math.tau * cutoff_freq / sample_rate
cos_omega = math.cos(omega)
alpha = math.sin(omega) / (2.0 * q)
b1 = (1.0 - cos_omega) / (1.0 + alpha)
b0 = b1 * 0.5
a1 = -2.0 * cos_omega / (1.0 + alpha)
a2 = (1.0 - alpha) / (1.0 + alpha)
cache[(cutoff_freq, sample_rate)] = torch.tensor([b0, b1, b0]), torch.tensor(
[1.0, a1, a2]
)
return cache[(cutoff_freq, sample_rate)]
def augment_audio(
clean: torch.Tensor,
sample_rate: int,
noise_files: list[Union[str, bytes, os.PathLike]],
ir_files: list[Union[str, bytes, os.PathLike]],
) -> torch.Tensor:
# [1, wav_length]
assert clean.size(0) == 1
n_samples = clean.size(1)
snr_candidates = [-20, -25, -30, -35, -40, -45]
original_clean_rms = clean.square().mean().sqrt_()
# noise を取得して clean と concat する
noise = get_noise(n_samples, sample_rate, noise_files)
signals = torch.cat([clean, noise])
# clean, noise に異なるランダムフィルタをかける
signals = random_filter(signals)
# clean, noise にリバーブをかける
if torch.rand(()) < 0.5:
ir_file = ir_files[torch.randint(0, len(ir_files), ())]
ir, sr = torchaudio.load(ir_file, backend="soundfile")
assert ir.size() == (2, sr), ir.size()
assert sr == sample_rate, (sr, sample_rate)
signals = convolve(signals, ir)
# clean, noise に同じ LPF をかける
if torch.rand(()) < 0.2:
if signals.abs().max() > 0.8:
signals /= signals.abs().max() * 1.25
cutoff_freq_candidates = [2000, 3000, 4000, 6000]
cutoff_freq = cutoff_freq_candidates[
torch.randint(0, len(cutoff_freq_candidates), ())
]
b, a = get_butterworth_lpf(cutoff_freq, sample_rate)
signals = torchaudio.functional.lfilter(signals, a, b, clamp=False)
# clean の音量を合わせる
clean, noise = signals
clean_rms = clean.square().mean().sqrt_()
clean *= original_clean_rms / clean_rms
# clean, noise の音量をピークを重視して取る
clean_level = clean.square().square_().mean().sqrt_().sqrt_()
noise_level = noise.square().square_().mean().sqrt_().sqrt_()
# SNR
snr = snr_candidates[torch.randint(0, len(snr_candidates), ())]
# noisy を生成
noisy = clean + noise * (10.0 ** (snr / 20.0) * clean_level / (noise_level + 1e-5))
return noisy
class WavDataset(torch.utils.data.Dataset):
def __init__(
self,
audio_files: list[tuple[Path, int]],
in_sample_rate: int = 16000,
out_sample_rate: int = 24000,
wav_length: int = 4 * 24000, # 4s
segment_length: int = 100, # 1s
noise_files: Optional[list[Union[str, bytes, os.PathLike]]] = None,
ir_files: Optional[list[Union[str, bytes, os.PathLike]]] = None,
):
self.audio_files = audio_files
self.in_sample_rate = in_sample_rate
self.out_sample_rate = out_sample_rate
self.wav_length = wav_length
self.segment_length = segment_length
self.noise_files = noise_files
self.ir_files = ir_files
if (noise_files is None) is not (ir_files is None):
raise ValueError("noise_files and ir_files must be both None or not None")
self.in_hop_length = in_sample_rate // 100
self.out_hop_length = out_sample_rate // 100 # 10ms 刻み
def __getitem__(self, index: int) -> tuple[torch.Tensor, torch.Tensor, int, int]:
file, speaker_id = self.audio_files[index]
clean_wav, sample_rate = torchaudio.load(file, backend="soundfile")
formant_shift_candidates = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0]
formant_shift = formant_shift_candidates[
torch.randint(0, len(formant_shift_candidates), ()).item()
]
resampler_fraction = Fraction(
sample_rate / self.out_sample_rate * 2.0 ** (formant_shift / 12.0)
).limit_denominator(300)
clean_wav = get_resampler(
resampler_fraction.numerator, resampler_fraction.denominator
)(clean_wav)
assert clean_wav.size(0) == 1
assert clean_wav.size(1) != 0
clean_wav = F.pad(clean_wav, (self.wav_length, self.wav_length))
if self.noise_files is None:
assert False
noisy_wav_16k = get_resampler(self.out_sample_rate, self.in_sample_rate)(
clean_wav
)
else:
clean_wav_16k = get_resampler(self.out_sample_rate, self.in_sample_rate)(
clean_wav
)
noisy_wav_16k = augment_audio(
clean_wav_16k, self.in_sample_rate, self.noise_files, self.ir_files
)
clean_wav = clean_wav.squeeze_(0)
noisy_wav_16k = noisy_wav_16k.squeeze_(0)
# 音量をランダマイズする
amplitude = torch.rand(()).item() * 0.899 + 0.1
factor = amplitude / clean_wav.abs().max()
clean_wav *= factor
noisy_wav_16k *= factor
while noisy_wav_16k.abs().max() >= 1.0:
clean_wav *= 0.5
noisy_wav_16k *= 0.5
return clean_wav, noisy_wav_16k, speaker_id, formant_shift
def __len__(self) -> int:
return len(self.audio_files)
def collate(
self, batch: list[tuple[torch.Tensor, torch.Tensor, int, int]]
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
assert self.wav_length % self.out_hop_length == 0
length = self.wav_length // self.out_hop_length
clean_wavs = []
noisy_wavs = []
slice_starts = []
speaker_ids = []
formant_shifts = []
for clean_wav, noisy_wav, speaker_id, formant_shift in batch:
# 発声部分をランダムに 1 箇所選ぶ
(voiced,) = clean_wav.nonzero(as_tuple=True)
assert voiced.numel() != 0
center = voiced[torch.randint(0, voiced.numel(), ()).item()].item()
# 発声部分が中央にくるように、スライス区間を選ぶ
slice_start = center - self.segment_length * self.out_hop_length // 2
assert slice_start >= 0
# スライス区間が含まれるように、ランダムに wav_length の長さを切り出す
r = torch.randint(0, length - self.segment_length + 1, ()).item()
offset = slice_start - r * self.out_hop_length
clean_wavs.append(clean_wav[offset : offset + self.wav_length])
offset_in_sample_rate = int(
round(offset * self.in_sample_rate / self.out_sample_rate)
)
noisy_wavs.append(
noisy_wav[
offset_in_sample_rate : offset_in_sample_rate
+ length * self.in_hop_length
]
)
slice_start = r
slice_starts.append(slice_start)
speaker_ids.append(speaker_id)
formant_shifts.append(formant_shift)
clean_wavs = torch.stack(clean_wavs)
noisy_wavs = torch.stack(noisy_wavs)
slice_starts = torch.tensor(slice_starts)
speaker_ids = torch.tensor(speaker_ids)
formant_shifts = torch.tensor(formant_shifts)
return (
clean_wavs, # [batch_size, wav_length]
noisy_wavs, # [batch_size, wav_length]
slice_starts, # Long[batch_size]
speaker_ids, # Long[batch_size]
formant_shifts, # Long[batch_size]
)
# %% [markdown]
# ## Train
# %%
AUDIO_FILE_SUFFIXES = {
".wav",
".aif",
".aiff",
".fla",
".flac",
".oga",
".ogg",
".opus",
".mp3",
}
def prepare_training():
# 各種準備をする
# 副作用として、出力ディレクトリと TensorBoard のログファイルなどが生成される
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"device={device}")
torch.backends.cudnn.benchmark = True
torch.backends.cuda.matmul.allow_tf32 = True
(h, in_wav_dataset_dir, out_dir, resume) = (
prepare_training_configs_for_experiment
if is_notebook()
else prepare_training_configs
)()
print("config:")
pprint(h)
print()
h = AttrDict(h)
if not in_wav_dataset_dir.is_dir():
raise ValueError(f"{in_wav_dataset_dir} is not found.")
if resume:
latest_checkpoint_file = out_dir / "checkpoint_latest.pt"
if not latest_checkpoint_file.is_file():
raise ValueError(f"{latest_checkpoint_file} is not found.")
else:
if out_dir.is_dir():
if (out_dir / "checkpoint_latest.pt").is_file():
raise ValueError(
f"{out_dir / 'checkpoint_latest.pt'} already exists. "
"Please specify a different output directory, or use --resume option."
)
for file in out_dir.iterdir():
if file.suffix == ".pt":
raise ValueError(
f"{out_dir} already contains model files. "
"Please specify a different output directory."
)
else:
out_dir.mkdir(parents=True)
in_ir_wav_dir = repo_root() / h.in_ir_wav_dir
in_noise_wav_dir = repo_root() / h.in_noise_wav_dir
in_test_wav_dir = repo_root() / h.in_test_wav_dir
assert in_wav_dataset_dir.is_dir(), in_wav_dataset_dir
assert out_dir.is_dir(), out_dir
assert in_ir_wav_dir.is_dir(), in_ir_wav_dir
assert in_noise_wav_dir.is_dir(), in_noise_wav_dir
assert in_test_wav_dir.is_dir(), in_test_wav_dir
# .wav または *.flac のファイルを再帰的に取得
noise_files = sorted(
list(in_noise_wav_dir.rglob("*.wav")) + list(in_noise_wav_dir.rglob("*.flac"))
)
if len(noise_files) == 0:
raise ValueError(f"No audio data found in {in_noise_wav_dir}.")
ir_files = sorted(
list(in_ir_wav_dir.rglob("*.wav")) + list(in_ir_wav_dir.rglob("*.flac"))
)
if len(ir_files) == 0:
raise ValueError(f"No audio data found in {in_ir_wav_dir}.")
# TODO: 無音除去とか
def get_training_filelist(in_wav_dataset_dir: Path):
min_data_per_speaker = 1
speakers: list[str] = []
training_filelist: list[tuple[Path, int]] = []
speaker_audio_files: list[list[Path]] = []
for speaker_dir in sorted(in_wav_dataset_dir.iterdir()):
if not speaker_dir.is_dir():
continue
candidates = []
for wav_file in sorted(speaker_dir.rglob("*")):
if (
not wav_file.is_file()
or wav_file.suffix.lower() not in AUDIO_FILE_SUFFIXES
):
continue
candidates.append(wav_file)
if len(candidates) >= min_data_per_speaker:
speaker_id = len(speakers)
speakers.append(speaker_dir.name)
training_filelist.extend([(file, speaker_id) for file in candidates])
speaker_audio_files.append(candidates)
return speakers, training_filelist, speaker_audio_files
speakers, training_filelist, speaker_audio_files = get_training_filelist(
in_wav_dataset_dir
)
n_speakers = len(speakers)
if n_speakers == 0:
raise ValueError(f"No speaker data found in {in_wav_dataset_dir}.")
print(f"{n_speakers=}")
for i, speaker in enumerate(speakers):
print(f" {i:{len(str(n_speakers - 1))}d}: {speaker}")
print()
print(f"{len(training_filelist)=}")
def get_test_filelist(
in_test_wav_dir: Path, n_speakers: int
) -> list[tuple[Path, list[int]]]:
max_n_test_files = 1000
test_filelist = []
rng = Random(42)
def get_target_id_generator():
if n_speakers > 8:
while True:
order = list(range(n_speakers))
rng.shuffle(order)
yield from order
else:
while True:
yield from range(n_speakers)
target_id_generator = get_target_id_generator()
for file in sorted(in_test_wav_dir.iterdir())[:max_n_test_files]:
if file.suffix.lower() not in AUDIO_FILE_SUFFIXES:
continue
target_ids = [next(target_id_generator) for _ in range(min(8, n_speakers))]
test_filelist.append((file, target_ids))
return test_filelist
test_filelist = get_test_filelist(in_test_wav_dir, n_speakers)
if len(test_filelist) == 0:
warnings.warn(f"No audio data found in {test_filelist}.")
print(f"{len(test_filelist)=}")
for file, target_ids in test_filelist[:12]:
print(f" {file}, {target_ids}")
if len(test_filelist) > 12:
print(" ...")
print()
# データ
training_dataset = WavDataset(
training_filelist,
in_sample_rate=h.in_sample_rate,
out_sample_rate=h.out_sample_rate,
wav_length=h.wav_length,
segment_length=h.segment_length,
noise_files=noise_files,
ir_files=ir_files,
)
training_loader = torch.utils.data.DataLoader(
training_dataset,
num_workers=min(h.num_workers, os.cpu_count()),
collate_fn=training_dataset.collate,
shuffle=True,
sampler=None,
batch_size=h.batch_size,
pin_memory=True,
drop_last=True,
)
print("Computing mean F0s of target speakers...", end="")
speaker_f0s = []
for speaker, files in enumerate(speaker_audio_files):
if len(files) > 10:
files = Random(42).sample(files, 10)
f0 = compute_mean_f0(files)
speaker_f0s.append(f0)
if speaker % 5 == 0:
print()
print(f" {speaker:3d}: {f0:.1f}Hz", end=",")
print()
print("Done.")
print("Computing pitch shifts for test files...")
test_pitch_shifts = []
source_f0s = []
for i, (file, target_ids) in enumerate(tqdm(test_filelist)):
source_f0 = compute_mean_f0([file], method="harvest")
source_f0s.append(source_f0)
if source_f0 != source_f0:
test_pitch_shifts.append([0] * len(target_ids))
continue
pitch_shifts = []
for target_id in target_ids:
target_f0 = speaker_f0s[target_id]
if target_f0 != target_f0:
pitch_shift = 0
else:
pitch_shift = int(round(12 * math.log2(target_f0 / source_f0)))
pitch_shifts.append(pitch_shift)
test_pitch_shifts.append(pitch_shifts)
print("Done.")
# モデルと最適化
phone_extractor = PhoneExtractor().to(device).eval().requires_grad_(False)
phone_extractor_checkpoint = torch.load(
repo_root() / h.phone_extractor_file, map_location="cpu"
)
print(
phone_extractor.load_state_dict(phone_extractor_checkpoint["phone_extractor"])
)
del phone_extractor_checkpoint
pitch_estimator = PitchEstimator().to(device).eval().requires_grad_(False)
pitch_estimator_checkpoint = torch.load(
repo_root() / h.pitch_estimator_file, map_location="cpu"
)
print(
pitch_estimator.load_state_dict(pitch_estimator_checkpoint["pitch_estimator"])
)
del pitch_estimator_checkpoint
net_g = ConverterNetwork(
phone_extractor,
pitch_estimator,
n_speakers,
h.hidden_channels,
).to(device)
net_d = MultiPeriodDiscriminator(san=h.san).to(device)
optim_g = torch.optim.AdamW(
net_g.parameters(),
h.learning_rate,
betas=h.adam_betas,
eps=h.adam_eps,
)
optim_d = torch.optim.AdamW(
net_d.parameters(),
h.learning_rate,
betas=h.adam_betas,
eps=h.adam_eps,
)
grad_scaler = torch.cuda.amp.GradScaler(enabled=h.use_amp)
grad_balancer = GradBalancer(
weights={
"loss_mel": h.grad_weight_mel,
"loss_adv": h.grad_weight_adv,
"loss_fm": h.grad_weight_fm,
},
ema_decay=h.grad_balancer_ema_decay,
)
resample_to_in_sample_rate = torchaudio.transforms.Resample(
h.out_sample_rate, h.in_sample_rate
).to(device)
# チェックポイント読み出し
initial_iteration = 0
if resume:
checkpoint_file = latest_checkpoint_file
elif h.pretrained_file is not None:
checkpoint_file = repo_root() / h.pretrained_file
else:
checkpoint_file = None
if checkpoint_file is not None:
checkpoint = torch.load(checkpoint_file, map_location="cpu")
if not resume: # ファインチューニング
checkpoint_n_speakers = len(checkpoint["net_g"]["embed_speaker.weight"])
initial_speaker_embedding = checkpoint["net_g"]["embed_speaker.weight"][:1]
# initial_speaker_embedding = checkpoint["net_g"]["embed_speaker.weight"].mean(
# 0, keepdim=True
# )
if True:
# 0 とかランダムとかの方が良いかもしれない
checkpoint["net_g"]["embed_speaker.weight"] = initial_speaker_embedding[
[0] * n_speakers
]
else: # 話者追加用
assert n_speakers > checkpoint_n_speakers
print(
f"embed_speaker.weight was padded: {checkpoint_n_speakers} -> {n_speakers}"
)
checkpoint["net_g"]["embed_speaker.weight"] = F.pad(
checkpoint["net_g"]["embed_speaker.weight"],
(0, 0, 0, n_speakers - checkpoint_n_speakers),
)
checkpoint["net_g"]["embed_speaker.weight"][
checkpoint_n_speakers:
] = initial_speaker_embedding
print(net_g.load_state_dict(checkpoint["net_g"], strict=False))
print(net_d.load_state_dict(checkpoint["net_d"], strict=False))
if resume:
optim_g.load_state_dict(checkpoint["optim_g"])
optim_d.load_state_dict(checkpoint["optim_d"])
initial_iteration = checkpoint["iteration"]
grad_balancer.load_state_dict(checkpoint["grad_balancer"])
grad_scaler.load_state_dict(checkpoint["grad_scaler"])
# スケジューラ
def get_cosine_annealing_warmup_scheduler(
optimizer: torch.optim.Optimizer,
warmup_epochs: int,
total_epochs: int,
min_learning_rate: float,
) -> torch.optim.lr_scheduler.LambdaLR:
lr_ratio = min_learning_rate / optimizer.param_groups[0]["lr"]
m = 0.5 * (1.0 - lr_ratio)
a = 0.5 * (1.0 + lr_ratio)
def lr_lambda(current_epoch: int) -> float:
if current_epoch < warmup_epochs:
return current_epoch / warmup_epochs
elif current_epoch < total_epochs:
rate = (current_epoch - warmup_epochs) / (total_epochs - warmup_epochs)
return math.cos(rate * math.pi) * m + a
else:
return min_learning_rate
return torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
scheduler_g = get_cosine_annealing_warmup_scheduler(
optim_g, h.warmup_steps, h.n_steps, h.min_learning_rate
)
scheduler_d = get_cosine_annealing_warmup_scheduler(
optim_d, h.warmup_steps, h.n_steps, h.min_learning_rate
)
for _ in range(initial_iteration + 1):
scheduler_g.step()
scheduler_d.step()
net_g.train()
net_d.train()
# ログとか
dict_scalars = defaultdict(list)
quality_tester = QualityTester().eval().to(device)
writer = SummaryWriter(out_dir)
writer.add_text(
"log",
f"start training w/ {torch.cuda.get_device_name(device) if torch.cuda.is_available() else 'cpu'}.",
initial_iteration,
)
if not resume:
with open(out_dir / "config.json", "w", encoding="utf-8") as f:
json.dump(dict(h), f, indent=4)
if not is_notebook():
shutil.copy(__file__, out_dir)
return (
device,
in_wav_dataset_dir,
h,
out_dir,
speakers,
test_filelist,
training_loader,
speaker_f0s,
test_pitch_shifts,
phone_extractor,
pitch_estimator,
net_g,
net_d,
optim_g,
optim_d,
grad_scaler,
grad_balancer,
resample_to_in_sample_rate,
initial_iteration,
scheduler_g,
scheduler_d,
dict_scalars,
quality_tester,
writer,
)
if __name__ == "__main__":
(
device,
in_wav_dataset_dir,
h,
out_dir,
speakers,
test_filelist,
training_loader,
speaker_f0s,
test_pitch_shifts,
phone_extractor,
pitch_estimator,
net_g,
net_d,
optim_g,
optim_d,
grad_scaler,
grad_balancer,
resample_to_in_sample_rate,
initial_iteration,
scheduler_g,
scheduler_d,
dict_scalars,
quality_tester,
writer,
) = prepare_training()
# 学習
for iteration in tqdm(range(initial_iteration, h.n_steps)):
# === 1. データ前処理 ===
try:
batch = next(data_iter)
except:
data_iter = iter(training_loader)
batch = next(data_iter)
(
clean_wavs,
noisy_wavs_16k,
slice_starts,
speaker_ids,
formant_shift_semitone,
) = map(lambda x: x.to(device, non_blocking=True), batch)
# === 2.1 Discriminator の学習 ===
with torch.cuda.amp.autocast(h.use_amp):
# Generator
y, y_hat, y_hat_for_backward, loss_mel = net_g.forward_and_compute_loss(
noisy_wavs_16k[:, None, :],
speaker_ids,
formant_shift_semitone,
slice_start_indices=slice_starts,
slice_segment_length=h.segment_length,
y_all=clean_wavs[:, None, :],
)
assert y_hat.isfinite().all()
assert loss_mel.isfinite().all()
# Discriminator
loss_discriminator, discriminator_d_stats = (
net_d.forward_and_compute_discriminator_loss(y, y_hat.detach())
)
optim_d.zero_grad()
grad_scaler.scale(loss_discriminator).backward()
grad_scaler.unscale_(optim_d)
grad_norm_d, d_grad_norm_stats = compute_grad_norm(net_d, True)
grad_scaler.step(optim_d)
# === 2.2 Generator の学習 ===
with torch.cuda.amp.autocast(h.use_amp):
# Discriminator
loss_adv, loss_fm, discriminator_g_stats = (
net_d.forward_and_compute_generator_loss(y, y_hat)
)
optim_g.zero_grad()
gradient_balancer_stats = grad_balancer.backward(
{
"loss_mel": loss_mel,
"loss_adv": loss_adv,
"loss_fm": loss_fm,
},
y_hat_for_backward,
grad_scaler,
skip_update_ema=iteration > 10 and iteration % 5 != 0,
)
grad_scaler.unscale_(optim_g)
grad_norm_g, g_grad_norm_stats = compute_grad_norm(net_g, True)
grad_scaler.step(optim_g)
grad_scaler.update()
# === 3. ログ ===
dict_scalars["loss_g/loss_mel"].append(loss_mel.item())
dict_scalars["loss_g/loss_fm"].append(loss_fm.item())
dict_scalars["loss_g/loss_adv"].append(loss_adv.item())
dict_scalars["other/grad_scale"].append(grad_scaler.get_scale())
dict_scalars["loss_d/loss_discriminator"].append(loss_discriminator.item())
if math.isfinite(grad_norm_d):
dict_scalars["other/gradient_norm_d"].append(grad_norm_d)
for name, value in d_grad_norm_stats.items():
dict_scalars[f"~gradient_norm_d/{name}"].append(value)
if math.isfinite(grad_norm_g):
dict_scalars["other/gradient_norm_g"].append(grad_norm_g)
for name, value in g_grad_norm_stats.items():
dict_scalars[f"~gradient_norm_g/{name}"].append(value)
dict_scalars["other/lr_g"].append(scheduler_g.get_last_lr()[0])
dict_scalars["other/lr_d"].append(scheduler_d.get_last_lr()[0])
for k, v in discriminator_d_stats.items():
dict_scalars[f"~loss_discriminator/{k}"].append(v)
for k, v in discriminator_g_stats.items():
dict_scalars[f"~loss_discriminator/{k}"].append(v)
for k, v in gradient_balancer_stats.items():
dict_scalars[f"~gradient_balancer/{k}"].append(v)
if (iteration + 1) % 1000 == 0 or iteration == 0:
for name, scalars in dict_scalars.items():
if scalars:
writer.add_scalar(name, sum(scalars) / len(scalars), iteration + 1)
scalars.clear()
# === 4. 検証 ===
if (iteration + 1) % 50000 == 0 or iteration + 1 in {
1,
5000,
10000,
30000,
h.n_steps,
}:
net_g.eval()
torch.cuda.empty_cache()
dict_qualities_all = defaultdict(list)
n_added_wavs = 0
with torch.inference_mode():
for i, ((file, target_ids), pitch_shift_semitones) in enumerate(
zip(test_filelist, test_pitch_shifts)
):
source_wav, sr = torchaudio.load(file, backend="soundfile")
source_wav = source_wav.to(device)
if sr != h.in_sample_rate:
source_wav = get_resampler(sr, h.in_sample_rate, device)(
source_wav
)
source_wav = source_wav.to(device)
original_source_wav_length = source_wav.size(1)
# 長さのパターンを減らしてキャッシュを効かせる
if source_wav.size(1) % h.in_sample_rate == 0:
padded_source_wav = source_wav
else:
padded_source_wav = F.pad(
source_wav,
(
0,
h.in_sample_rate
- source_wav.size(1) % h.in_sample_rate,
),
)
converted = net_g(
padded_source_wav[[0] * len(target_ids), None],
torch.tensor(target_ids, device=device),
torch.tensor(
[0.0] * len(target_ids), device=device
), # フォルマントシフト
torch.tensor(
[float(p) for p in pitch_shift_semitones], device=device
),
).squeeze_(1)[:, : original_source_wav_length // 160 * 240]
if i < 12:
if iteration == 0:
writer.add_audio(
f"source/y_{i:02d}",
source_wav,
iteration + 1,
h.in_sample_rate,
)
for d in range(
min(len(target_ids), 1 + (12 - i - 1) // len(test_filelist))
):
idx_in_batch = n_added_wavs % len(target_ids)
writer.add_audio(
f"converted/y_hat_{i:02d}_{target_ids[idx_in_batch]:03d}_{pitch_shift_semitones[idx_in_batch]:+02d}",
converted[idx_in_batch],
iteration + 1,
h.out_sample_rate,
)
n_added_wavs += 1
converted = resample_to_in_sample_rate(converted)
quality = quality_tester.test(converted, source_wav)
for metric_name, values in quality.items():
dict_qualities_all[metric_name].extend(values)
assert n_added_wavs == min(
12, len(test_filelist) * len(test_filelist[0][1])
), (
n_added_wavs,
len(test_filelist),
len(speakers),
len(test_filelist[0][1]),
)
dict_qualities = {
metric_name: sum(values) / len(values)
for metric_name, values in dict_qualities_all.items()
if len(values)
}
for metric_name, value in dict_qualities.items():
writer.add_scalar(f"validation/{metric_name}", value, iteration + 1)
for metric_name, values in dict_qualities_all.items():
for i, value in enumerate(values):
writer.add_scalar(
f"~validation_{metric_name}/{i:03d}", value, iteration + 1
)
del dict_qualities, dict_qualities_all
gc.collect()
net_g.train()
torch.cuda.empty_cache()
# === 5. 保存 ===
if (iteration + 1) % 50000 == 0 or iteration + 1 in {
1,
5000,
10000,
30000,
h.n_steps,
}:
# チェックポイント
name = f"{in_wav_dataset_dir.name}_{iteration + 1:08d}"
checkpoint_file_save = out_dir / f"checkpoint_{name}.pt"
if checkpoint_file_save.exists():
checkpoint_file_save = checkpoint_file_save.with_name(
f"{checkpoint_file_save.name}_{hash(None):x}"
)
torch.save(
{
"iteration": iteration + 1,
"net_g": net_g.state_dict(),
"phone_extractor": phone_extractor.state_dict(),
"pitch_estimator": pitch_estimator.state_dict(),
"net_d": net_d.state_dict(),
"optim_g": optim_g.state_dict(),
"optim_d": optim_d.state_dict(),
"grad_balancer": grad_balancer.state_dict(),
"grad_scaler": grad_scaler.state_dict(),
"h": dict(h),
},
checkpoint_file_save,
)
shutil.copy(checkpoint_file_save, out_dir / "checkpoint_latest.pt")
# 推論用
paraphernalia_dir = out_dir / f"paraphernalia_{name}"
if paraphernalia_dir.exists():
paraphernalia_dir = paraphernalia_dir.with_name(
f"{paraphernalia_dir.name}_{hash(None):x}"
)
paraphernalia_dir.mkdir()
phone_extractor_fp16 = PhoneExtractor()
phone_extractor_fp16.load_state_dict(phone_extractor.state_dict())
phone_extractor_fp16.remove_weight_norm()
phone_extractor_fp16.merge_weights()
phone_extractor_fp16.half()
phone_extractor_fp16.dump(paraphernalia_dir / f"phone_extractor.bin")
del phone_extractor_fp16
pitch_estimator_fp16 = PitchEstimator()
pitch_estimator_fp16.load_state_dict(pitch_estimator.state_dict())
pitch_estimator_fp16.merge_weights()
pitch_estimator_fp16.half()
pitch_estimator_fp16.dump(paraphernalia_dir / f"pitch_estimator.bin")
del pitch_estimator_fp16
net_g_fp16 = ConverterNetwork(
nn.Module(), nn.Module(), len(speakers), h.hidden_channels
)
net_g_fp16.load_state_dict(net_g.state_dict())
net_g_fp16.remove_weight_norm()
net_g_fp16.merge_weights()
net_g_fp16.half()
net_g_fp16.dump(paraphernalia_dir / f"waveform_generator.bin")
with open(paraphernalia_dir / f"speaker_embeddings.bin", "wb") as f:
dump_layer(net_g_fp16.embed_speaker, f)
with open(paraphernalia_dir / f"formant_shift_embeddings.bin", "wb") as f:
dump_layer(net_g_fp16.embed_formant_shift, f)
del net_g_fp16
shutil.copy(repo_root() / "assets/images/noimage.png", paraphernalia_dir)
with open(
paraphernalia_dir / f"beatrice_paraphernalia_{name}.toml",
"w",
encoding="utf-8",
) as f:
f.write(
f'''[model]
version = "{PARAPHERNALIA_VERSION}"
name = "{name}"
description = """
No description for this model.
このモデルの説明はありません。
"""
'''
)
for speaker_id, (speaker, speaker_f0) in enumerate(
zip(speakers, speaker_f0s)
):
average_pitch = 69.0 + 12.0 * math.log2(speaker_f0 / 440.0)
average_pitch = round(average_pitch * 8.0) / 8.0
f.write(
f'''
[voice.{speaker_id}]
name = "{speaker}"
description = """
No description for this voice.
この声の説明はありません。
"""
average_pitch = {average_pitch}
[voice.{speaker_id}.portrait]
path = "noimage.png"
description = """
"""
'''
)
del paraphernalia_dir
# TODO: phone_extractor, pitch_estimator が既知のモデルであれば dump を省略
# === 6. スケジューラ更新 ===
scheduler_g.step()
scheduler_d.step()
print("Training finished.")