Files
storyteller-ml/tts/tacotron2-cac/preprocessor/vctk.py
T
2023-04-03 00:02:15 -07:00

275 lines
11 KiB
Python

import os
import random
import json
import re
import tgt
import librosa
import numpy as np
from tqdm import tqdm
from pathlib import Path
import audio as Audio
from text import text_to_sequence
from utils.tools import save_mel_and_audio, plot_embedding
random.seed(1234)
class Preprocessor:
def __init__(self, config):
self.dataset = config["dataset"]
self.in_dir = config["path"]["corpus_path"]
self.wav_tag = config["path"]["wav_tag"]
self.wav_dir = config["path"]["wav_dir"]
self.txt_dir = config["path"]["txt_dir"]
self.out_dir = config["path"]["preprocessed_path"]
self.val_size = config["preprocessing"]["val_size"]
self.sampling_rate = config["preprocessing"]["audio"]["sampling_rate"]
self.skip_len = config["preprocessing"]["audio"]["skip_len"]
self.trim_top_db = config["preprocessing"]["audio"]["trim_top_db"]
self.filter_length = config["preprocessing"]["stft"]["filter_length"]
self.hop_length = config["preprocessing"]["stft"]["hop_length"]
self.silence_audio_size = config["preprocessing"]["audio"]["silence_audio_size"]
self.pre_emphasis = config["preprocessing"]["audio"]["pre_emphasis"]
self.max_wav_value = config["preprocessing"]["audio"]["max_wav_value"]
self.sanity_check = config["preprocessing"]["sanity_check"]
self.cleaners = config["preprocessing"]["text"]["text_cleaners"]
self.STFT = Audio.stft.TacotronSTFT(
config["preprocessing"]["stft"]["filter_length"],
config["preprocessing"]["stft"]["hop_length"],
config["preprocessing"]["stft"]["win_length"],
config["preprocessing"]["mel"]["n_mel_channels"],
config["preprocessing"]["audio"]["sampling_rate"],
config["preprocessing"]["mel"]["mel_fmin"],
config["preprocessing"]["mel"]["mel_fmax"],
)
self.val_prior = self.val_prior_names(os.path.join(self.out_dir, "val.txt"))
self.speaker_emb = None
def val_prior_names(self, val_prior_path):
val_prior_names = set()
if os.path.isfile(val_prior_path):
print("Load pre-defined validation set...")
with open(val_prior_path, "r", encoding="utf-8") as f:
for m in f.readlines():
val_prior_names.add(m.split("|")[0])
return list(val_prior_names)
else:
return None
def _init_spker_embeds(self, spkers):
spker_embeds = dict()
for spker in spkers:
spker_embeds[spker] = list()
return spker_embeds
def build_from_path(self):
txt_dir = os.path.join(self.in_dir, self.txt_dir)
wav_dir = os.path.join(self.in_dir, self.wav_dir)
embedding_dir = os.path.join(self.out_dir, "spker_embed")
os.makedirs((os.path.join(self.out_dir, "text")), exist_ok=True)
os.makedirs((os.path.join(self.out_dir, "mel")), exist_ok=True)
os.makedirs(embedding_dir, exist_ok=True)
print("Processing Data ...")
# out = list()
train = list()
val = list()
n_frames = 0
mel_min = float('inf')
mel_max = -float('inf')
max_seq_len = -float('inf')
sub_dirs = os.listdir(txt_dir)
if self.speaker_emb is not None:
spker_embeds = self._init_spker_embeds(sub_dirs)
skip_speakers = set()
preprocessed_dirs = os.listdir(embedding_dir)
for preprocessed_dir in preprocessed_dirs:
skip_speakers.add(preprocessed_dir.split("-")[0])
speakers = {}
for spker_id, speaker in enumerate(tqdm(sub_dirs)):
speakers[speaker] = spker_id
for i, txt_name in enumerate(tqdm(os.listdir(os.path.join(txt_dir, speaker)))):
basename = txt_name.split(".")[0]
with open(os.path.join(txt_dir, speaker, txt_name), "r") as f:
text = f.readline().strip("\n")
wav_path = os.path.join(os.path.join(wav_dir, speaker), "{}_{}.flac".format(basename, self.wav_tag))
if not os.path.isfile(wav_path):
print("[Error] No flac file:{}".format(wav_path))
continue
ret = self.process_utterance(text, wav_path, speaker, basename, skip_speakers)
if ret is None:
continue
else:
info, n, m_min, m_max, spker_embed = ret
# out.append(info)
if self.val_prior is not None:
if basename not in self.val_prior:
train.append(info)
else:
val.append(info)
else:
if i == 0 or i == 1:
val.append(info)
else:
train.append(info)
if self.speaker_emb is not None:
spker_embeds[speaker].append(spker_embed)
if mel_min > m_min:
mel_min = m_min
if mel_max < m_max:
mel_max = m_max
if n > max_seq_len:
max_seq_len = n
n_frames += n
# Calculate and save mean speaker embedding of this speaker
if self.speaker_emb is not None and speaker not in skip_speakers:
spker_embed_filename = '{}-spker_embed.npy'.format(speaker)
np.save(os.path.join(self.out_dir, 'spker_embed', spker_embed_filename), \
np.mean(spker_embeds[speaker], axis=0), allow_pickle=False)
# Save files
with open(os.path.join(self.out_dir, "speakers.json"), "w") as f:
f.write(json.dumps(speakers))
with open(os.path.join(self.out_dir, "stats.json"), "w") as f:
stats = {
"mel": [
float(mel_min),
float(mel_max),
],
"max_seq_len": max_seq_len
}
f.write(json.dumps(stats))
print(
"Total time: {} hours".format(
n_frames * self.hop_length / self.sampling_rate / 3600
)
)
print("plot speaker embedding...")
plot_embedding(
self.out_dir, *self.load_embedding(embedding_dir),
self.divide_speaker_by_gender(self.in_dir), filename="spker_embed_tsne.png"
)
# random.shuffle(out)
# out = [r for r in out if r is not None]
if self.val_prior is None:
random.shuffle(train)
train = [r for r in train if r is not None]
val = [r for r in val if r is not None]
# Write metadata
with open(os.path.join(self.out_dir, "train.txt"), "w", encoding="utf-8") as f:
for m in train:
f.write(m + "\n")
with open(os.path.join(self.out_dir, "val.txt"), "w", encoding="utf-8") as f:
for m in val:
f.write(m + "\n")
return train, val #out
def load_audio(self, wav_path):
wav_raw, _ = librosa.load(wav_path, self.sampling_rate)
if len(wav_raw) < self.skip_len:
return None
wav = wav_raw / np.abs(wav_raw).max() * 0.999
wav = librosa.effects.trim(wav, top_db=self.trim_top_db, frame_length=self.filter_length, hop_length=self.hop_length)[0]
if self.pre_emphasis:
wav = np.append(wav[0], wav[1:] - 0.97 * wav[:-1])
wav = wav / np.abs(wav).max() * 0.999
wav = np.append(wav, [0.] * self.hop_length * self.silence_audio_size)
wav = wav.astype(np.float32)
return wav_raw, wav
def process_utterance(self, raw_text, wav_path, speaker, basename, skip_speakers):
text_filename = "{}-text-{}.npy".format(speaker, basename)
mel_filename = "{}-mel-{}.npy".format(speaker, basename)
# Preprocess text
if not os.path.isfile(os.path.join(self.out_dir, "text", text_filename)):
text = np.array(text_to_sequence(raw_text, self.cleaners))
np.save(
os.path.join(self.out_dir, "text", text_filename),
text,
)
else:
text = np.load(os.path.join(self.out_dir, "text", text_filename))
# Load and process wav files
wav_raw = wav = None
if not os.path.isfile(os.path.join(self.out_dir, "mel", mel_filename)):
wav_raw, wav = self.load_audio(wav_path)
# Compute mel-scale spectrogram
mel_spectrogram = Audio.tools.get_mel_from_wav(wav, self.STFT)
# Sanity check
if self.sanity_check:
save_mel_and_audio(mel_spectrogram, wav*self.max_wav_value,
self.sampling_rate, self.out_dir, basename, tag="processed"
)
save_mel_and_audio(Audio.tools.get_mel_from_wav(wav_raw, self.STFT), wav_raw*self.max_wav_value,
self.sampling_rate, self.out_dir, basename, tag="raw"
)
exit(0) # quit for testing
np.save(
os.path.join(self.out_dir, "mel", mel_filename),
mel_spectrogram.T,
)
else:
mel_spectrogram = np.load(os.path.join(self.out_dir, "mel", mel_filename)).T
# Speaker embedding
spker_embed=None
if self.speaker_emb is not None and speaker not in skip_speakers:
if wav is not None:
spker_embed = self.speaker_emb(wav)
else:
wav_raw, wav = self.load_audio(wav_path)
spker_embed = self.speaker_emb(wav)
return (
"|".join([basename, speaker, raw_text]),
mel_spectrogram.shape[1],
np.min(mel_spectrogram),
np.max(mel_spectrogram),
spker_embed,
)
def divide_speaker_by_gender(self, in_dir, speaker_path="speaker-info.txt"):
speakers = dict()
with open(os.path.join(in_dir, speaker_path), encoding='utf-8') as f:
for line in tqdm(f):
if "ID" in line: continue
parts = [p.strip() for p in re.sub(' +', ' ',(line.strip())).split(' ')]
spk_id, gender = parts[0], parts[2]
speakers[str(spk_id)] = gender
return speakers
def load_embedding(self, embedding_dir):
embedding_path_list = [_ for _ in Path(embedding_dir).rglob('*.npy')]
embedding = None
embedding_speaker_id = list()
# Gather data
for path in tqdm(embedding_path_list):
embedding = np.concatenate((embedding, np.load(path)), axis=0) \
if embedding is not None else np.load(path)
embedding_speaker_id.append(str(str(path).split('/')[-1].split('-')[0]))
return embedding, embedding_speaker_id