|
|
|
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
import argparse
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
from operator import itemgetter
|
|
|
|
from pathlib import Path
|
|
|
|
from typing import Any
|
|
|
|
from typing import Dict
|
|
|
|
from typing import List
|
|
|
|
|
|
|
|
import jsonlines
|
|
|
|
import librosa
|
|
|
|
import numpy as np
|
|
|
|
import tqdm
|
|
|
|
import yaml
|
|
|
|
from yacs.config import CfgNode as Configuration
|
|
|
|
|
|
|
|
from paddlespeech.t2s.datasets.get_feats import LogMelFBank
|
|
|
|
from paddlespeech.t2s.frontend import English
|
|
|
|
|
|
|
|
|
|
|
|
def get_lj_sentences(file_name, frontend):
|
|
|
|
'''read MFA duration.txt
|
|
|
|
|
|
|
|
Args:
|
|
|
|
file_name (str or Path)
|
|
|
|
Returns:
|
|
|
|
Dict: sentence: {'utt': ([char], [int])}
|
|
|
|
'''
|
|
|
|
f = open(file_name, 'r')
|
|
|
|
sentence = {}
|
|
|
|
speaker_set = set()
|
|
|
|
for line in f:
|
|
|
|
line_list = line.strip().split('|')
|
|
|
|
utt = line_list[0]
|
|
|
|
speaker = utt.split("-")[0][:2]
|
|
|
|
speaker_set.add(speaker)
|
|
|
|
raw_text = line_list[-1]
|
|
|
|
phonemes = frontend.phoneticize(raw_text)
|
|
|
|
phonemes = phonemes[1:-1]
|
|
|
|
phonemes = [phn for phn in phonemes if not phn.isspace()]
|
|
|
|
sentence[utt] = (phonemes, speaker)
|
|
|
|
f.close()
|
|
|
|
return sentence, speaker_set
|
|
|
|
|
|
|
|
|
|
|
|
def get_input_token(sentence, output_path):
|
|
|
|
'''get phone set from training data and save it
|
|
|
|
|
|
|
|
Args:
|
|
|
|
sentence (Dict): sentence: {'utt': ([char], str)}
|
|
|
|
output_path (str or path): path to save phone_id_map
|
|
|
|
'''
|
|
|
|
phn_token = set()
|
|
|
|
for utt in sentence:
|
|
|
|
for phn in sentence[utt][0]:
|
|
|
|
if phn != "<eos>":
|
|
|
|
phn_token.add(phn)
|
|
|
|
phn_token = list(phn_token)
|
|
|
|
phn_token.sort()
|
|
|
|
phn_token = ["<pad>", "<unk>"] + phn_token
|
|
|
|
phn_token += ["<eos>"]
|
|
|
|
|
|
|
|
with open(output_path, 'w') as f:
|
|
|
|
for i, phn in enumerate(phn_token):
|
|
|
|
f.write(phn + ' ' + str(i) + '\n')
|
|
|
|
|
|
|
|
|
|
|
|
def get_spk_id_map(speaker_set, output_path):
|
|
|
|
speakers = sorted(list(speaker_set))
|
|
|
|
with open(output_path, 'w') as f:
|
|
|
|
for i, spk in enumerate(speakers):
|
|
|
|
f.write(spk + ' ' + str(i) + '\n')
|
|
|
|
|
|
|
|
|
|
|
|
def process_sentence(config: Dict[str, Any],
|
|
|
|
fp: Path,
|
|
|
|
sentences: Dict,
|
|
|
|
output_dir: Path,
|
|
|
|
mel_extractor=None):
|
|
|
|
utt_id = fp.stem
|
|
|
|
record = None
|
|
|
|
if utt_id in sentences:
|
|
|
|
# reading, resampling may occur
|
|
|
|
wav, _ = librosa.load(str(fp), sr=config.fs)
|
|
|
|
if len(wav.shape) != 1 or np.abs(wav).max() > 1.0:
|
|
|
|
return record
|
|
|
|
assert len(wav.shape) == 1, f"{utt_id} is not a mono-channel audio."
|
|
|
|
assert np.abs(wav).max(
|
|
|
|
) <= 1.0, f"{utt_id} is seems to be different that 16 bit PCM."
|
|
|
|
phones = sentences[utt_id][0]
|
|
|
|
speaker = sentences[utt_id][1]
|
|
|
|
logmel = mel_extractor.get_log_mel_fbank(wav, base='e')
|
|
|
|
# change duration according to mel_length
|
|
|
|
num_frames = logmel.shape[0]
|
|
|
|
mel_dir = output_dir / "data_speech"
|
|
|
|
mel_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
mel_path = mel_dir / (utt_id + "_speech.npy")
|
|
|
|
np.save(mel_path, logmel)
|
|
|
|
record = {
|
|
|
|
"utt_id": utt_id,
|
|
|
|
"phones": phones,
|
|
|
|
"text_lengths": len(phones),
|
|
|
|
"speech_lengths": num_frames,
|
|
|
|
"speech": str(mel_path),
|
|
|
|
"speaker": speaker
|
|
|
|
}
|
|
|
|
return record
|
|
|
|
|
|
|
|
|
|
|
|
def process_sentences(config,
|
|
|
|
fps: List[Path],
|
|
|
|
sentences: Dict,
|
|
|
|
output_dir: Path,
|
|
|
|
mel_extractor=None,
|
|
|
|
nprocs: int=1):
|
|
|
|
|
|
|
|
if nprocs == 1:
|
|
|
|
results = []
|
|
|
|
for fp in tqdm.tqdm(fps, total=len(fps)):
|
|
|
|
record = process_sentence(
|
|
|
|
config=config,
|
|
|
|
fp=fp,
|
|
|
|
sentences=sentences,
|
|
|
|
output_dir=output_dir,
|
|
|
|
mel_extractor=mel_extractor)
|
|
|
|
if record:
|
|
|
|
results.append(record)
|
|
|
|
else:
|
|
|
|
with ThreadPoolExecutor(nprocs) as pool:
|
|
|
|
futures = []
|
|
|
|
with tqdm.tqdm(total=len(fps)) as progress:
|
|
|
|
for fp in fps:
|
|
|
|
future = pool.submit(process_sentence, config, fp,
|
|
|
|
sentences, output_dir, mel_extractor)
|
|
|
|
future.add_done_callback(lambda p: progress.update())
|
|
|
|
futures.append(future)
|
|
|
|
|
|
|
|
results = []
|
|
|
|
for ft in futures:
|
|
|
|
record = ft.result()
|
|
|
|
if record:
|
|
|
|
results.append(record)
|
|
|
|
|
|
|
|
results.sort(key=itemgetter("utt_id"))
|
|
|
|
with jsonlines.open(output_dir / "metadata.jsonl", 'w') as writer:
|
|
|
|
for item in results:
|
|
|
|
writer.write(item)
|
|
|
|
print("Done")
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
# parse config and args
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
description="Preprocess audio and then extract features.")
|
|
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
"--dataset",
|
|
|
|
default="ljspeech",
|
|
|
|
type=str,
|
|
|
|
help="name of dataset, should in {ljspeech} now")
|
|
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
"--rootdir", default=None, type=str, help="directory to dataset.")
|
|
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
"--dumpdir",
|
|
|
|
type=str,
|
|
|
|
required=True,
|
|
|
|
help="directory to dump feature files.")
|
|
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
"--config-path",
|
|
|
|
default="conf/default.yaml",
|
|
|
|
type=str,
|
|
|
|
help="yaml format configuration file.")
|
|
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
"--num-cpu", type=int, default=1, help="number of process.")
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
config_path = Path(args.config_path).resolve()
|
|
|
|
root_dir = Path(args.rootdir).expanduser()
|
|
|
|
dumpdir = Path(args.dumpdir).expanduser()
|
|
|
|
# use absolute path
|
|
|
|
dumpdir = dumpdir.resolve()
|
|
|
|
dumpdir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
assert root_dir.is_dir()
|
|
|
|
|
|
|
|
with open(config_path, 'rt') as f:
|
|
|
|
_C = yaml.safe_load(f)
|
|
|
|
_C = Configuration(_C)
|
|
|
|
config = _C.clone()
|
|
|
|
|
|
|
|
phone_id_map_path = dumpdir / "phone_id_map.txt"
|
|
|
|
speaker_id_map_path = dumpdir / "speaker_id_map.txt"
|
|
|
|
|
|
|
|
if args.dataset == "ljspeech":
|
|
|
|
wav_files = sorted(list((root_dir / "wavs").rglob("*.wav")))
|
|
|
|
frontend = English()
|
|
|
|
sentences, speaker_set = get_lj_sentences(root_dir / "metadata.csv",
|
|
|
|
frontend)
|
|
|
|
get_input_token(sentences, phone_id_map_path)
|
|
|
|
get_spk_id_map(speaker_set, speaker_id_map_path)
|
|
|
|
# split data into 3 sections
|
|
|
|
num_train = 12900
|
|
|
|
num_dev = 100
|
|
|
|
train_wav_files = wav_files[:num_train]
|
|
|
|
dev_wav_files = wav_files[num_train:num_train + num_dev]
|
|
|
|
test_wav_files = wav_files[num_train + num_dev:]
|
|
|
|
|
|
|
|
train_dump_dir = dumpdir / "train" / "raw"
|
|
|
|
train_dump_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
dev_dump_dir = dumpdir / "dev" / "raw"
|
|
|
|
dev_dump_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
test_dump_dir = dumpdir / "test" / "raw"
|
|
|
|
test_dump_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
# Extractor
|
|
|
|
mel_extractor = LogMelFBank(
|
|
|
|
sr=config.fs,
|
|
|
|
n_fft=config.n_fft,
|
|
|
|
hop_length=config.n_shift,
|
|
|
|
win_length=config.win_length,
|
|
|
|
window=config.window,
|
|
|
|
n_mels=config.n_mels,
|
|
|
|
fmin=config.fmin,
|
|
|
|
fmax=config.fmax)
|
|
|
|
|
|
|
|
# process for the 3 sections
|
|
|
|
if train_wav_files:
|
|
|
|
process_sentences(
|
|
|
|
config=config,
|
|
|
|
fps=train_wav_files,
|
|
|
|
sentences=sentences,
|
|
|
|
output_dir=train_dump_dir,
|
|
|
|
mel_extractor=mel_extractor,
|
|
|
|
nprocs=args.num_cpu)
|
|
|
|
if dev_wav_files:
|
|
|
|
process_sentences(
|
|
|
|
config=config,
|
|
|
|
fps=dev_wav_files,
|
|
|
|
sentences=sentences,
|
|
|
|
output_dir=dev_dump_dir,
|
|
|
|
mel_extractor=mel_extractor,
|
|
|
|
nprocs=args.num_cpu)
|
|
|
|
if test_wav_files:
|
|
|
|
process_sentences(
|
|
|
|
config=config,
|
|
|
|
fps=test_wav_files,
|
|
|
|
sentences=sentences,
|
|
|
|
output_dir=test_dump_dir,
|
|
|
|
mel_extractor=mel_extractor,
|
|
|
|
nprocs=args.num_cpu)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|