chore: import upstream snapshot with attribution
CICD NeMo / cicd-main-unit-tests (push) Blocked by required conditions
CICD NeMo / cicd-main-speech (push) Blocked by required conditions
CICD NeMo / cicd-test-container-build (push) Blocked by required conditions
CICD NeMo / cicd-import-tests (push) Blocked by required conditions
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Blocked by required conditions
CICD NeMo / Nemo_CICD_Test (push) Blocked by required conditions
CICD NeMo / Coverage (e2e) (push) Blocked by required conditions
CICD NeMo / Coverage (unit-test) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
CICD NeMo / cicd-wait-in-queue (push) Waiting to run
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Blocked by required conditions
CICD NeMo / cicd-main-speech (push) Blocked by required conditions
CICD NeMo / cicd-test-container-build (push) Blocked by required conditions
CICD NeMo / cicd-import-tests (push) Blocked by required conditions
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Blocked by required conditions
CICD NeMo / Nemo_CICD_Test (push) Blocked by required conditions
CICD NeMo / Coverage (e2e) (push) Blocked by required conditions
CICD NeMo / Coverage (unit-test) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
CICD NeMo / cicd-wait-in-queue (push) Waiting to run
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. 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.
|
||||
|
||||
# USAGE: python add_noise.py --input_manifest=<manifest file of original "clean" dataset>
|
||||
# --noise_manifest=<manifest file poinitng to noise data>
|
||||
# --out_dir=<destination directory for noisy audio and manifests>
|
||||
# --snrs=<list of snrs at which noise should be added to the audio>
|
||||
# --seed=<seed for random number generator>
|
||||
# --num_workers=<number of parallel workers>
|
||||
# To be able to reproduce the same noisy dataset, use a fixed seed and num_workers=1
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
|
||||
from nemo.collections.asr.parts.preprocessing.perturb import NoisePerturbation
|
||||
from nemo.collections.asr.parts.preprocessing.segment import AudioSegment
|
||||
|
||||
rng = None
|
||||
att_factor = 0.8
|
||||
save_noise = False
|
||||
sample_rate = 16000
|
||||
|
||||
|
||||
def get_out_dir_name(out_dir, input_name, noise_name, snr):
|
||||
return os.path.join(out_dir, input_name, noise_name + "_" + str(snr) + "db")
|
||||
|
||||
|
||||
def create_manifest(input_manifest, noise_manifest, snrs, out_path, save_noise):
|
||||
os.makedirs(os.path.join(out_path, "manifests"), exist_ok=True)
|
||||
for snr in snrs:
|
||||
out_dir = get_out_dir_name(
|
||||
out_path,
|
||||
os.path.splitext(os.path.basename(input_manifest))[0],
|
||||
os.path.splitext(os.path.basename(noise_manifest))[0],
|
||||
snr,
|
||||
)
|
||||
out_mfst = os.path.join(
|
||||
os.path.join(out_path, "manifests"),
|
||||
os.path.splitext(os.path.basename(input_manifest))[0]
|
||||
+ "_"
|
||||
+ os.path.splitext(os.path.basename(noise_manifest))[0]
|
||||
+ "_"
|
||||
+ str(snr)
|
||||
+ "db"
|
||||
+ ".json",
|
||||
)
|
||||
with open(input_manifest, "r") as inf, open(out_mfst, "w") as outf:
|
||||
for line in inf:
|
||||
row = json.loads(line.strip())
|
||||
row['audio_filepath'] = os.path.join(out_dir, os.path.basename(row['audio_filepath']))
|
||||
if save_noise:
|
||||
file_ext = os.path.splitext(row['audio_filepath'])[1]
|
||||
noise_filename = os.path.basename(row['audio_filepath']).replace(file_ext, "_noise" + file_ext)
|
||||
row['noise_filepath'] = os.path.join(out_dir, noise_filename)
|
||||
outf.write(json.dumps(row) + "\n")
|
||||
|
||||
|
||||
def process_row(row):
|
||||
audio_file = row['audio_filepath']
|
||||
global sample_rate
|
||||
data_orig = AudioSegment.from_file(audio_file, target_sr=sample_rate, offset=0)
|
||||
for snr in row['snrs']:
|
||||
min_snr_db = snr
|
||||
max_snr_db = snr
|
||||
global att_factor
|
||||
perturber = NoisePerturbation(
|
||||
manifest_path=row['noise_manifest'], min_snr_db=min_snr_db, max_snr_db=max_snr_db, rng=rng
|
||||
)
|
||||
out_dir = get_out_dir_name(
|
||||
row['out_dir'],
|
||||
os.path.splitext(os.path.basename(row['input_manifest']))[0],
|
||||
os.path.splitext(os.path.basename(row['noise_manifest']))[0],
|
||||
snr,
|
||||
)
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
out_f = os.path.join(out_dir, os.path.basename(audio_file))
|
||||
if os.path.exists(out_f):
|
||||
continue
|
||||
data = copy.deepcopy(data_orig)
|
||||
perturber.perturb(data)
|
||||
|
||||
max_level = np.max(np.abs(data.samples))
|
||||
|
||||
norm_factor = att_factor / max_level
|
||||
new_samples = norm_factor * data.samples
|
||||
sf.write(out_f, new_samples.transpose(), sample_rate)
|
||||
|
||||
global save_noise
|
||||
if save_noise:
|
||||
noise_samples = new_samples - norm_factor * data_orig.samples
|
||||
out_f_ext = os.path.splitext(out_f)[1]
|
||||
out_f_noise = out_f.replace(out_f_ext, "_noise" + out_f_ext)
|
||||
sf.write(out_f_noise, noise_samples.transpose(), sample_rate)
|
||||
|
||||
|
||||
def add_noise(infile, snrs, noise_manifest, out_dir, num_workers=1):
|
||||
allrows = []
|
||||
|
||||
with open(infile, "r") as inf:
|
||||
for line in inf:
|
||||
row = json.loads(line.strip())
|
||||
row['snrs'] = snrs
|
||||
row['out_dir'] = out_dir
|
||||
row['noise_manifest'] = noise_manifest
|
||||
row['input_manifest'] = infile
|
||||
allrows.append(row)
|
||||
pool = multiprocessing.Pool(num_workers)
|
||||
pool.map(process_row, allrows)
|
||||
pool.close()
|
||||
print('Done!')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--input_manifest",
|
||||
type=str,
|
||||
required=True,
|
||||
help="clean test set",
|
||||
)
|
||||
parser.add_argument("--noise_manifest", type=str, required=True, help="path to noise manifest file")
|
||||
parser.add_argument("--out_dir", type=str, required=True, help="destination directory for audio and manifests")
|
||||
parser.add_argument("--snrs", type=int, nargs="+", default=[0, 10, 20, 30])
|
||||
parser.add_argument("--seed", type=int, default=None)
|
||||
parser.add_argument("--num_workers", default=1, type=int)
|
||||
parser.add_argument("--sample_rate", default=16000, type=int)
|
||||
parser.add_argument(
|
||||
"--attenuation_factor",
|
||||
default=0.8,
|
||||
type=float,
|
||||
help="Attenuation factor applied on the normalized noise-added samples before writing to wave",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--save_noise", default=False, action="store_true", help="save the noise added to the input signal"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
global sample_rate
|
||||
sample_rate = args.sample_rate
|
||||
global att_factor
|
||||
att_factor = args.attenuation_factor
|
||||
global save_noise
|
||||
save_noise = args.save_noise
|
||||
global rng
|
||||
rng = args.seed
|
||||
num_workers = args.num_workers
|
||||
|
||||
add_noise(args.input_manifest, args.snrs, args.noise_manifest, args.out_dir, num_workers=num_workers)
|
||||
create_manifest(args.input_manifest, args.noise_manifest, args.snrs, args.out_dir, args.save_noise)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,122 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. 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.
|
||||
|
||||
# USAGE:
|
||||
# python fisher_audio_to_wav.py \
|
||||
# --data_root=<FisherEnglishTrainingSpeech root> \
|
||||
# --dest_root=<destination dir root>
|
||||
#
|
||||
# Converts all .sph audio files in the Fisher dataset to .wav.
|
||||
# Requires sph2pipe to be installed.
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
parser = argparse.ArgumentParser(description='Convert Fisher .sph to .wav')
|
||||
parser.add_argument(
|
||||
"--data_root",
|
||||
default=None,
|
||||
type=str,
|
||||
required=True,
|
||||
help="The path to the root Fisher dataset folder.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dest_root",
|
||||
default=None,
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to the destination root directory.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def __convert_audio(in_path, out_path):
|
||||
"""
|
||||
Helper function that's called per thread, converts sph to wav.
|
||||
Args:
|
||||
in_path: source sph file to convert
|
||||
out_path: destination for wav file
|
||||
"""
|
||||
cmd = ["sph2pipe", "-f", "wav", "-p", in_path, out_path]
|
||||
subprocess.run(cmd)
|
||||
|
||||
|
||||
def __process_set(data_root, dst_root):
|
||||
"""
|
||||
Finds and converts all sph audio files in the given directory to wav.
|
||||
Args:
|
||||
data_folder: source directory with sph files to convert
|
||||
dst_root: where wav files will be stored
|
||||
"""
|
||||
sph_list = glob.glob(data_root)
|
||||
|
||||
if not os.path.exists(dst_root):
|
||||
os.makedirs(dst_root)
|
||||
|
||||
# Set up and execute concurrent audio conversion
|
||||
tp = concurrent.futures.ProcessPoolExecutor(max_workers=64)
|
||||
futures = []
|
||||
|
||||
for sph_path in tqdm(sph_list, desc="Submitting sph futures", unit="file"):
|
||||
audio_id, _ = os.path.splitext(os.path.basename(sph_path))
|
||||
out_path = os.path.join(dst_root, "{}.wav".format(audio_id))
|
||||
futures.append(tp.submit(__convert_audio, sph_path, out_path))
|
||||
|
||||
pbar = tqdm(total=len(sph_list), desc="Converting sph files", unit="file")
|
||||
count = 0
|
||||
for f in concurrent.futures.as_completed(futures):
|
||||
count += 1
|
||||
pbar.update()
|
||||
tp.shutdown()
|
||||
pbar.close()
|
||||
|
||||
|
||||
def main():
|
||||
data_root = args.data_root
|
||||
dest_root = args.dest_root
|
||||
|
||||
logging.info("\n\nConverting audio for Part 1")
|
||||
__process_set(
|
||||
os.path.join(
|
||||
data_root,
|
||||
"LDC2004S13-Part1",
|
||||
"fisher_eng_tr_sp_d*",
|
||||
"audio",
|
||||
"*",
|
||||
"*.sph",
|
||||
),
|
||||
os.path.join(dest_root, "LDC2004S13-Part1", "audio_wav"),
|
||||
)
|
||||
|
||||
logging.info("\n\nConverting audio for Part 2")
|
||||
__process_set(
|
||||
os.path.join(
|
||||
data_root,
|
||||
"LDC2005S13-Part2",
|
||||
"fe_03_p2_sph*",
|
||||
"audio",
|
||||
"*",
|
||||
"*.sph",
|
||||
),
|
||||
os.path.join(dest_root, "LDC2005S13-Part2", "audio_wav"),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,141 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. 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 itertools
|
||||
import os
|
||||
from argparse import ArgumentParser
|
||||
from typing import Dict
|
||||
|
||||
from syllabify import syllabify
|
||||
|
||||
|
||||
"""
|
||||
Usage:
|
||||
cd NeMo/scripts && python dataset_processing/g2p/convert_cmu_arpabet_to_ipa.py
|
||||
"""
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = ArgumentParser("ARPABET to IPA conversion sctipt")
|
||||
parser.add_argument(
|
||||
'--cmu_arpabet',
|
||||
help="Path to CMU ARPABET dictionary file",
|
||||
type=str,
|
||||
default="tts_dataset_files/cmudict-0.7b_nv22.10",
|
||||
)
|
||||
parser.add_argument("--ipa_out", help="Path to save IPA version of the dictionary", type=str, required=True)
|
||||
parser.add_argument(
|
||||
"--mapping",
|
||||
help="ARPABET to IPA phoneme mapping file",
|
||||
type=str,
|
||||
default="tts_dataset_files/cmudict-arpabet_to_ipa_nv22.10.tsv",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def convert_arp_to_ipa(arp_to_ipa_dict: Dict[str, str], arp_input: str, remove_space: bool = False) -> str:
|
||||
"""
|
||||
Converts ARPABET phoneme to IPA based on arp_to_ipa_dict mapping
|
||||
|
||||
Args:
|
||||
arp_to_ipa_dict: ARPABET to IPA phonemes mapping
|
||||
arp_input: ARPABET input
|
||||
remove_space: set to TRUE to remove spaces between IPA phonemes
|
||||
|
||||
Returns:
|
||||
input word in IPA form
|
||||
"""
|
||||
|
||||
primary_stress = "ˈ"
|
||||
secondary_stress = "ˌ"
|
||||
stress_dict = {"0": "", "1": primary_stress, "2": secondary_stress}
|
||||
|
||||
word_ipa = ""
|
||||
phonemes = arp_input.split()
|
||||
|
||||
# split ARPABET phoneme input into syllables,
|
||||
# e.g. syllabify(["HH", "AH0", "L", "OW1"]) -> [(['HH'], ['AH0'], []), (['L'], ['OW1'], [])]
|
||||
syllables = syllabify(phonemes)
|
||||
|
||||
for syl_idx, syll in enumerate(syllables):
|
||||
syll_stress = ""
|
||||
syll_ipa = ""
|
||||
|
||||
# syll is a tuple of lists of phonemes, here we flatten it and get rid of empty entries,
|
||||
# e.g. (['HH'], ['AH0'], []) -> ['HH', 'AH0']
|
||||
syll = [x for x in itertools.chain.from_iterable(syll)]
|
||||
for phon_idx, phon in enumerate(syll):
|
||||
if phon[-1].isdigit():
|
||||
syll_stress = phon[-1]
|
||||
if syll_stress not in stress_dict:
|
||||
raise ValueError(f"{syll_stress} unknown")
|
||||
syll_stress = stress_dict[syll_stress]
|
||||
|
||||
# some phonemes are followed by a digit that represents stress, e.g., `AH0`
|
||||
if phon not in arp_to_ipa_dict and phon[-1].isdigit():
|
||||
phon = phon[:-1]
|
||||
|
||||
if phon not in arp_to_ipa_dict:
|
||||
raise ValueError(f"|{phon}| phoneme not found in |{arp_input}|")
|
||||
else:
|
||||
ipa_phone = arp_to_ipa_dict[phon]
|
||||
syll_ipa += ipa_phone + " "
|
||||
|
||||
word_ipa += " " + syll_stress + syll_ipa.strip()
|
||||
|
||||
word_ipa = word_ipa.strip()
|
||||
if remove_space:
|
||||
word_ipa = word_ipa.replace(" ", "")
|
||||
return word_ipa
|
||||
|
||||
|
||||
def _get_arpabet_to_ipa_mapping(arp_ipa_map_file: str) -> Dict[str, str]:
|
||||
"""
|
||||
arp_ipa_map_file: Arpabet to IPA phonemes mapping
|
||||
"""
|
||||
arp_to_ipa = {}
|
||||
with open(arp_ipa_map_file, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
arp, ipa = line.strip().split("\t")
|
||||
arp_to_ipa[arp] = ipa
|
||||
return arp_to_ipa
|
||||
|
||||
|
||||
def convert_cmu_arpabet_to_ipa(arp_ipa_map_file: str, arp_dict_file: str, output_ipa_file: str):
|
||||
"""
|
||||
Converts CMU ARPABET-based dictionary to IPA.
|
||||
|
||||
Args:
|
||||
arp_ipa_map_file: ARPABET to IPA phoneme mapping file
|
||||
arp_dict_file: path to ARPABET version of CMU dictionary
|
||||
output_ipa_file: path to output IPA version of CMU dictionary
|
||||
"""
|
||||
arp_to_ipa_dict = _get_arpabet_to_ipa_mapping(arp_ipa_map_file)
|
||||
with open(arp_dict_file, "r", encoding="utf-8") as f_arp, open(output_ipa_file, "w", encoding="utf-8") as f_ipa:
|
||||
for line in f_arp:
|
||||
if line.startswith(";;;"):
|
||||
f_ipa.write(line)
|
||||
else:
|
||||
# First, split the line at " #" if there are comments in the dictionary file following the mapping entries.
|
||||
# Next, split at default " " separator.
|
||||
graphemes, phonemes = line.split(" #")[0].strip().split(" ")
|
||||
ipa_form = convert_arp_to_ipa(arp_to_ipa_dict, phonemes, remove_space=True)
|
||||
f_ipa.write(f"{graphemes} {ipa_form}\n")
|
||||
|
||||
print(f"IPA version of {os.path.abspath(arp_dict_file)} saved in {os.path.abspath(output_ipa_file)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_args()
|
||||
convert_cmu_arpabet_to_ipa(args.mapping, args.cmu_arpabet, args.ipa_out)
|
||||
@@ -0,0 +1,288 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. 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.
|
||||
#
|
||||
# Copyright (c) 2012-2013 Kyle Gorman <gormanky@ohsu.edu>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a
|
||||
# copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included
|
||||
# in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# syllabify.py: prosodic parsing of ARPABET entries
|
||||
# source: https://github.com/kylebgorman/syllabify
|
||||
|
||||
from itertools import chain
|
||||
|
||||
## constants
|
||||
SLAX = {
|
||||
"IH1",
|
||||
"IH2",
|
||||
"EH1",
|
||||
"EH2",
|
||||
"AE1",
|
||||
"AE2",
|
||||
"AH1",
|
||||
"AH2",
|
||||
"UH1",
|
||||
"UH2",
|
||||
}
|
||||
VOWELS = {
|
||||
"IY1",
|
||||
"IY2",
|
||||
"IY0",
|
||||
"EY1",
|
||||
"EY2",
|
||||
"EY0",
|
||||
"AA1",
|
||||
"AA2",
|
||||
"AA0",
|
||||
"ER1",
|
||||
"ER2",
|
||||
"ER0",
|
||||
"AW1",
|
||||
"AW2",
|
||||
"AW0",
|
||||
"AO1",
|
||||
"AO2",
|
||||
"AO0",
|
||||
"AY1",
|
||||
"AY2",
|
||||
"AY0",
|
||||
"OW1",
|
||||
"OW2",
|
||||
"OW0",
|
||||
"OY1",
|
||||
"OY2",
|
||||
"OY0",
|
||||
"IH0",
|
||||
"EH0",
|
||||
"AE0",
|
||||
"AH0",
|
||||
"UH0",
|
||||
"UW1",
|
||||
"UW2",
|
||||
"UW0",
|
||||
"UW",
|
||||
"IY",
|
||||
"EY",
|
||||
"AA",
|
||||
"ER",
|
||||
"AW",
|
||||
"AO",
|
||||
"AY",
|
||||
"OW",
|
||||
"OY",
|
||||
"UH",
|
||||
"IH",
|
||||
"EH",
|
||||
"AE",
|
||||
"AH",
|
||||
"UH",
|
||||
} | SLAX
|
||||
|
||||
## licit medial onsets
|
||||
|
||||
O2 = {
|
||||
("P", "R"),
|
||||
("T", "R"),
|
||||
("K", "R"),
|
||||
("B", "R"),
|
||||
("D", "R"),
|
||||
("G", "R"),
|
||||
("F", "R"),
|
||||
("TH", "R"),
|
||||
("P", "L"),
|
||||
("K", "L"),
|
||||
("B", "L"),
|
||||
("G", "L"),
|
||||
("F", "L"),
|
||||
("S", "L"),
|
||||
("K", "W"),
|
||||
("G", "W"),
|
||||
("S", "W"),
|
||||
("S", "P"),
|
||||
("S", "T"),
|
||||
("S", "K"),
|
||||
("HH", "Y"), # "clerihew"
|
||||
("R", "W"),
|
||||
}
|
||||
O3 = {("S", "T", "R"), ("S", "K", "L"), ("T", "R", "W")} # "octroi"
|
||||
|
||||
# This does not represent anything like a complete list of onsets, but
|
||||
# merely those that need to be maximized in medial position.
|
||||
|
||||
|
||||
def syllabify(pron, alaska_rule=True):
|
||||
"""
|
||||
Syllabifies a CMU dictionary (ARPABET) word string
|
||||
|
||||
# Alaska rule:
|
||||
>>> pprint(syllabify('AH0 L AE1 S K AH0'.split())) # Alaska
|
||||
'-AH0-.L-AE1-S.K-AH0-'
|
||||
>>> pprint(syllabify('AH0 L AE1 S K AH0'.split(), 0)) # Alaska
|
||||
'-AH0-.L-AE1-.S K-AH0-'
|
||||
|
||||
# huge medial onsets:
|
||||
>>> pprint(syllabify('M IH1 N S T R AH0 L'.split())) # minstrel
|
||||
'M-IH1-N.S T R-AH0-L'
|
||||
>>> pprint(syllabify('AA1 K T R W AA0 R'.split())) # octroi
|
||||
'-AA1-K.T R W-AA0-R'
|
||||
|
||||
# destressing
|
||||
>>> pprint(destress(syllabify('M IH1 L AH0 T EH2 R IY0'.split())))
|
||||
'M-IH-.L-AH-.T-EH-.R-IY-'
|
||||
|
||||
# normal treatment of 'j':
|
||||
>>> pprint(syllabify('M EH1 N Y UW0'.split())) # menu
|
||||
'M-EH1-N.Y-UW0-'
|
||||
>>> pprint(syllabify('S P AE1 N Y AH0 L'.split())) # spaniel
|
||||
'S P-AE1-N.Y-AH0-L'
|
||||
>>> pprint(syllabify('K AE1 N Y AH0 N'.split())) # canyon
|
||||
'K-AE1-N.Y-AH0-N'
|
||||
>>> pprint(syllabify('M IH0 N Y UW2 EH1 T'.split())) # minuet
|
||||
'M-IH0-N.Y-UW2-.-EH1-T'
|
||||
>>> pprint(syllabify('JH UW1 N Y ER0'.split())) # junior
|
||||
'JH-UW1-N.Y-ER0-'
|
||||
>>> pprint(syllabify('K L EH R IH HH Y UW'.split())) # clerihew
|
||||
'K L-EH-.R-IH-.HH Y-UW-'
|
||||
|
||||
# nuclear treatment of 'j'
|
||||
>>> pprint(syllabify('R EH1 S K Y UW0'.split())) # rescue
|
||||
'R-EH1-S.K-Y UW0-'
|
||||
>>> pprint(syllabify('T R IH1 B Y UW0 T'.split())) # tribute
|
||||
'T R-IH1-B.Y-UW0-T'
|
||||
>>> pprint(syllabify('N EH1 B Y AH0 L AH0'.split())) # nebula
|
||||
'N-EH1-B.Y-AH0-.L-AH0-'
|
||||
>>> pprint(syllabify('S P AE1 CH UH0 L AH0'.split())) # spatula
|
||||
'S P-AE1-.CH-UH0-.L-AH0-'
|
||||
>>> pprint(syllabify('AH0 K Y UW1 M AH0 N'.split())) # acumen
|
||||
'-AH0-K.Y-UW1-.M-AH0-N'
|
||||
>>> pprint(syllabify('S AH1 K Y AH0 L IH0 N T'.split())) # succulent
|
||||
'S-AH1-K.Y-AH0-.L-IH0-N T'
|
||||
>>> pprint(syllabify('F AO1 R M Y AH0 L AH0'.split())) # formula
|
||||
'F-AO1 R-M.Y-AH0-.L-AH0-'
|
||||
>>> pprint(syllabify('V AE1 L Y UW0'.split())) # value
|
||||
'V-AE1-L.Y-UW0-'
|
||||
|
||||
# everything else
|
||||
>>> pprint(syllabify('N AO0 S T AE1 L JH IH0 K'.split())) # nostalgic
|
||||
'N-AO0-.S T-AE1-L.JH-IH0-K'
|
||||
>>> pprint(syllabify('CH ER1 CH M AH0 N'.split())) # churchmen
|
||||
'CH-ER1-CH.M-AH0-N'
|
||||
>>> pprint(syllabify('K AA1 M P AH0 N S EY2 T'.split())) # compensate
|
||||
'K-AA1-M.P-AH0-N.S-EY2-T'
|
||||
>>> pprint(syllabify('IH0 N S EH1 N S'.split())) # inCENSE
|
||||
'-IH0-N.S-EH1-N S'
|
||||
>>> pprint(syllabify('IH1 N S EH2 N S'.split())) # INcense
|
||||
'-IH1-N.S-EH2-N S'
|
||||
>>> pprint(syllabify('AH0 S EH1 N D'.split())) # ascend
|
||||
'-AH0-.S-EH1-N D'
|
||||
>>> pprint(syllabify('R OW1 T EY2 T'.split())) # rotate
|
||||
'R-OW1-.T-EY2-T'
|
||||
>>> pprint(syllabify('AA1 R T AH0 S T'.split())) # artist
|
||||
'-AA1 R-.T-AH0-S T'
|
||||
>>> pprint(syllabify('AE1 K T ER0'.split())) # actor
|
||||
'-AE1-K.T-ER0-'
|
||||
>>> pprint(syllabify('P L AE1 S T ER0'.split())) # plaster
|
||||
'P L-AE1-S.T-ER0-'
|
||||
>>> pprint(syllabify('B AH1 T ER0'.split())) # butter
|
||||
'B-AH1-.T-ER0-'
|
||||
>>> pprint(syllabify('K AE1 M AH0 L'.split())) # camel
|
||||
'K-AE1-.M-AH0-L'
|
||||
>>> pprint(syllabify('AH1 P ER0'.split())) # upper
|
||||
'-AH1-.P-ER0-'
|
||||
>>> pprint(syllabify('B AH0 L UW1 N'.split())) # balloon
|
||||
'B-AH0-.L-UW1-N'
|
||||
>>> pprint(syllabify('P R OW0 K L EY1 M'.split())) # proclaim
|
||||
'P R-OW0-.K L-EY1-M'
|
||||
>>> pprint(syllabify('IH0 N S EY1 N'.split())) # insane
|
||||
'-IH0-N.S-EY1-N'
|
||||
>>> pprint(syllabify('IH0 K S K L UW1 D'.split())) # exclude
|
||||
'-IH0-K.S K L-UW1-D'
|
||||
"""
|
||||
## main pass
|
||||
mypron = list(pron)
|
||||
nuclei = []
|
||||
onsets = []
|
||||
i = -1
|
||||
for j, seg in enumerate(mypron):
|
||||
if seg in VOWELS:
|
||||
nuclei.append([seg])
|
||||
onsets.append(mypron[i + 1 : j]) # actually interludes, r.n.
|
||||
i = j
|
||||
codas = [mypron[i + 1 :]]
|
||||
## resolve disputes and compute coda
|
||||
for i in range(1, len(onsets)):
|
||||
coda = []
|
||||
# boundary cases
|
||||
if len(onsets[i]) > 1 and onsets[i][0] == "R":
|
||||
nuclei[i - 1].append(onsets[i].pop(0))
|
||||
if len(onsets[i]) > 2 and onsets[i][-1] == "Y":
|
||||
nuclei[i].insert(0, onsets[i].pop())
|
||||
if len(onsets[i]) > 1 and alaska_rule and nuclei[i - 1][-1] in SLAX and onsets[i][0] == "S":
|
||||
coda.append(onsets[i].pop(0))
|
||||
# onset maximization
|
||||
depth = 1
|
||||
if len(onsets[i]) > 1:
|
||||
if tuple(onsets[i][-2:]) in O2:
|
||||
depth = 3 if tuple(onsets[i][-3:]) in O3 else 2
|
||||
for j in range(len(onsets[i]) - depth):
|
||||
coda.append(onsets[i].pop(0))
|
||||
# store coda
|
||||
codas.insert(i - 1, coda)
|
||||
|
||||
## verify that all segments are included in the ouput
|
||||
output = list(zip(onsets, nuclei, codas)) # in Python3 zip is a generator
|
||||
flat_output = list(chain.from_iterable(chain.from_iterable(output)))
|
||||
if flat_output != mypron:
|
||||
raise ValueError(f"could not syllabify {mypron}, got {flat_output}")
|
||||
return output
|
||||
|
||||
|
||||
def pprint(syllab):
|
||||
"""
|
||||
Pretty-print a syllabification
|
||||
"""
|
||||
return ".".join("-".join(" ".join(p) for p in syl) for syl in syllab)
|
||||
|
||||
|
||||
def destress(syllab):
|
||||
"""
|
||||
Generate a syllabification with nuclear stress information removed
|
||||
"""
|
||||
syls = []
|
||||
for onset, nucleus, coda in syllab:
|
||||
nuke = [p[:-1] if p[-1] in {"0", "1", "2"} else p for p in nucleus]
|
||||
syls.append((onset, nuke, coda))
|
||||
return syls
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import doctest
|
||||
|
||||
doctest.testmod()
|
||||
@@ -0,0 +1,177 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. 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.
|
||||
|
||||
# USAGE: python get_aishell_data.py --data_root=<where to put data>
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import tarfile
|
||||
import urllib.request
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.utils.tar_utils import safe_extract
|
||||
|
||||
parser = argparse.ArgumentParser(description="Aishell Data download")
|
||||
parser.add_argument("--data_root", required=True, default=None, type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
URL = {"data_aishell": "http://www.openslr.org/resources/33/data_aishell.tgz"}
|
||||
|
||||
|
||||
def __retrieve_with_progress(source: str, filename: str):
|
||||
"""
|
||||
Downloads source to destination
|
||||
Displays progress bar
|
||||
Args:
|
||||
source: url of resource
|
||||
destination: local filepath
|
||||
Returns:
|
||||
"""
|
||||
with open(filename, "wb") as f:
|
||||
response = urllib.request.urlopen(source)
|
||||
total = response.length
|
||||
|
||||
if total is None:
|
||||
f.write(response.content)
|
||||
else:
|
||||
with tqdm(total=total, unit="B", unit_scale=True, unit_divisor=1024) as pbar:
|
||||
for data in response:
|
||||
f.write(data)
|
||||
pbar.update(len(data))
|
||||
|
||||
|
||||
def __maybe_download_file(destination: str, source: str):
|
||||
"""
|
||||
Downloads source to destination if it doesn't exist.
|
||||
If exists, skips download
|
||||
Args:
|
||||
destination: local filepath
|
||||
source: url of resource
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
source = URL[source]
|
||||
if not os.path.exists(destination):
|
||||
logging.info("{0} does not exist. Downloading ...".format(destination))
|
||||
__retrieve_with_progress(source, filename=destination + ".tmp")
|
||||
os.rename(destination + ".tmp", destination)
|
||||
logging.info("Downloaded {0}.".format(destination))
|
||||
else:
|
||||
logging.info("Destination {0} exists. Skipping.".format(destination))
|
||||
return destination
|
||||
|
||||
|
||||
def __extract_all_files(filepath: str, data_root: str, data_dir: str):
|
||||
if not os.path.exists(data_dir):
|
||||
extract_file(filepath, data_root)
|
||||
audio_dir = os.path.join(data_dir, "wav")
|
||||
for subfolder, _, filelist in os.walk(audio_dir):
|
||||
for ftar in filelist:
|
||||
extract_file(os.path.join(subfolder, ftar), subfolder)
|
||||
else:
|
||||
logging.info("Skipping extracting. Data already there %s" % data_dir)
|
||||
|
||||
|
||||
def extract_file(filepath: str, data_dir: str):
|
||||
try:
|
||||
with tarfile.open(filepath) as tar:
|
||||
safe_extract(tar, data_dir)
|
||||
except Exception:
|
||||
logging.info("Not extracting. Maybe already there?")
|
||||
|
||||
|
||||
def __process_data(data_folder: str, dst_folder: str):
|
||||
"""
|
||||
To generate manifest
|
||||
Args:
|
||||
data_folder: source with wav files
|
||||
dst_folder: where manifest files will be stored
|
||||
Returns:
|
||||
|
||||
"""
|
||||
|
||||
if not os.path.exists(dst_folder):
|
||||
os.makedirs(dst_folder)
|
||||
|
||||
transcript_file = os.path.join(data_folder, "transcript", "aishell_transcript_v0.8.txt")
|
||||
transcript_dict = {}
|
||||
with open(transcript_file, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
audio_id, text = line.split(" ", 1)
|
||||
# remove white space
|
||||
text = text.replace(" ", "")
|
||||
transcript_dict[audio_id] = text
|
||||
|
||||
data_types = ["train", "dev", "test"]
|
||||
vocab_count = {}
|
||||
for dt in data_types:
|
||||
json_lines = []
|
||||
audio_dir = os.path.join(data_folder, "wav", dt)
|
||||
for sub_folder, _, file_list in os.walk(audio_dir):
|
||||
for fname in file_list:
|
||||
audio_path = os.path.join(sub_folder, fname)
|
||||
audio_id = fname.strip(".wav")
|
||||
if audio_id not in transcript_dict:
|
||||
continue
|
||||
text = transcript_dict[audio_id]
|
||||
for li in text:
|
||||
vocab_count[li] = vocab_count.get(li, 0) + 1
|
||||
duration = subprocess.check_output(["soxi", "-D", audio_path])
|
||||
duration = float(duration)
|
||||
json_lines.append(
|
||||
json.dumps(
|
||||
{
|
||||
"audio_filepath": os.path.abspath(audio_path),
|
||||
"duration": duration,
|
||||
"text": text,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
|
||||
manifest_path = os.path.join(dst_folder, dt + ".json")
|
||||
with open(manifest_path, "w", encoding="utf-8") as fout:
|
||||
for line in json_lines:
|
||||
fout.write(line + "\n")
|
||||
|
||||
vocab = sorted(vocab_count.items(), key=lambda k: k[1], reverse=True)
|
||||
vocab_file = os.path.join(dst_folder, "vocab.txt")
|
||||
with open(vocab_file, "w", encoding="utf-8") as f:
|
||||
for v, c in vocab:
|
||||
f.write(v + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
data_root = args.data_root
|
||||
data_set = "data_aishell"
|
||||
logging.info("\n\nWorking on: {0}".format(data_set))
|
||||
file_path = os.path.join(data_root, data_set + ".tgz")
|
||||
logging.info("Getting {0}".format(data_set))
|
||||
__maybe_download_file(file_path, data_set)
|
||||
logging.info("Extracting {0}".format(data_set))
|
||||
data_folder = os.path.join(data_root, data_set)
|
||||
__extract_all_files(file_path, data_root, data_folder)
|
||||
logging.info("Processing {0}".format(data_set))
|
||||
__process_data(data_folder, data_folder)
|
||||
logging.info("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,229 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. 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.
|
||||
#
|
||||
|
||||
# Copyright (c) 2020, SeanNaren. 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.
|
||||
#
|
||||
|
||||
# To convert mp3 files to wav using sox, you must have installed sox with mp3 support
|
||||
# For example sudo apt-get install libsox-fmt-mp3
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
import tarfile
|
||||
import urllib.request
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.utils.tar_utils import safe_extract
|
||||
|
||||
parser = argparse.ArgumentParser(description='Downloads and processes Mozilla Common Voice dataset.')
|
||||
parser.add_argument("--data_root", default='CommonVoice_dataset/', type=str, help="Directory to store the dataset.")
|
||||
parser.add_argument('--manifest_dir', default='./', type=str, help='Output directory for manifests')
|
||||
parser.add_argument("--num_workers", default=multiprocessing.cpu_count(), type=int, help="Workers to process dataset.")
|
||||
parser.add_argument('--sample_rate', default=16000, type=int, help='Sample rate')
|
||||
parser.add_argument('--n_channels', default=1, type=int, help='Number of channels for output wav files')
|
||||
parser.add_argument("--log", dest="log", action="store_true", default=False)
|
||||
parser.add_argument("--cleanup", dest="cleanup", action="store_true", default=False)
|
||||
parser.add_argument(
|
||||
'--files_to_process',
|
||||
nargs='+',
|
||||
default=['test.tsv', 'dev.tsv', 'train.tsv'],
|
||||
type=str,
|
||||
help='list of *.csv file names to process',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
default='cv-corpus-5.1-2020-06-22',
|
||||
type=str,
|
||||
help='Version of the dataset (obtainable via https://commonvoice.mozilla.org/en/datasets',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--language',
|
||||
default='en',
|
||||
type=str,
|
||||
help='Which language to download.(default english,'
|
||||
'check https://commonvoice.mozilla.org/en/datasets for more language codes',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
COMMON_VOICE_URL = (
|
||||
f"https://voice-prod-bundler-ee1969a6ce8178826482b88e843c335139bd3fb4.s3.amazonaws.com/"
|
||||
"{}/{}.tar.gz".format(args.version, args.language)
|
||||
)
|
||||
COMMON_VOICE_USER_AGENT = (
|
||||
'Mozilla/5.0 (Windows NT 10.0; WOW64) ' 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'
|
||||
)
|
||||
|
||||
|
||||
def _load_sox():
|
||||
try:
|
||||
import sox
|
||||
from sox import Transformer
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Optional dependency 'sox' is required by this script. Install it with: pip install sox"
|
||||
) from None
|
||||
|
||||
return sox, Transformer
|
||||
|
||||
|
||||
def download_commonvoice_archive(url: str, output_path: str):
|
||||
request = urllib.request.Request(url, headers={'User-Agent': COMMON_VOICE_USER_AGENT})
|
||||
with urllib.request.urlopen(request) as response, open(output_path, 'wb') as f:
|
||||
while True:
|
||||
chunk = response.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
def create_manifest(data: List[tuple], output_name: str, manifest_path: str):
|
||||
output_file = Path(manifest_path) / output_name
|
||||
output_file.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
with output_file.open(mode='w') as f:
|
||||
for wav_path, duration, text in tqdm(data, total=len(data)):
|
||||
if wav_path != '':
|
||||
# skip invalid input files that could not be converted
|
||||
f.write(
|
||||
json.dumps({'audio_filepath': os.path.abspath(wav_path), "duration": duration, 'text': text})
|
||||
+ '\n'
|
||||
)
|
||||
|
||||
|
||||
def process_files(csv_file, data_root, num_workers):
|
||||
"""Read *.csv file description, convert mp3 to wav, process text.
|
||||
Save results to data_root.
|
||||
|
||||
Args:
|
||||
csv_file: str, path to *.csv file with data description, usually start from 'cv-'
|
||||
data_root: str, path to dir to save results; wav/ dir will be created
|
||||
"""
|
||||
sox, Transformer = _load_sox()
|
||||
wav_dir = os.path.join(data_root, 'wav/')
|
||||
os.makedirs(wav_dir, exist_ok=True)
|
||||
audio_clips_path = os.path.dirname(csv_file) + '/clips/'
|
||||
|
||||
def process(x):
|
||||
file_path, text = x
|
||||
file_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||
text = text.lower().strip()
|
||||
audio_path = os.path.join(audio_clips_path, file_path)
|
||||
if os.path.getsize(audio_path) == 0:
|
||||
logging.warning(f'Skipping empty audio file {audio_path}')
|
||||
return '', '', ''
|
||||
|
||||
output_wav_path = os.path.join(wav_dir, file_name + '.wav')
|
||||
|
||||
if not os.path.exists(output_wav_path):
|
||||
tfm = Transformer()
|
||||
tfm.rate(samplerate=args.sample_rate)
|
||||
tfm.channels(n_channels=args.n_channels)
|
||||
tfm.build(input_filepath=audio_path, output_filepath=output_wav_path)
|
||||
|
||||
duration = sox.file_info.duration(output_wav_path)
|
||||
return output_wav_path, duration, text
|
||||
|
||||
logging.info('Converting mp3 to wav for {}.'.format(csv_file))
|
||||
with open(csv_file) as csvfile:
|
||||
reader = csv.DictReader(csvfile, delimiter='\t')
|
||||
next(reader, None) # skip the headers
|
||||
data = []
|
||||
for row in reader:
|
||||
file_name = row['path']
|
||||
# add the mp3 extension if the tsv entry does not have it
|
||||
if not file_name.endswith('.mp3'):
|
||||
file_name += '.mp3'
|
||||
data.append((file_name, row['sentence']))
|
||||
with ThreadPool(num_workers) as pool:
|
||||
data = list(tqdm(pool.imap(process, data), total=len(data)))
|
||||
return data
|
||||
|
||||
|
||||
def main():
|
||||
if args.log:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
data_root = args.data_root
|
||||
os.makedirs(data_root, exist_ok=True)
|
||||
|
||||
target_unpacked_dir = os.path.join(data_root, "CV_unpacked")
|
||||
|
||||
if os.path.exists(target_unpacked_dir):
|
||||
logging.info('Find existing folder {}'.format(target_unpacked_dir))
|
||||
else:
|
||||
logging.info("Could not find Common Voice, Downloading corpus...")
|
||||
|
||||
# some dataset versions are packaged in different named files, so forcing
|
||||
output_archive_filename = args.language + '.tar.gz'
|
||||
output_archive_filename = os.path.join(data_root, output_archive_filename)
|
||||
|
||||
download_commonvoice_archive(COMMON_VOICE_URL, output_archive_filename)
|
||||
filename = f"{args.language}.tar.gz"
|
||||
target_file = os.path.join(data_root, os.path.basename(filename))
|
||||
|
||||
os.makedirs(target_unpacked_dir, exist_ok=True)
|
||||
logging.info("Unpacking corpus to {} ...".format(target_unpacked_dir))
|
||||
with tarfile.open(target_file) as tar:
|
||||
safe_extract(tar, target_unpacked_dir)
|
||||
if args.cleanup:
|
||||
logging.info("removing tar archive to save space")
|
||||
os.remove(target_file)
|
||||
|
||||
folder_path = os.path.join(target_unpacked_dir, args.version + f'/{args.language}/')
|
||||
if not os.path.isdir(folder_path):
|
||||
# try without language
|
||||
folder_path = os.path.join(target_unpacked_dir, args.version)
|
||||
if not os.path.isdir(folder_path):
|
||||
# try without version
|
||||
folder_path = target_unpacked_dir
|
||||
if not os.path.isdir(folder_path):
|
||||
logging.error(f'unable to locate unpacked files in {folder_path}')
|
||||
sys.exit()
|
||||
|
||||
for csv_file in args.files_to_process:
|
||||
data = process_files(
|
||||
csv_file=os.path.join(folder_path, csv_file),
|
||||
data_root=os.path.join(data_root, os.path.splitext(csv_file)[0]),
|
||||
num_workers=args.num_workers,
|
||||
)
|
||||
logging.info('Creating manifests...')
|
||||
create_manifest(
|
||||
data=data,
|
||||
output_name=f'commonvoice_{os.path.splitext(csv_file)[0]}_manifest.json',
|
||||
manifest_path=args.manifest_dir,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,138 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. 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.
|
||||
|
||||
# USAGE: python get_demand_data.py --data_root=<where to put data>
|
||||
# --data_set=<datasets_to_download>
|
||||
# where <datasets_to_download> can be: one or more of the 16 kHz noise profiles
|
||||
# listed at https://zenodo.org/record/1227121#.Ygb4avXMKJk ,
|
||||
# or ALL
|
||||
# You can put more than one data_set comma-separated:
|
||||
# --data_sets=DKITCHEN,DLIVING,NRIVER
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import urllib.request
|
||||
|
||||
parser = argparse.ArgumentParser(description='LibriSpeech Data download')
|
||||
parser.add_argument("--data_root", required=True, default=None, type=str)
|
||||
parser.add_argument("--data_sets", default="ALL", type=str)
|
||||
|
||||
parser.add_argument('--log', dest='log', action='store_true', default=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
URLS = {
|
||||
'DKITCHEN': ("https://zenodo.org/record/1227121/files/DKITCHEN_16k.zip"),
|
||||
'DLIVING': ("https://zenodo.org/record/1227121/files/DLIVING_16k.zip"),
|
||||
'DWASHING': ("https://zenodo.org/record/1227121/files/DWASHING_16k.zip"),
|
||||
'NFIELD': ("https://zenodo.org/record/1227121/files/NFIELD_16k.zip"),
|
||||
'NPARK': ("https://zenodo.org/record/1227121/files/NPARK_16k.zip"),
|
||||
'NRIVER': ("https://zenodo.org/record/1227121/files/NRIVER_16k.zip"),
|
||||
'OHALLWAY': ("https://zenodo.org/record/1227121/files/OHALLWAY_16k.zip"),
|
||||
'OMEETING': ("https://zenodo.org/record/1227121/files/OMEETING_16k.zip"),
|
||||
'OOFFICE': ("https://zenodo.org/record/1227121/files/OOFFICE_16k.zip"),
|
||||
'PCAFETER': ("https://zenodo.org/record/1227121/files/PCAFETER_16k.zip"),
|
||||
'PRESTO': ("https://zenodo.org/record/1227121/files/PRESTO_16k.zip"),
|
||||
'PSTATION': ("https://zenodo.org/record/1227121/files/PSTATION_16k.zip"),
|
||||
'SPSQUARE': ("https://zenodo.org/record/1227121/files/SPSQUARE_16k.zip"),
|
||||
'STRAFFIC': ("https://zenodo.org/record/1227121/files/STRAFFIC_16k.zip"),
|
||||
'TBUS': ("https://zenodo.org/record/1227121/files/TBUS_16k.zip"),
|
||||
'TCAR': ("https://zenodo.org/record/1227121/files/TCAR_16k.zip"),
|
||||
'TMETRO': ("https://zenodo.org/record/1227121/files/TMETRO_16k.zip"),
|
||||
}
|
||||
|
||||
|
||||
def __maybe_download_file(destination: str, source: str):
|
||||
"""
|
||||
Downloads source to destination if it doesn't exist.
|
||||
If exists, skips download
|
||||
Args:
|
||||
destination: local filepath
|
||||
source: url of resource
|
||||
Returns:
|
||||
"""
|
||||
source = URLS[source]
|
||||
if not os.path.exists(destination):
|
||||
logging.info("{0} does not exist. Downloading ...".format(destination))
|
||||
urllib.request.urlretrieve(source, filename=destination + '.tmp')
|
||||
os.rename(destination + '.tmp', destination)
|
||||
logging.info("Downloaded {0}.".format(destination))
|
||||
else:
|
||||
logging.info("Destination {0} exists. Skipping.".format(destination))
|
||||
return destination
|
||||
|
||||
|
||||
def __extract_file(filepath: str, data_dir: str):
|
||||
shutil.unpack_archive(filepath, data_dir)
|
||||
|
||||
|
||||
def __create_manifest(dst_folder: str):
|
||||
"""
|
||||
Create manifests for the noise files
|
||||
Args:
|
||||
file_path: path to a source transcript with flac sources
|
||||
dst_folder: path where manifests will be created
|
||||
Returns:
|
||||
|
||||
a list of metadata entries for processed files.
|
||||
"""
|
||||
# Read directory
|
||||
# Get all wav file names
|
||||
# create line per wav file in manifest
|
||||
noise_name = os.path.basename(dst_folder)
|
||||
wav_files = glob.glob(dst_folder + "/*.wav")
|
||||
wav_files.sort()
|
||||
os.makedirs(os.path.join(os.path.dirname(dst_folder), "manifests"), exist_ok=True)
|
||||
with open(os.path.join(os.path.dirname(dst_folder), "manifests", noise_name + ".json"), "w") as mfst_f:
|
||||
for wav_f in wav_files:
|
||||
dur = subprocess.check_output(["soxi", "-D", wav_f])
|
||||
row = {"audio_filepath": wav_f, "text": "", "duration": float(dur)}
|
||||
mfst_f.write(json.dumps(row) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
data_root = args.data_root
|
||||
data_sets = args.data_sets
|
||||
|
||||
if args.log:
|
||||
print("here")
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
if not os.path.exists(data_root):
|
||||
os.makedirs(data_root)
|
||||
|
||||
if data_sets == "ALL":
|
||||
data_sets = URLS.keys()
|
||||
else:
|
||||
data_sets = data_sets.split(',')
|
||||
|
||||
for data_set in data_sets:
|
||||
if data_set not in URLS.keys():
|
||||
raise ValueError(f"{data_sets} is not part of demand noise database")
|
||||
logging.info("\n\nWorking on: {0}".format(data_set))
|
||||
filepath = os.path.join(data_root, data_set + "_16k.zip")
|
||||
logging.info("Getting {0}".format(data_set))
|
||||
__maybe_download_file(filepath, data_set.upper())
|
||||
logging.info("Extracting {0}".format(data_set))
|
||||
__extract_file(filepath, data_root)
|
||||
logging.info("Processing {0}".format(data_set))
|
||||
__create_manifest(os.path.join(data_root, data_set))
|
||||
logging.info('Done!')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,221 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. 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.
|
||||
#
|
||||
# USAGE: python get_librispeech_data.py --data_root=<where to put data>
|
||||
# --data_set=<datasets_to_download> --num_workers=<number of parallel workers>
|
||||
# where <datasets_to_download> can be: dev_clean, dev_other, test_clean,
|
||||
# test_other, train_clean_100, train_clean_360, train_other_500 or ALL
|
||||
# You can also put more than one data_set comma-separated:
|
||||
# --data_set=dev_clean,train_clean_100
|
||||
import argparse
|
||||
import fnmatch
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import subprocess
|
||||
import tarfile
|
||||
import urllib.request
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.utils.tar_utils import safe_extract
|
||||
|
||||
parser = argparse.ArgumentParser(description="LibriSpeech Data download")
|
||||
parser.add_argument("--data_root", required=True, default=None, type=str)
|
||||
parser.add_argument("--data_sets", default="dev_clean", type=str)
|
||||
parser.add_argument("--num_workers", default=4, type=int)
|
||||
parser.add_argument("--log", dest="log", action="store_true", default=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
URLS = {
|
||||
"TRAIN_CLEAN_100": ("http://www.openslr.org/resources/12/train-clean-100.tar.gz"),
|
||||
"TRAIN_CLEAN_360": ("http://www.openslr.org/resources/12/train-clean-360.tar.gz"),
|
||||
"TRAIN_OTHER_500": ("http://www.openslr.org/resources/12/train-other-500.tar.gz"),
|
||||
"DEV_CLEAN": "http://www.openslr.org/resources/12/dev-clean.tar.gz",
|
||||
"DEV_OTHER": "http://www.openslr.org/resources/12/dev-other.tar.gz",
|
||||
"TEST_CLEAN": "http://www.openslr.org/resources/12/test-clean.tar.gz",
|
||||
"TEST_OTHER": "http://www.openslr.org/resources/12/test-other.tar.gz",
|
||||
"DEV_CLEAN_2": "https://www.openslr.org/resources/31/dev-clean-2.tar.gz",
|
||||
"TRAIN_CLEAN_5": "https://www.openslr.org/resources/31/train-clean-5.tar.gz",
|
||||
}
|
||||
|
||||
|
||||
def _load_sox_transformer():
|
||||
try:
|
||||
from sox import Transformer
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Optional dependency 'sox' is required by this script. Install it with: pip install sox"
|
||||
) from None
|
||||
|
||||
return Transformer
|
||||
|
||||
|
||||
def __retrieve_with_progress(source: str, filename: str):
|
||||
"""
|
||||
Downloads source to destination
|
||||
Displays progress bar
|
||||
Args:
|
||||
source: url of resource
|
||||
destination: local filepath
|
||||
Returns:
|
||||
"""
|
||||
with open(filename, "wb") as f:
|
||||
response = urllib.request.urlopen(source)
|
||||
total = response.length
|
||||
|
||||
if total is None:
|
||||
f.write(response.content)
|
||||
else:
|
||||
with tqdm(total=total, unit="B", unit_scale=True, unit_divisor=1024) as pbar:
|
||||
for data in response:
|
||||
f.write(data)
|
||||
pbar.update(len(data))
|
||||
|
||||
|
||||
def __maybe_download_file(destination: str, source: str):
|
||||
"""
|
||||
Downloads source to destination if it doesn't exist.
|
||||
If exists, skips download
|
||||
Args:
|
||||
destination: local filepath
|
||||
source: url of resource
|
||||
Returns:
|
||||
"""
|
||||
source = URLS[source]
|
||||
if not os.path.exists(destination):
|
||||
logging.info("{0} does not exist. Downloading ...".format(destination))
|
||||
|
||||
__retrieve_with_progress(source, filename=destination + ".tmp")
|
||||
|
||||
os.rename(destination + ".tmp", destination)
|
||||
logging.info("Downloaded {0}.".format(destination))
|
||||
else:
|
||||
logging.info("Destination {0} exists. Skipping.".format(destination))
|
||||
return destination
|
||||
|
||||
|
||||
def __extract_file(filepath: str, data_dir: str):
|
||||
try:
|
||||
with tarfile.open(filepath) as tar:
|
||||
safe_extract(tar, data_dir)
|
||||
except Exception:
|
||||
logging.info("Not extracting. Maybe already there?")
|
||||
|
||||
|
||||
def __process_transcript(file_path: str, dst_folder: str):
|
||||
"""
|
||||
Converts flac files to wav from a given transcript, capturing the metadata.
|
||||
Args:
|
||||
file_path: path to a source transcript with flac sources
|
||||
dst_folder: path where wav files will be stored
|
||||
Returns:
|
||||
a list of metadata entries for processed files.
|
||||
"""
|
||||
Transformer = _load_sox_transformer()
|
||||
entries = []
|
||||
root = os.path.dirname(file_path)
|
||||
with open(file_path, encoding="utf-8") as fin:
|
||||
for line in fin:
|
||||
id, text = line[: line.index(" ")], line[line.index(" ") + 1 :]
|
||||
transcript_text = text.lower().strip()
|
||||
|
||||
# Convert FLAC file to WAV
|
||||
flac_file = os.path.join(root, id + ".flac")
|
||||
wav_file = os.path.join(dst_folder, id + ".wav")
|
||||
if not os.path.exists(wav_file):
|
||||
Transformer().build(flac_file, wav_file)
|
||||
# check duration
|
||||
duration = subprocess.check_output(["soxi", "-D", wav_file])
|
||||
|
||||
entry = {}
|
||||
entry["audio_filepath"] = os.path.abspath(wav_file)
|
||||
entry["duration"] = float(duration)
|
||||
entry["text"] = transcript_text
|
||||
entries.append(entry)
|
||||
return entries
|
||||
|
||||
|
||||
def __process_data(data_folder: str, dst_folder: str, manifest_file: str, num_workers: int):
|
||||
"""
|
||||
Converts flac to wav and build manifests's json
|
||||
Args:
|
||||
data_folder: source with flac files
|
||||
dst_folder: where wav files will be stored
|
||||
manifest_file: where to store manifest
|
||||
num_workers: number of parallel workers processing files
|
||||
Returns:
|
||||
"""
|
||||
|
||||
if not os.path.exists(dst_folder):
|
||||
os.makedirs(dst_folder)
|
||||
|
||||
files = []
|
||||
entries = []
|
||||
|
||||
for root, dirnames, filenames in os.walk(data_folder):
|
||||
for filename in fnmatch.filter(filenames, "*.trans.txt"):
|
||||
files.append(os.path.join(root, filename))
|
||||
|
||||
with multiprocessing.Pool(num_workers) as p:
|
||||
processing_func = functools.partial(__process_transcript, dst_folder=dst_folder)
|
||||
results = p.imap(processing_func, files)
|
||||
for result in tqdm(results, total=len(files)):
|
||||
entries.extend(result)
|
||||
|
||||
with open(manifest_file, "w") as fout:
|
||||
for m in entries:
|
||||
fout.write(json.dumps(m) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
data_root = args.data_root
|
||||
data_sets = args.data_sets
|
||||
num_workers = args.num_workers
|
||||
|
||||
if args.log:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
if data_sets == "ALL":
|
||||
data_sets = "dev_clean,dev_other,train_clean_100,train_clean_360,train_other_500,test_clean,test_other"
|
||||
if data_sets == "mini":
|
||||
data_sets = "dev_clean_2,train_clean_5"
|
||||
for data_set in data_sets.split(","):
|
||||
logging.info("\n\nWorking on: {0}".format(data_set))
|
||||
filepath = os.path.join(data_root, data_set + ".tar.gz")
|
||||
logging.info("Getting {0}".format(data_set))
|
||||
__maybe_download_file(filepath, data_set.upper())
|
||||
logging.info("Extracting {0}".format(data_set))
|
||||
__extract_file(filepath, data_root)
|
||||
logging.info("Processing {0}".format(data_set))
|
||||
__process_data(
|
||||
os.path.join(
|
||||
os.path.join(data_root, "LibriSpeech"),
|
||||
data_set.replace("_", "-"),
|
||||
),
|
||||
os.path.join(
|
||||
os.path.join(data_root, "LibriSpeech"),
|
||||
data_set.replace("_", "-"),
|
||||
)
|
||||
+ "-processed",
|
||||
os.path.join(data_root, data_set + ".json"),
|
||||
num_workers=num_workers,
|
||||
)
|
||||
logging.info("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,165 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. 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.
|
||||
|
||||
# USAGE: python get_openslr_rir_data.py --data_root=<where to put data>
|
||||
# Data is downloaded from OpenSLR's "Room Impulse Response and Noise Database"
|
||||
# RIRs in multichannel files are separated into single channel files and
|
||||
# a json file that can be used as in input to NeMo is created
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import urllib.request
|
||||
from shutil import copy, move
|
||||
from zipfile import ZipFile
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
parser = argparse.ArgumentParser(description="OpenSLR RIR Data download and process")
|
||||
parser.add_argument("--data_root", required=True, default=None, type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
URLS = {
|
||||
"SLR28": ("http://www.openslr.org/resources/28/rirs_noises.zip"),
|
||||
}
|
||||
|
||||
|
||||
def __retrieve_with_progress(source: str, filename: str):
|
||||
"""
|
||||
Downloads source to destination
|
||||
Displays progress bar
|
||||
Args:
|
||||
source: url of resource
|
||||
destination: local filepath
|
||||
Returns:
|
||||
"""
|
||||
with open(filename, "wb") as f:
|
||||
response = urllib.request.urlopen(source)
|
||||
total = response.length
|
||||
|
||||
if total is None:
|
||||
f.write(response.content)
|
||||
else:
|
||||
with tqdm(total=total, unit="B", unit_scale=True, unit_divisor=1024) as pbar:
|
||||
for data in response:
|
||||
f.write(data)
|
||||
pbar.update(len(data))
|
||||
|
||||
|
||||
def __maybe_download_file(destination: str, source: str):
|
||||
"""
|
||||
Downloads source to destination if it doesn't exist.
|
||||
If exists, skips download
|
||||
Args:
|
||||
destination: local filepath
|
||||
source: url of resource
|
||||
Returns:
|
||||
"""
|
||||
source = URLS[source]
|
||||
if not os.path.exists(destination):
|
||||
logging.info("{0} does not exist. Downloading ...".format(destination))
|
||||
__retrieve_with_progress(source, filename=destination + ".tmp")
|
||||
os.rename(destination + ".tmp", destination)
|
||||
logging.info("Downloaded {0}.".format(destination))
|
||||
else:
|
||||
logging.info("Destination {0} exists. Skipping.".format(destination))
|
||||
return destination
|
||||
|
||||
|
||||
def __extract_file(filepath: str, data_dir: str):
|
||||
try:
|
||||
with ZipFile(filepath, "r") as zipObj:
|
||||
zipObj.extractall(data_dir)
|
||||
except Exception:
|
||||
logging.info("Not extracting. Maybe already there?")
|
||||
|
||||
|
||||
def __process_data(data_folder: str, dst_folder: str, manifest_file: str):
|
||||
"""
|
||||
Converts flac to wav and build manifests's json
|
||||
Args:
|
||||
data_folder: source with flac files
|
||||
dst_folder: where wav files will be stored
|
||||
manifest_file: where to store manifest
|
||||
Returns:
|
||||
"""
|
||||
if not os.path.exists(dst_folder):
|
||||
os.makedirs(dst_folder)
|
||||
|
||||
real_rir_list = os.path.join(data_folder, "RIRS_NOISES", "real_rirs_isotropic_noises", "rir_list")
|
||||
rirfiles = []
|
||||
with open(real_rir_list, "r") as rir_f:
|
||||
for line in rir_f:
|
||||
rirfiles.append(os.path.join(data_folder, line.rstrip().split(" ")[4]))
|
||||
|
||||
real_rir_folder = os.path.join(dst_folder, "real_rirs")
|
||||
if not os.path.exists(real_rir_folder):
|
||||
os.makedirs(real_rir_folder)
|
||||
# split multi-channel rir files to single channel
|
||||
for rir_f in rirfiles:
|
||||
n_chans = int(subprocess.check_output(["soxi", "-c", rir_f]))
|
||||
if n_chans == 1:
|
||||
copy(rir_f, real_rir_folder)
|
||||
else:
|
||||
for chan in range(1, n_chans + 1):
|
||||
chan_file_name = os.path.join(
|
||||
real_rir_folder,
|
||||
os.path.splitext(os.path.basename(rir_f))[0] + "-" + str(chan) + ".wav",
|
||||
)
|
||||
_ = subprocess.check_output(["sox", rir_f, chan_file_name, "remix", str(chan)])
|
||||
|
||||
# move simulated rirs to processed
|
||||
if not os.path.exists(os.path.join(dst_folder, "simulated_rirs")):
|
||||
move(os.path.join(data_folder, "RIRS_NOISES", "simulated_rirs"), dst_folder)
|
||||
|
||||
os.chdir(dst_folder)
|
||||
all_rirs = glob.glob("**/*.wav", recursive=True)
|
||||
with open(manifest_file, "w") as man_f:
|
||||
entry = {}
|
||||
for rir in all_rirs:
|
||||
rir_file = os.path.join(dst_folder, rir)
|
||||
duration = subprocess.check_output(["soxi", "-D", rir_file])
|
||||
entry["audio_filepath"] = rir_file
|
||||
entry["duration"] = float(duration)
|
||||
entry["offset"] = 0
|
||||
entry["text"] = "_"
|
||||
man_f.write(json.dumps(entry) + "\n")
|
||||
|
||||
print("Done!")
|
||||
|
||||
|
||||
def main():
|
||||
data_root = os.path.abspath(args.data_root)
|
||||
data_set = "slr28"
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
logging.info("\n\nWorking on: {0}".format(data_set))
|
||||
filepath = os.path.join(data_root, data_set + ".zip")
|
||||
logging.info("Getting {0}".format(data_set))
|
||||
__maybe_download_file(filepath, data_set.upper())
|
||||
logging.info("Extracting {0}".format(data_set))
|
||||
__extract_file(filepath, data_root)
|
||||
logging.info("Processing {0}".format(data_set))
|
||||
__process_data(
|
||||
data_root,
|
||||
os.path.join(os.path.join(data_root, "processed")),
|
||||
os.path.join(os.path.join(data_root, "processed", "rir.json")),
|
||||
)
|
||||
logging.info("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,135 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. 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
|
||||
import json
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from nemo.utils import logging
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Convert kaldi data folder to manifest.json")
|
||||
parser.add_argument(
|
||||
"--data_dir",
|
||||
required=True,
|
||||
type=str,
|
||||
help="data in kaldi format",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifest",
|
||||
required=True,
|
||||
type=str,
|
||||
help="path to store the manifest file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--with_aux_data",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="whether to include auxiliary data in the manifest",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
kaldi_folder = args.data_dir
|
||||
required_data = {
|
||||
"audio_filepath": os.path.join(kaldi_folder, "wav.scp"),
|
||||
"duration": os.path.join(kaldi_folder, "segments"),
|
||||
"text": os.path.join(kaldi_folder, "text"),
|
||||
}
|
||||
aux_data = {
|
||||
"speaker": os.path.join(kaldi_folder, "utt2spk"),
|
||||
"gender": os.path.join(kaldi_folder, "utt2gender"),
|
||||
}
|
||||
output_names = list(required_data.keys())
|
||||
|
||||
# check if required files exist
|
||||
for name, file in required_data.items():
|
||||
if not os.path.exists(file):
|
||||
raise ValueError(f"{os.path.basename(file)} is not in {kaldi_folder}.")
|
||||
|
||||
# read wav.scp
|
||||
wavscp = pd.read_csv(required_data["audio_filepath"], sep=" ", header=None)
|
||||
if wavscp.shape[1] > 2:
|
||||
logging.warning(
|
||||
f"""More than two columns in 'wav.scp': {wavscp.shape[1]}.
|
||||
Maybe it contains pipes? Pipe processing can be slow at runtime."""
|
||||
)
|
||||
wavscp = pd.read_csv(
|
||||
required_data["audio_filepath"],
|
||||
sep="^([^ ]+) ",
|
||||
engine="python",
|
||||
header=None,
|
||||
usecols=[1, 2],
|
||||
names=["wav_label", "audio_filepath"],
|
||||
)
|
||||
else:
|
||||
wavscp = wavscp.rename(columns={0: "wav_label", 1: "audio_filepath"})
|
||||
|
||||
# read text
|
||||
text = pd.read_csv(
|
||||
required_data["text"],
|
||||
sep="^([^ ]+) ",
|
||||
engine="python",
|
||||
header=None,
|
||||
usecols=[1, 2],
|
||||
names=["label", "text"],
|
||||
)
|
||||
|
||||
# read segments
|
||||
segments = pd.read_csv(
|
||||
required_data["duration"],
|
||||
sep=" ",
|
||||
header=None,
|
||||
names=["label", "wav_label", "offset", "end"],
|
||||
)
|
||||
# add offset if needed
|
||||
if len(segments.offset) > len(segments.offset[segments.offset == 0.0]):
|
||||
logging.info("Adding offset field.")
|
||||
output_names.insert(2, "offset")
|
||||
segments["duration"] = (segments.end - segments.offset).round(decimals=3)
|
||||
|
||||
# merge data
|
||||
wav_segments_text = pd.merge(
|
||||
pd.merge(segments, wavscp, how="inner", on="wav_label"),
|
||||
text,
|
||||
how="inner",
|
||||
on="label",
|
||||
)
|
||||
|
||||
if args.with_aux_data:
|
||||
# check if auxiliary data is present
|
||||
for name, aux_file in aux_data.items():
|
||||
if os.path.exists(aux_file):
|
||||
logging.info(f"Adding info from '{os.path.basename(aux_file)}'.")
|
||||
wav_segments_text = pd.merge(
|
||||
wav_segments_text,
|
||||
pd.read_csv(aux_file, sep=" ", header=None, names=["label", name]),
|
||||
how="left",
|
||||
on="label",
|
||||
)
|
||||
output_names.append(name)
|
||||
else:
|
||||
logging.info(f"'{os.path.basename(aux_file)}' does not exist. Skipping ...")
|
||||
|
||||
# write data to .json
|
||||
entries = wav_segments_text[output_names].to_dict(orient="records")
|
||||
with open(args.manifest, "w", encoding="utf-8") as fout:
|
||||
for m in entries:
|
||||
fout.write(json.dumps(m, ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,109 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. 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.
|
||||
|
||||
# USAGE: python process_aishell2_data.py
|
||||
# --audio_folder=<source data>
|
||||
# --dest_folder=<where to store the results>
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
parser = argparse.ArgumentParser(description="Processing Aishell2 Data")
|
||||
parser.add_argument("--audio_folder", default=None, type=str, required=True, help="Audio (wav) data directory.")
|
||||
parser.add_argument("--dest_folder", default=None, type=str, required=True, help="Destination directory.")
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def __process_data(data_folder: str, dst_folder: str):
|
||||
"""
|
||||
To generate manifest
|
||||
Args:
|
||||
data_folder: source with wav files
|
||||
dst_folder: where manifest files will be stored
|
||||
Returns:
|
||||
"""
|
||||
if not os.path.exists(dst_folder):
|
||||
os.makedirs(dst_folder)
|
||||
data_type = ['dev', 'test', 'train']
|
||||
for data in data_type:
|
||||
dst_file = os.path.join(dst_folder, data + ".json")
|
||||
uttrances = []
|
||||
wav_dir = os.path.join(data_folder, "wav", data)
|
||||
transcript_file = os.path.join(data_folder, "transcript", data, "trans.txt")
|
||||
trans_text = {}
|
||||
with open(transcript_file, "r", encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip().split()
|
||||
utterance_id, text = line[0], " ".join(line[1:])
|
||||
trans_text[utterance_id] = text.upper()
|
||||
session_list = os.listdir(wav_dir)
|
||||
for sessions in session_list:
|
||||
cur_dir = os.path.join(wav_dir, sessions)
|
||||
for wavs in os.listdir(cur_dir):
|
||||
audio_id = wavs.strip(".wav")
|
||||
audio_filepath = os.path.abspath(os.path.join(cur_dir, wavs))
|
||||
duration = subprocess.check_output(["soxi", "-D", audio_filepath])
|
||||
duration = float(duration)
|
||||
text = trans_text[audio_id]
|
||||
uttrances.append(
|
||||
json.dumps(
|
||||
{"audio_filepath": audio_filepath, "duration": duration, "text": text}, ensure_ascii=False
|
||||
)
|
||||
)
|
||||
with open(dst_file, "w") as f:
|
||||
for line in uttrances:
|
||||
f.write(line + "\n")
|
||||
|
||||
|
||||
def __get_vocab(data_folder: str, des_dir: str):
|
||||
"""
|
||||
To generate the vocabulary file
|
||||
Args:
|
||||
data_folder: source with the transcript file
|
||||
dst_folder: where the file will be stored
|
||||
Returns:
|
||||
"""
|
||||
if not os.path.exists(des_dir):
|
||||
os.makedirs(des_dir)
|
||||
trans_file = os.path.join(data_folder, "transcript", "train", "trans.txt")
|
||||
vocab_dict = {}
|
||||
with open(trans_file, "r", encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip().split()
|
||||
text = " ".join(line[1:])
|
||||
for i in text.upper():
|
||||
if i in vocab_dict:
|
||||
vocab_dict[i] += 1
|
||||
else:
|
||||
vocab_dict[i] = 1
|
||||
vocab_dict = sorted(vocab_dict.items(), key=lambda k: k[1], reverse=True)
|
||||
vocab = os.path.join(des_dir, "vocab.txt")
|
||||
vocab = open(vocab, "w", encoding='utf-8')
|
||||
for k in vocab_dict:
|
||||
vocab.write(k[0] + "\n")
|
||||
vocab.close()
|
||||
|
||||
|
||||
def main():
|
||||
source_data = args.audio_folder
|
||||
des_dir = args.dest_folder
|
||||
print("begin to process data...")
|
||||
__process_data(source_data, des_dir)
|
||||
__get_vocab(source_data, des_dir)
|
||||
print("finish all!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,89 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. 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
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import librosa
|
||||
|
||||
parser = argparse.ArgumentParser(description="AN4 dataset download and processing")
|
||||
parser.add_argument("--data_root", required=True, default=None, type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def build_manifest(data_root, transcripts_path, manifest_path, wav_path):
|
||||
with open(transcripts_path, 'r') as fin:
|
||||
with open(manifest_path, 'w') as fout:
|
||||
for line in fin:
|
||||
# Lines look like this:
|
||||
# <s> transcript </s> (fileID)
|
||||
transcript = line[: line.find('(') - 1].lower()
|
||||
transcript = transcript.replace('<s>', '').replace('</s>', '')
|
||||
transcript = transcript.strip()
|
||||
|
||||
file_id = line[line.find('(') + 1 : -2] # e.g. "cen4-fash-b"
|
||||
audio_path = os.path.join(
|
||||
data_root,
|
||||
wav_path,
|
||||
file_id[file_id.find('-') + 1 : file_id.rfind('-')],
|
||||
file_id + '.wav',
|
||||
)
|
||||
|
||||
duration = librosa.core.get_duration(filename=audio_path)
|
||||
|
||||
# Write the metadata to the manifest
|
||||
metadata = {
|
||||
"audio_filepath": audio_path,
|
||||
"duration": duration,
|
||||
"text": transcript,
|
||||
}
|
||||
json.dump(metadata, fout)
|
||||
fout.write('\n')
|
||||
|
||||
|
||||
def main():
|
||||
data_root = os.path.abspath(args.data_root)
|
||||
|
||||
# Convert from .sph to .wav
|
||||
logging.info("Converting audio files to .wav...")
|
||||
sph_list = glob.glob(os.path.join(data_root, 'an4/**/*.sph'), recursive=True)
|
||||
for sph_path in sph_list:
|
||||
wav_path = sph_path[:-4] + '.wav'
|
||||
cmd = ['sox', sph_path, wav_path]
|
||||
subprocess.run(cmd)
|
||||
logging.info("Finished conversion.")
|
||||
|
||||
# Build manifests
|
||||
logging.info("Building training manifest...")
|
||||
train_transcripts = os.path.join(data_root, 'an4/etc/an4_train.transcription')
|
||||
train_manifest = os.path.join(data_root, 'an4/train_manifest.json')
|
||||
train_wavs = os.path.join(data_root, 'an4/wav/an4_clstk')
|
||||
build_manifest(data_root, train_transcripts, train_manifest, train_wavs)
|
||||
logging.info("Training manifests created.")
|
||||
|
||||
logging.info("Building test manifest...")
|
||||
test_transcripts = os.path.join(data_root, 'an4/etc/an4_test.transcription')
|
||||
test_manifest = os.path.join(data_root, 'an4/test_manifest.json')
|
||||
test_wavs = os.path.join(data_root, 'an4/wav/an4test_clstk')
|
||||
build_manifest(data_root, test_transcripts, test_manifest, test_wavs)
|
||||
logging.info("Test manifest created.")
|
||||
|
||||
logging.info("Done with AN4 processing!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,434 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. 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.
|
||||
|
||||
# USAGE:
|
||||
# python process_fisher_data.py \
|
||||
# --audio_root=<audio (.wav) directory>
|
||||
# --transcript_root=<LDC Fisher dataset directory> \
|
||||
# --dest_root=<destination directory> \
|
||||
# --data_sets=LDC2004S13-Part1,LDC2005S13-Part2 \
|
||||
# --remove_noises
|
||||
#
|
||||
# Matches Fisher dataset transcripts to the corresponding audio file (.wav),
|
||||
# and slices them into min_slice_duration segments with one speaker.
|
||||
# Also performs some other processing on transcripts.
|
||||
#
|
||||
# Heavily derived from Patter's Fisher processing script.
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from math import ceil, floor
|
||||
|
||||
import numpy as np
|
||||
import scipy.io.wavfile as wavfile
|
||||
from tqdm import tqdm
|
||||
|
||||
parser = argparse.ArgumentParser(description="Fisher Data Processing")
|
||||
parser.add_argument(
|
||||
"--audio_root",
|
||||
default=None,
|
||||
type=str,
|
||||
required=True,
|
||||
help="The path to the root of the audio (wav) data folder.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--transcript_root",
|
||||
default=None,
|
||||
type=str,
|
||||
required=True,
|
||||
help="The path to the root of the transcript data folder.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dest_root",
|
||||
default=None,
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to the destination root directory.",
|
||||
)
|
||||
|
||||
# Optional arguments
|
||||
parser.add_argument(
|
||||
"--min_slice_duration",
|
||||
default=10.0,
|
||||
type=float,
|
||||
help="Minimum audio slice duration after processing.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--keep_low_conf",
|
||||
action="store_true",
|
||||
help="Keep all utterances with low confidence transcripts",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--remove_noises",
|
||||
action="store_true",
|
||||
help="Removes transcripted noises such as [laughter].",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--noises_to_emoji",
|
||||
action="store_true",
|
||||
help="Converts transcripts for noises to an emoji character.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Total number of files before segmenting, and train/val/test splits
|
||||
NUM_FILES = 5850 + 5849
|
||||
TRAIN_END_IDX = int(NUM_FILES * 0.8)
|
||||
VAL_END_IDX = int(NUM_FILES * 0.9)
|
||||
|
||||
# Known transcription errors and their fixes (from Mozilla)
|
||||
TRANSCRIPT_BUGS = {
|
||||
"fe_03_00265-B-3353-3381": "correct",
|
||||
"fe_03_00991-B-52739-52829": "that's one of those",
|
||||
"fe_03_10282-A-34442-34484.wav": "they don't want",
|
||||
"fe_03_10677-B-10104-10641": "uh my mine yeah the german shepherd "
|
||||
+ "pitbull mix he snores almost as loud "
|
||||
+ "as i do",
|
||||
"fe_03_00027-B-39380-39405": None,
|
||||
"fe_03_11487-B-3109-23406": None,
|
||||
"fe_03_01326-A-30742-30793": None,
|
||||
}
|
||||
|
||||
TRANSCRIPT_NUMBERS = {
|
||||
"401k": "four o one k",
|
||||
"f16": "f sixteen",
|
||||
"m16": "m sixteen",
|
||||
"ak47": "a k forty seven",
|
||||
"v8": "v eight",
|
||||
"y2k": "y two k",
|
||||
"mp3": "m p three",
|
||||
"vh1": "v h one",
|
||||
"90210": "nine o two one o",
|
||||
"espn2": "e s p n two",
|
||||
"u2": "u two",
|
||||
"dc3s": "d c threes",
|
||||
"book 2": "book two",
|
||||
"s2b": "s two b",
|
||||
"3d": "three d",
|
||||
}
|
||||
|
||||
TAG_MAP = {
|
||||
"[laughter]": "🤣",
|
||||
"[laugh]": "🤣",
|
||||
"[noise]": "😕",
|
||||
"[sigh]": "😕",
|
||||
"[cough]": "😕",
|
||||
"[mn]": "😕",
|
||||
"[breath]": "😕",
|
||||
"[lipsmack]": "😕",
|
||||
"[[skip]]": "",
|
||||
"[pause]": "",
|
||||
"[sneeze]": "😕",
|
||||
}
|
||||
|
||||
|
||||
def __write_sample(dest, file_id, count, file_count, sample_rate, audio, duration, transcript):
|
||||
"""
|
||||
Writes one slice to the given target directory.
|
||||
Args:
|
||||
dest: the destination directory
|
||||
file_id: name of the transcript/audio file for this block
|
||||
count: the count of segments in the file so far
|
||||
file_count: the total number of filse processed so far
|
||||
sample rate: sample rate of the audio data
|
||||
audio: audio data of the current sample
|
||||
duration: audio duration of the current sample
|
||||
transcript: transcript of the current sample
|
||||
"""
|
||||
partition = __partition_name(file_count)
|
||||
audio_path = os.path.join(dest, partition, f"{file_id}_{count:03}.wav")
|
||||
|
||||
# Write audio
|
||||
wavfile.write(audio_path, sample_rate, audio)
|
||||
|
||||
# Write transcript info
|
||||
transcript = {
|
||||
"audio_filepath": audio_path,
|
||||
"duration": duration,
|
||||
"text": transcript,
|
||||
}
|
||||
|
||||
# Append to manifest
|
||||
manifest_path = os.path.join(dest, f"manifest_{partition}.json")
|
||||
with open(manifest_path, 'a') as f:
|
||||
json.dump(transcript, f)
|
||||
f.write('\n')
|
||||
|
||||
|
||||
def __normalize(utt):
|
||||
replace_table = str.maketrans(dict.fromkeys('()*;:"!&{},.-?'))
|
||||
utt = (
|
||||
utt.lower()
|
||||
.replace('[uh]', 'uh')
|
||||
.replace('[um]', 'um')
|
||||
.replace('<noise>', '[noise]')
|
||||
.replace('<spoken_noise>', '[vocalized-noise]')
|
||||
.replace('.period', 'period')
|
||||
.replace('.dot', 'dot')
|
||||
.replace('-hyphen', 'hyphen')
|
||||
.replace('._', ' ')
|
||||
.translate(replace_table)
|
||||
)
|
||||
utt = re.sub(r"'([a-z]+)'", r'\1', utt) # Unquote quoted words
|
||||
return utt
|
||||
|
||||
|
||||
def __process_utterance(file_id, trans_path, line, keep_low_conf, rem_noises, emojify):
|
||||
"""
|
||||
Processes one utterance (one line of a transcript).
|
||||
Args:
|
||||
file_id: the ID of the transcript file
|
||||
trans_path: transcript path
|
||||
line: one line in the transcript file
|
||||
keep_low_conf: whether to keep low confidence lines
|
||||
rem_noises: whether to remove noise symbols
|
||||
emojify: whether to convert noise symbols to emoji, lower precedence
|
||||
"""
|
||||
# Check for lines to skip (comments, empty, low confidence)
|
||||
if line.startswith('#') or not line.strip() or (not keep_low_conf and '((' in line):
|
||||
return None, None, None, None
|
||||
|
||||
# Data and sanity checks
|
||||
line = line.split()
|
||||
|
||||
t_start, t_end = float(line[0]), float(line[1])
|
||||
if (t_start < 0) or (t_end < t_start):
|
||||
print(f"Invalid time: {t_start} to {t_end} in {trans_path}")
|
||||
return None, None, None, None
|
||||
|
||||
channel = line[2]
|
||||
idx = 0 if line[2] == 'A:' else 1
|
||||
|
||||
if channel not in ('A:', 'B:'):
|
||||
print(f"Could not read channel info ({channel}) in {trans_path}")
|
||||
return None, None, None, None
|
||||
|
||||
# Replacements as necessary
|
||||
line_id = '-'.join([file_id, channel[0], str(t_start * 10), str(t_end * 10)])
|
||||
|
||||
content = TRANSCRIPT_BUGS.get(line_id, ' '.join(line[3:]))
|
||||
|
||||
if content is None:
|
||||
return None, None, None, None
|
||||
|
||||
for tag, newtag in TRANSCRIPT_NUMBERS.items():
|
||||
content = content.replace(tag, newtag)
|
||||
|
||||
content = __normalize(content)
|
||||
|
||||
if rem_noises:
|
||||
for k, _ in TAG_MAP.items():
|
||||
content = content.replace(k, '')
|
||||
elif emojify:
|
||||
for k, v in TAG_MAP.items():
|
||||
content = content.replace(k, v)
|
||||
|
||||
return t_start, t_end, idx, content
|
||||
|
||||
|
||||
def __process_one_file(
|
||||
trans_path,
|
||||
sample_rate,
|
||||
audio_data,
|
||||
file_id,
|
||||
dst_root,
|
||||
min_slice_duration,
|
||||
file_count,
|
||||
keep_low_conf,
|
||||
rem_noises,
|
||||
emojify,
|
||||
):
|
||||
"""
|
||||
Creates one block of audio slices and their corresponding transcripts.
|
||||
Args:
|
||||
trans_path: filepath to transcript
|
||||
sample_rate: sample rate of the audio
|
||||
audio_data: numpy array of shape [samples, channels]
|
||||
file_id: identifying label, e.g. 'fe_03_01102'
|
||||
dst_root: path to destination directory
|
||||
min_slice_duration: min number of seconds for an audio slice
|
||||
file_count: total number of files processed so far
|
||||
keep_low_conf: keep utterances with low-confidence transcripts
|
||||
rem_noises: remove noise symbols
|
||||
emojify: convert noise symbols into emoji characters
|
||||
"""
|
||||
count = 0
|
||||
|
||||
with open(trans_path, encoding="utf-8") as fin:
|
||||
fin.readline() # Comment w/ corresponding sph filename
|
||||
fin.readline() # Comment about transcriber
|
||||
|
||||
transcript_buffers = ['', ''] # [A buffer, B buffer]
|
||||
audio_buffers = [[], []]
|
||||
buffer_durations = [0.0, 0.0]
|
||||
|
||||
for line in fin:
|
||||
t_start, t_end, idx, content = __process_utterance(
|
||||
file_id, trans_path, line, keep_low_conf, rem_noises, emojify
|
||||
)
|
||||
|
||||
if content is None or not content:
|
||||
continue
|
||||
|
||||
duration = t_end - t_start
|
||||
|
||||
# Append utterance to buffer
|
||||
transcript_buffers[idx] += content
|
||||
audio_buffers[idx].append(
|
||||
audio_data[
|
||||
floor(t_start * sample_rate) : ceil(t_end * sample_rate),
|
||||
idx,
|
||||
]
|
||||
)
|
||||
buffer_durations[idx] += duration
|
||||
|
||||
if buffer_durations[idx] < min_slice_duration:
|
||||
transcript_buffers[idx] += ' '
|
||||
else:
|
||||
# Write out segment and transcript
|
||||
count += 1
|
||||
__write_sample(
|
||||
dst_root,
|
||||
file_id,
|
||||
count,
|
||||
file_count,
|
||||
sample_rate,
|
||||
np.concatenate(audio_buffers[idx], axis=0),
|
||||
buffer_durations[idx],
|
||||
transcript_buffers[idx],
|
||||
)
|
||||
|
||||
# Clear buffers
|
||||
transcript_buffers[idx] = ''
|
||||
audio_buffers[idx] = []
|
||||
buffer_durations[idx] = 0.0
|
||||
|
||||
# Note: We drop any shorter "scraps" at the end of the file, if
|
||||
# they end up shorter than min_slice_duration.
|
||||
|
||||
|
||||
def __partition_name(file_count):
|
||||
if file_count >= VAL_END_IDX:
|
||||
return "test"
|
||||
elif file_count >= TRAIN_END_IDX:
|
||||
return "val"
|
||||
else:
|
||||
return "train"
|
||||
|
||||
|
||||
def __process_data(
|
||||
audio_root,
|
||||
transcript_root,
|
||||
dst_root,
|
||||
min_slice_duration,
|
||||
file_count,
|
||||
keep_low_conf,
|
||||
rem_noises,
|
||||
emojify,
|
||||
):
|
||||
"""
|
||||
Converts Fisher wav files to numpy arrays, segments audio and transcripts.
|
||||
Args:
|
||||
audio_root: source directory with the wav files
|
||||
transcript_root: source directory with the transcript files
|
||||
(can be the same as audio_root)
|
||||
dst_root: where the processed and segmented files will be stored
|
||||
min_slice_duration: minimum number of seconds for a slice of output
|
||||
file_count: total number of files processed so far
|
||||
keep_low_conf: whether or not to keep low confidence transcriptions
|
||||
rem_noises: whether to remove noise symbols
|
||||
emojify: whether to convert noise symbols to emoji, lower precedence
|
||||
Assumes:
|
||||
1. There is exactly one transcripts directory in data_folder
|
||||
2. Audio files are all: <audio_root>/audio-wav/fe_03_xxxxx.wav
|
||||
"""
|
||||
transcript_list = glob.glob(os.path.join(transcript_root, "fe_03_p*_tran*", "data", "trans", "*", "*.txt"))
|
||||
print("Found {} transcripts.".format(len(transcript_list)))
|
||||
|
||||
count = file_count
|
||||
|
||||
# Grab audio file associated with each transcript, and slice
|
||||
for trans_path in tqdm(transcript_list, desc="Matching and segmenting"):
|
||||
file_id, _ = os.path.splitext(os.path.basename(trans_path))
|
||||
audio_path = os.path.join(audio_root, "audio_wav", file_id + ".wav")
|
||||
|
||||
sample_rate, audio_data = wavfile.read(audio_path)
|
||||
|
||||
# Create a set of segments (a block) for each file
|
||||
__process_one_file(
|
||||
trans_path,
|
||||
sample_rate,
|
||||
audio_data,
|
||||
file_id,
|
||||
dst_root,
|
||||
min_slice_duration,
|
||||
count,
|
||||
keep_low_conf,
|
||||
rem_noises,
|
||||
emojify,
|
||||
)
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
|
||||
def main():
|
||||
# Arguments to the script
|
||||
audio_root = args.audio_root
|
||||
transcript_root = args.transcript_root
|
||||
dest_root = args.dest_root
|
||||
|
||||
min_slice_duration = args.min_slice_duration
|
||||
keep_low_conf = args.keep_low_conf
|
||||
rem_noises = args.remove_noises
|
||||
emojify = args.noises_to_emoji
|
||||
|
||||
print(f"Expected number of files to segment: {NUM_FILES}")
|
||||
print("With a 80/10/10 split:")
|
||||
print(f"Number of training files: {TRAIN_END_IDX}")
|
||||
print(f"Number of validation files: {VAL_END_IDX - TRAIN_END_IDX}")
|
||||
print(f"Number of test files: {NUM_FILES - VAL_END_IDX}")
|
||||
|
||||
if not os.path.exists(os.path.join(dest_root, 'train/')):
|
||||
os.makedirs(os.path.join(dest_root, 'train/'))
|
||||
os.makedirs(os.path.join(dest_root, 'val/'))
|
||||
os.makedirs(os.path.join(dest_root, 'test/'))
|
||||
else:
|
||||
# Wipe manifest contents first
|
||||
open(os.path.join(dest_root, "manifest_train.json"), 'w').close()
|
||||
open(os.path.join(dest_root, "manifest_val.json"), 'w').close()
|
||||
open(os.path.join(dest_root, "manifest_test.json"), 'w').close()
|
||||
|
||||
file_count = 0
|
||||
|
||||
for data_set in ['LDC2004S13-Part1', 'LDC2005S13-Part2']:
|
||||
print(f"\n\nWorking on dataset: {data_set}")
|
||||
file_count = __process_data(
|
||||
os.path.join(audio_root, data_set),
|
||||
os.path.join(transcript_root, data_set),
|
||||
dest_root,
|
||||
min_slice_duration,
|
||||
file_count,
|
||||
keep_low_conf,
|
||||
rem_noises,
|
||||
emojify,
|
||||
)
|
||||
|
||||
print(f"Total file count so far: {file_count}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,285 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. 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.
|
||||
|
||||
# This script is heavily derived from the Patter HUB5 processing script written
|
||||
# by Ryan Leary
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from math import ceil, floor
|
||||
from operator import attrgetter
|
||||
|
||||
import numpy as np
|
||||
import scipy.io.wavfile as wavfile
|
||||
from tqdm import tqdm
|
||||
|
||||
parser = argparse.ArgumentParser(description="Prepare HUB5 data for training/eval")
|
||||
parser.add_argument(
|
||||
"--data_root",
|
||||
default=None,
|
||||
type=str,
|
||||
required=True,
|
||||
help="The path to the root LDC HUB5 dataset directory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dest_root",
|
||||
default=None,
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to the destination root directory for processed files.",
|
||||
)
|
||||
|
||||
# Optional arguments
|
||||
parser.add_argument(
|
||||
"--min_slice_duration",
|
||||
default=10.0,
|
||||
type=float,
|
||||
help="Minimum audio slice duration after processing.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
StmUtterance = namedtuple(
|
||||
'StmUtterance',
|
||||
[
|
||||
'filename',
|
||||
'channel',
|
||||
'speaker_id',
|
||||
'begin',
|
||||
'end',
|
||||
'label',
|
||||
'transcript',
|
||||
],
|
||||
)
|
||||
STM_LINE_FMT = re.compile(r"^(\w+)\s+(\w+)\s+(\w+)\s+([0-9.]+)\s+([0-9.]+)\s+(<.*>)?\s+(.+)$")
|
||||
|
||||
# Transcription errors and their fixes
|
||||
TRANSCRIPT_BUGS = {"en_4622-B-12079-12187": "KIND OF WEIRD BUT"}
|
||||
|
||||
|
||||
def get_utt_id(segment):
|
||||
"""
|
||||
Gives utterance IDs in a form like: en_4156-a-36558-37113
|
||||
"""
|
||||
return "{}-{}-{}-{}".format(
|
||||
segment.filename,
|
||||
segment.channel,
|
||||
int(segment.begin * 100),
|
||||
int(segment.end * 100),
|
||||
)
|
||||
|
||||
|
||||
def convert_utterances(sph_path, wav_path):
|
||||
"""
|
||||
Converts a sphere audio file to wav.
|
||||
"""
|
||||
cmd = ["sph2pipe", "-f", "wav", "-p", sph_path, wav_path]
|
||||
subprocess.run(cmd)
|
||||
|
||||
|
||||
def create_wavs(data_root, dest_root):
|
||||
"""
|
||||
Converts the English sph files to wav using sph2pipe.
|
||||
"""
|
||||
sph_root = os.path.join(data_root, "hub5e_00", "english")
|
||||
sph_list = glob.glob(os.path.join(sph_root, "*.sph"))
|
||||
|
||||
# Iterate over each sphere file and conver to wav
|
||||
for sph_path in tqdm(sph_list, desc="Converting to wav", unit="file"):
|
||||
sph_name, _ = os.path.splitext(os.path.basename(sph_path))
|
||||
wav_path = os.path.join(dest_root, 'full_audio_wav', sph_name + ".wav")
|
||||
cmd = ["sph2pipe", "-f", "wav", "-p", sph_path, wav_path]
|
||||
subprocess.run(cmd)
|
||||
|
||||
|
||||
def process_transcripts(dataset_root):
|
||||
"""
|
||||
Reads in transcripts for each audio segment and processes them.
|
||||
"""
|
||||
stm_path = os.path.join(
|
||||
dataset_root,
|
||||
"2000_hub5_eng_eval_tr",
|
||||
"reference",
|
||||
"hub5e00.english.000405.stm",
|
||||
)
|
||||
results = []
|
||||
chars = set()
|
||||
|
||||
with open(stm_path, "r") as fh:
|
||||
for line in fh:
|
||||
# lines with ';;' are comments
|
||||
if line.startswith(";;"):
|
||||
continue
|
||||
|
||||
if "IGNORE_TIME_SEGMENT_" in line:
|
||||
continue
|
||||
line = line.replace("<B_ASIDE>", "").replace("<E_ASIDE>", "")
|
||||
line = line.replace("(%HESITATION)", "UH")
|
||||
line = line.replace("-", "")
|
||||
line = line.replace("(%UH)", "UH")
|
||||
line = line.replace("(%AH)", "UH")
|
||||
line = line.replace("(", "").replace(")", "")
|
||||
|
||||
line = line.lower()
|
||||
|
||||
m = STM_LINE_FMT.search(line.strip())
|
||||
utt = StmUtterance(*m.groups())
|
||||
|
||||
# Convert begin/end times to float
|
||||
utt = utt._replace(begin=float(utt.begin))
|
||||
utt = utt._replace(end=float(utt.end))
|
||||
|
||||
# Check for utterance in dict of transcript mistakes
|
||||
transcript_update = TRANSCRIPT_BUGS.get(get_utt_id(utt))
|
||||
if transcript_update is not None:
|
||||
utt = utt._replace(transcript=transcript_update)
|
||||
|
||||
results.append(utt)
|
||||
chars.update(list(utt.transcript))
|
||||
return results, chars
|
||||
|
||||
|
||||
def write_one_segment(dest_root, speaker_id, count, audio, sr, duration, transcript):
|
||||
"""
|
||||
Writes out one segment of audio, and writes its corresponding transcript
|
||||
in the manifest.
|
||||
|
||||
Args:
|
||||
dest_root: the path to the output directory root
|
||||
speaker_id: ID of the speaker, used in file naming
|
||||
count: number of segments from this speaker so far
|
||||
audio: the segment's audio data
|
||||
sr: sample rate of the audio
|
||||
duration: duration of the audio
|
||||
transcript: the corresponding transcript
|
||||
"""
|
||||
audio_path = os.path.join(dest_root, "audio", f"{speaker_id}_{count:03}.wav")
|
||||
|
||||
manifest_path = os.path.join(dest_root, "manifest_hub5.json")
|
||||
|
||||
# Write audio
|
||||
wavfile.write(audio_path, sr, audio)
|
||||
|
||||
# Write transcript
|
||||
transcript = {
|
||||
"audio_filepath": audio_path,
|
||||
"duration": duration,
|
||||
"text": transcript,
|
||||
}
|
||||
with open(manifest_path, 'a') as f:
|
||||
json.dump(transcript, f)
|
||||
f.write('\n')
|
||||
|
||||
|
||||
def segment_audio(info_list, dest_root, min_slice_duration):
|
||||
"""
|
||||
Combines audio into >= min_slice_duration segments of the same speaker,
|
||||
and writes the combined transcripts into a manifest.
|
||||
|
||||
Args:
|
||||
info_list: list of StmUtterance objects with transcript information.
|
||||
dest_root: path to output destination
|
||||
min_slice_duration: min number of seconds per output audio slice
|
||||
"""
|
||||
info_list = sorted(info_list, key=attrgetter('speaker_id', 'begin'))
|
||||
|
||||
prev_id = None # For checking audio concatenation
|
||||
id_count = 0
|
||||
|
||||
sample_rate, audio_data = None, None
|
||||
transcript_buffer = ''
|
||||
audio_buffer = []
|
||||
buffer_duration = 0.0
|
||||
|
||||
# Iterate through utterances to build segments
|
||||
for info in info_list:
|
||||
if info.speaker_id != prev_id:
|
||||
# Scrap the remainder in the buffers and start next segment
|
||||
prev_id = info.speaker_id
|
||||
id_count = 0
|
||||
|
||||
sample_rate, audio_data = wavfile.read(os.path.join(dest_root, 'full_audio_wav', info.filename + '.wav'))
|
||||
transcript_buffer = ''
|
||||
audio_buffer = []
|
||||
buffer_duration = 0.0
|
||||
|
||||
# Append utterance info to buffers
|
||||
transcript_buffer += info.transcript
|
||||
channel = 0 if info.channel.lower() == 'a' else 1
|
||||
audio_buffer.append(
|
||||
audio_data[
|
||||
floor(info.begin * sample_rate) : ceil(info.end * sample_rate),
|
||||
channel,
|
||||
]
|
||||
)
|
||||
buffer_duration += info.end - info.begin
|
||||
|
||||
if buffer_duration < min_slice_duration:
|
||||
transcript_buffer += ' '
|
||||
else:
|
||||
# Write out segment and transcript
|
||||
id_count += 1
|
||||
write_one_segment(
|
||||
dest_root,
|
||||
info.speaker_id,
|
||||
id_count,
|
||||
np.concatenate(audio_buffer, axis=0),
|
||||
sample_rate,
|
||||
buffer_duration,
|
||||
transcript_buffer,
|
||||
)
|
||||
|
||||
transcript_buffer = ''
|
||||
audio_buffer = []
|
||||
buffer_duration = 0.0
|
||||
|
||||
|
||||
def main():
|
||||
data_root = args.data_root
|
||||
dest_root = args.dest_root
|
||||
|
||||
min_slice_duration = args.min_slice_duration
|
||||
|
||||
if not os.path.exists(os.path.join(dest_root, 'full_audio_wav')):
|
||||
os.makedirs(os.path.join(dest_root, 'full_audio_wav'))
|
||||
if not os.path.exists(os.path.join(dest_root, 'audio')):
|
||||
os.makedirs(os.path.join(dest_root, 'audio'))
|
||||
|
||||
# Create/wipe manifest contents
|
||||
open(os.path.join(dest_root, "manifest_hub5.json"), 'w').close()
|
||||
|
||||
# Convert full audio files from .sph to .wav
|
||||
create_wavs(data_root, dest_root)
|
||||
|
||||
# Get each audio transcript from transcript file
|
||||
info_list, chars = process_transcripts(data_root)
|
||||
|
||||
print("Writing out vocab file", file=sys.stderr)
|
||||
with open(os.path.join(dest_root, "vocab.txt"), 'w') as fh:
|
||||
for x in sorted(list(chars)):
|
||||
fh.write(x + "\n")
|
||||
|
||||
# Segment the audio data
|
||||
print("Segmenting audio and writing manifest")
|
||||
segment_audio(info_list, dest_root, min_slice_duration)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,521 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. 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.
|
||||
|
||||
"""
|
||||
Usage:
|
||||
|
||||
python process_speech_commands_data.py \
|
||||
--data_root=<absolute path to where the data should be stored> \
|
||||
--data_version=<either 1 or 2, indicating version of the dataset> \
|
||||
--class_split=<either "all" or "sub", indicates whether all 30/35 classes should be used, or the 10+2 split should be used> \
|
||||
--num_processes=<number of processes to use for data preprocessing> \
|
||||
--rebalance \
|
||||
--log
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import tarfile
|
||||
import urllib.request
|
||||
from collections import defaultdict
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
from typing import Dict, List, Set, Tuple
|
||||
|
||||
import librosa
|
||||
import numpy as np
|
||||
import soundfile
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.utils.tar_utils import safe_extract
|
||||
|
||||
URL_v1 = 'http://download.tensorflow.org/data/speech_commands_v0.01.tar.gz'
|
||||
URL_v2 = 'http://download.tensorflow.org/data/speech_commands_v0.02.tar.gz'
|
||||
|
||||
|
||||
def __maybe_download_file(destination: str, source: str) -> str:
|
||||
"""
|
||||
Downloads source to destination if it doesn't exist.
|
||||
If exists, skips download
|
||||
|
||||
Args:
|
||||
destination: local filepath
|
||||
source: url of resource
|
||||
|
||||
Returns:
|
||||
Local filepath of the downloaded file
|
||||
"""
|
||||
if not os.path.exists(destination):
|
||||
logging.info(f'{destination} does not exist. Downloading ...')
|
||||
urllib.request.urlretrieve(source, filename=destination + '.tmp')
|
||||
os.rename(destination + '.tmp', destination)
|
||||
logging.info(f'Downloaded {destination}.')
|
||||
else:
|
||||
logging.info(f'Destination {destination} exists. Skipping.')
|
||||
return destination
|
||||
|
||||
|
||||
def __extract_all_files(filepath: str, data_dir: str):
|
||||
if not os.path.exists(data_dir):
|
||||
extract_file(filepath, data_dir)
|
||||
else:
|
||||
logging.info(f'Skipping extracting. Data already there {data_dir}')
|
||||
|
||||
|
||||
def extract_file(filepath: str, data_dir: str):
|
||||
try:
|
||||
with tarfile.open(filepath) as tar:
|
||||
safe_extract(tar, data_dir)
|
||||
except Exception:
|
||||
logging.info('Not extracting. Maybe already there?')
|
||||
|
||||
|
||||
def __get_mp_chunksize(dataset_size: int, num_processes: int) -> int:
|
||||
"""
|
||||
Returns the number of chunks to split the dataset into for multiprocessing.
|
||||
|
||||
Args:
|
||||
dataset_size: size of the dataset
|
||||
num_processes: number of processes to use for multiprocessing
|
||||
|
||||
Returns:
|
||||
Number of chunks to split the dataset into for multiprocessing
|
||||
"""
|
||||
chunksize = dataset_size // num_processes
|
||||
return chunksize if chunksize > 0 else 1
|
||||
|
||||
|
||||
def __construct_filepaths(
|
||||
all_files: List[str],
|
||||
valset_uids: Set[str],
|
||||
testset_uids: Set[str],
|
||||
class_split: str,
|
||||
class_subset: List[str],
|
||||
pattern: str,
|
||||
) -> Tuple[Dict[str, int], Dict[str, List[tuple]], List[tuple], List[tuple], List[tuple], List[tuple], List[tuple]]:
|
||||
"""
|
||||
Prepares the filepaths for the dataset.
|
||||
|
||||
Args:
|
||||
all_files: list of all files in the dataset
|
||||
valset_uids: set of uids of files in the validation set
|
||||
testset_uids: set of uids of files in the test set
|
||||
class_split: whether to use all classes as distinct labels, or to use
|
||||
10 classes subset and rest of the classes as noise or background
|
||||
class_subset: list of classes to consider if `class_split` is set to `sub`
|
||||
pattern: regex pattern to match the file names in the dataset
|
||||
"""
|
||||
|
||||
label_count = defaultdict(int)
|
||||
label_filepaths = defaultdict(list)
|
||||
unknown_val_filepaths = []
|
||||
unknown_test_filepaths = []
|
||||
|
||||
train, val, test = [], [], []
|
||||
for entry in all_files:
|
||||
r = re.match(pattern, entry)
|
||||
if r:
|
||||
label, uid = r.group(2), r.group(3)
|
||||
|
||||
if label == '_background_noise_' or label == 'silence':
|
||||
continue
|
||||
|
||||
if class_split == 'sub' and label not in class_subset:
|
||||
label = 'unknown'
|
||||
|
||||
if uid in valset_uids:
|
||||
unknown_val_filepaths.append((label, entry))
|
||||
elif uid in testset_uids:
|
||||
unknown_test_filepaths.append((label, entry))
|
||||
|
||||
if uid not in valset_uids and uid not in testset_uids:
|
||||
label_count[label] += 1
|
||||
label_filepaths[label].append((label, entry))
|
||||
|
||||
if label == 'unknown':
|
||||
continue
|
||||
|
||||
if uid in valset_uids:
|
||||
val.append((label, entry))
|
||||
elif uid in testset_uids:
|
||||
test.append((label, entry))
|
||||
else:
|
||||
train.append((label, entry))
|
||||
|
||||
return {
|
||||
'label_count': label_count,
|
||||
'label_filepaths': label_filepaths,
|
||||
'unknown_val_filepaths': unknown_val_filepaths,
|
||||
'unknown_test_filepaths': unknown_test_filepaths,
|
||||
'train': train,
|
||||
'val': val,
|
||||
'test': test,
|
||||
}
|
||||
|
||||
|
||||
def __construct_silence_set(
|
||||
rng: np.random.RandomState, sampling_rate: int, silence_stride: int, data_folder: str, background_noise: str
|
||||
) -> List[str]:
|
||||
"""
|
||||
Creates silence files given a background noise.
|
||||
|
||||
Args:
|
||||
rng: Random state for random number generator
|
||||
sampling_rate: sampling rate of the audio
|
||||
silence_stride: stride for creating silence files
|
||||
data_folder: folder containing the silence directory
|
||||
background_noise: filepath of the background noise
|
||||
|
||||
Returns:
|
||||
List of filepaths of silence files
|
||||
"""
|
||||
silence_files = []
|
||||
if '.wav' in background_noise:
|
||||
y, sr = librosa.load(background_noise, sr=sampling_rate)
|
||||
|
||||
for i in range(0, len(y) - sampling_rate, silence_stride):
|
||||
file_path = f'silence/{os.path.basename(background_noise)[:-4]}_{i}.wav'
|
||||
y_slice = y[i : i + sampling_rate] * rng.uniform(0.0, 1.0)
|
||||
out_file_path = os.path.join(data_folder, file_path)
|
||||
soundfile.write(out_file_path, y_slice, sr)
|
||||
|
||||
silence_files.append(('silence', out_file_path))
|
||||
|
||||
return silence_files
|
||||
|
||||
|
||||
def __rebalance_files(max_count: int, label_filepath: str) -> Tuple[str, List[str], int]:
|
||||
"""
|
||||
Rebalance the number of samples for a class.
|
||||
|
||||
Args:
|
||||
max_count: maximum number of samples for a class
|
||||
label_filepath: list of filepaths for a class
|
||||
|
||||
Returns:
|
||||
Rebalanced list of filepaths along with the label name and the number of samples
|
||||
"""
|
||||
command, samples = label_filepath
|
||||
filepaths = [sample[1] for sample in samples]
|
||||
|
||||
rng = np.random.RandomState(0)
|
||||
filepaths = np.asarray(filepaths)
|
||||
num_samples = len(filepaths)
|
||||
|
||||
if num_samples < max_count:
|
||||
difference = max_count - num_samples
|
||||
duplication_ids = rng.choice(num_samples, difference, replace=True)
|
||||
filepaths = np.append(filepaths, filepaths[duplication_ids], axis=0)
|
||||
|
||||
return command, filepaths, num_samples
|
||||
|
||||
|
||||
def __prepare_metadata(skip_duration, sample: Tuple[str, str]) -> dict:
|
||||
"""
|
||||
Creates the manifest entry for a file.
|
||||
|
||||
Args:
|
||||
skip_duration: Whether to skip the computation of duration
|
||||
sample: Tuple of label and filepath
|
||||
|
||||
Returns:
|
||||
Manifest entry of the file
|
||||
"""
|
||||
label, audio_path = sample
|
||||
return json.dumps(
|
||||
{
|
||||
'audio_filepath': audio_path,
|
||||
'duration': 0.0 if skip_duration else librosa.core.get_duration(filename=audio_path),
|
||||
'command': label,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def __process_data(
|
||||
data_folder: str,
|
||||
dst_folder: str,
|
||||
num_processes: int = 1,
|
||||
rebalance: bool = False,
|
||||
class_split: str = 'all',
|
||||
skip_duration: bool = False,
|
||||
):
|
||||
"""
|
||||
Processes the data and generates the manifests.
|
||||
|
||||
Args:
|
||||
data_folder: source with wav files and validation / test lists
|
||||
dst_folder: where manifest files will be stored
|
||||
num_processes: number of processes
|
||||
rebalance: rebalance the classes to have same number of samples
|
||||
class_split: whether to use all classes as distinct labels, or to use
|
||||
10 classes subset and rest of the classes as noise or background
|
||||
skip_duration: Bool whether to skip duration computation. Use this only for
|
||||
colab notebooks where knowing duration is not necessary for demonstration
|
||||
"""
|
||||
|
||||
os.makedirs(dst_folder, exist_ok=True)
|
||||
|
||||
# Used for 10 classes + silence + unknown class setup - Only used when class_split is 'sub'
|
||||
class_subset = ['yes', 'no', 'up', 'down', 'left', 'right', 'on', 'off', 'stop', 'go']
|
||||
|
||||
pattern = re.compile(r'(.+\/)?(\w+)\/([^_]+)_.+wav')
|
||||
all_files = glob.glob(os.path.join(data_folder, '*/*wav'))
|
||||
|
||||
# Get files in the validation set
|
||||
valset_uids = set()
|
||||
with open(os.path.join(data_folder, 'validation_list.txt')) as fin:
|
||||
for line in fin:
|
||||
r = re.match(pattern, line)
|
||||
if r:
|
||||
valset_uids.add(r.group(3))
|
||||
|
||||
# Get files in the test set
|
||||
testset_uids = set()
|
||||
with open(os.path.join(data_folder, 'testing_list.txt')) as fin:
|
||||
for line in fin:
|
||||
r = re.match(pattern, line)
|
||||
if r:
|
||||
testset_uids.add(r.group(3))
|
||||
|
||||
logging.info('Validation and test set lists extracted')
|
||||
|
||||
filepath_info = __construct_filepaths(all_files, valset_uids, testset_uids, class_split, class_subset, pattern)
|
||||
label_count = filepath_info['label_count']
|
||||
label_filepaths = filepath_info['label_filepaths']
|
||||
unknown_val_filepaths = filepath_info['unknown_val_filepaths']
|
||||
unknown_test_filepaths = filepath_info['unknown_test_filepaths']
|
||||
train = filepath_info['train']
|
||||
val = filepath_info['val']
|
||||
test = filepath_info['test']
|
||||
|
||||
logging.info('Prepared filepaths for dataset')
|
||||
|
||||
pool = Pool(num_processes)
|
||||
|
||||
# Add silence and unknown class label samples
|
||||
if class_split == 'sub':
|
||||
logging.info('Perforiming 10+2 class subsplit')
|
||||
|
||||
silence_path = os.path.join(data_folder, 'silence')
|
||||
os.makedirs(silence_path, exist_ok=True)
|
||||
|
||||
silence_stride = 1000 # 0.0625 second stride
|
||||
sampling_rate = 16000
|
||||
folder = os.path.join(data_folder, '_background_noise_')
|
||||
|
||||
silence_files = []
|
||||
rng = np.random.RandomState(0)
|
||||
|
||||
background_noise_files = [os.path.join(folder, x) for x in os.listdir(folder)]
|
||||
silence_set_fn = partial(__construct_silence_set, rng, sampling_rate, silence_stride, data_folder)
|
||||
for silence_flist in tqdm(
|
||||
pool.imap(
|
||||
silence_set_fn, background_noise_files, __get_mp_chunksize(len(background_noise_files), num_processes)
|
||||
),
|
||||
total=len(background_noise_files),
|
||||
desc='Constructing silence set',
|
||||
):
|
||||
silence_files.extend(silence_flist)
|
||||
|
||||
rng = np.random.RandomState(0)
|
||||
rng.shuffle(silence_files)
|
||||
logging.info(f'Constructed silence set of {len(silence_files)}')
|
||||
|
||||
# Create the splits
|
||||
rng = np.random.RandomState(0)
|
||||
silence_split = 0.1
|
||||
unknown_split = 0.1
|
||||
|
||||
# train split
|
||||
num_total_samples = sum([label_count[cls] for cls in class_subset])
|
||||
num_silence_samples = int(np.ceil(silence_split * num_total_samples))
|
||||
|
||||
# initialize sample
|
||||
label_count['silence'] = 0
|
||||
label_filepaths['silence'] = []
|
||||
|
||||
for silence_id in range(num_silence_samples):
|
||||
label_count['silence'] += 1
|
||||
label_filepaths['silence'].append(silence_files[silence_id])
|
||||
|
||||
train.extend(label_filepaths['silence'])
|
||||
|
||||
# Update train unknown set
|
||||
unknown_train_samples = label_filepaths['unknown']
|
||||
|
||||
rng.shuffle(unknown_train_samples)
|
||||
unknown_size = int(np.ceil(unknown_split * num_total_samples))
|
||||
|
||||
label_count['unknown'] = unknown_size
|
||||
label_filepaths['unknown'] = unknown_train_samples[:unknown_size]
|
||||
|
||||
train.extend(label_filepaths['unknown'])
|
||||
|
||||
logging.info('Train set prepared')
|
||||
|
||||
# val set silence
|
||||
num_val_samples = len(val)
|
||||
num_silence_samples = int(np.ceil(silence_split * num_val_samples))
|
||||
|
||||
val_idx = label_count['silence'] + 1
|
||||
for silence_id in range(num_silence_samples):
|
||||
val.append(silence_files[val_idx + silence_id])
|
||||
|
||||
# Update val unknown set
|
||||
rng.shuffle(unknown_val_filepaths)
|
||||
unknown_size = int(np.ceil(unknown_split * num_val_samples))
|
||||
|
||||
val.extend(unknown_val_filepaths[:unknown_size])
|
||||
|
||||
logging.info('Validation set prepared')
|
||||
|
||||
# test set silence
|
||||
num_test_samples = len(test)
|
||||
num_silence_samples = int(np.ceil(silence_split * num_test_samples))
|
||||
|
||||
test_idx = val_idx + num_silence_samples + 1
|
||||
for silence_id in range(num_silence_samples):
|
||||
test.append(silence_files[test_idx + silence_id])
|
||||
|
||||
# Update test unknown set
|
||||
rng.shuffle(unknown_test_filepaths)
|
||||
unknown_size = int(np.ceil(unknown_split * num_test_samples))
|
||||
|
||||
test.extend(unknown_test_filepaths[:unknown_size])
|
||||
|
||||
logging.info('Test set prepared')
|
||||
|
||||
max_command = None
|
||||
max_count = -1
|
||||
for command, count in label_count.items():
|
||||
if command == 'unknown':
|
||||
continue
|
||||
|
||||
if count > max_count:
|
||||
max_count = count
|
||||
max_command = command
|
||||
|
||||
if rebalance:
|
||||
logging.info(f'Command with maximum number of samples = {max_command} with {max_count} samples')
|
||||
logging.info(f'Rebalancing dataset by duplicating classes with less than {max_count} samples...')
|
||||
|
||||
rebalance_fn = partial(__rebalance_files, max_count)
|
||||
for command, filepaths, num_samples in tqdm(
|
||||
pool.imap(rebalance_fn, label_filepaths.items(), __get_mp_chunksize(len(label_filepaths), num_processes)),
|
||||
total=len(label_filepaths),
|
||||
desc='Rebalancing dataset',
|
||||
):
|
||||
if num_samples < max_count:
|
||||
logging.info(f'Extended class label {command} from {num_samples} samples to {len(filepaths)} samples')
|
||||
label_filepaths[command] = [(command, filepath) for filepath in filepaths]
|
||||
|
||||
del train
|
||||
train = []
|
||||
for label, samples in label_filepaths.items():
|
||||
train.extend(samples)
|
||||
|
||||
manifests = [
|
||||
('train_manifest.json', train),
|
||||
('validation_manifest.json', val),
|
||||
('test_manifest.json', test),
|
||||
]
|
||||
|
||||
metadata_fn = partial(__prepare_metadata, skip_duration)
|
||||
for manifest_filename, dataset in manifests:
|
||||
num_files = len(dataset)
|
||||
|
||||
logging.info(f'Preparing manifest : {manifest_filename} with #{num_files} files')
|
||||
|
||||
manifest = [
|
||||
metadata
|
||||
for metadata in tqdm(
|
||||
pool.imap(metadata_fn, dataset, __get_mp_chunksize(len(dataset), num_processes)),
|
||||
total=num_files,
|
||||
desc=f'Preparing {manifest_filename}',
|
||||
)
|
||||
]
|
||||
|
||||
with open(os.path.join(dst_folder, manifest_filename), 'w') as fout:
|
||||
for metadata in manifest:
|
||||
fout.write(metadata + '\n')
|
||||
|
||||
logging.info(f'Finished construction of manifest. Path: {os.path.join(dst_folder, manifest_filename)}')
|
||||
|
||||
pool.close()
|
||||
|
||||
if skip_duration:
|
||||
logging.info(
|
||||
f'\n<<NOTE>> Duration computation was skipped for demonstration purposes on Colaboratory.\n'
|
||||
f'In order to replicate paper results and properly perform data augmentation, \n'
|
||||
f'please recompute the manifest file without the `--skip_duration` flag !\n'
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Google Speech Commands Data download and preprocessing')
|
||||
parser.add_argument('--data_root', required=True, help='Root directory for storing data')
|
||||
parser.add_argument(
|
||||
'--data_version',
|
||||
required=True,
|
||||
default=1,
|
||||
type=int,
|
||||
choices=[1, 2],
|
||||
help='Version of the speech commands dataset to download',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--class_split', default='all', choices=['all', 'sub'], help='Whether to consider all classes or only a subset'
|
||||
)
|
||||
parser.add_argument('--num_processes', default=1, type=int, help='Number of processes')
|
||||
parser.add_argument('--rebalance', action='store_true', help='Rebalance the number of samples in each class')
|
||||
parser.add_argument('--skip_duration', action='store_true', help='Skip computing duration of audio files')
|
||||
parser.add_argument('--log', action='store_true', help='Generate logs')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.log:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
data_root = args.data_root
|
||||
data_set = f'google_speech_recognition_v{args.data_version}'
|
||||
data_folder = os.path.join(data_root, data_set)
|
||||
|
||||
logging.info(f'Working on: {data_set}')
|
||||
|
||||
URL = URL_v1 if args.data_version == 1 else URL_v2
|
||||
|
||||
# Download and extract
|
||||
if not os.path.exists(data_folder):
|
||||
file_path = os.path.join(data_root, data_set + '.tar.bz2')
|
||||
logging.info(f'Getting {data_set}')
|
||||
__maybe_download_file(file_path, URL)
|
||||
logging.info(f'Extracting {data_set}')
|
||||
__extract_all_files(file_path, data_folder)
|
||||
|
||||
logging.info(f'Processing {data_set}')
|
||||
__process_data(
|
||||
data_folder,
|
||||
data_folder,
|
||||
num_processes=args.num_processes,
|
||||
rebalance=args.rebalance,
|
||||
class_split=args.class_split,
|
||||
skip_duration=args.skip_duration,
|
||||
)
|
||||
logging.info('Done!')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,503 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. 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.
|
||||
"""
|
||||
Usage:
|
||||
|
||||
python process_vad_data.py \
|
||||
--out_dir=<output path to where the generated manifest should be stored> \
|
||||
--speech_data_root=<path where the speech data are stored> \
|
||||
--background_data_root=<path where the background data are stored> \
|
||||
--rebalance_method=<'under' or 'over' or 'fixed'> \
|
||||
--log
|
||||
(Optional --demo (for demonstration in tutorial). If you want to use your own background noise data, make sure to delete --demo)
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tarfile
|
||||
import urllib.request
|
||||
|
||||
import librosa
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
from sklearn.model_selection import train_test_split
|
||||
|
||||
from nemo.utils.tar_utils import safe_extract
|
||||
|
||||
sr = 16000
|
||||
|
||||
# google speech command v2
|
||||
URL = "http://download.tensorflow.org/data/speech_commands_v0.02.tar.gz"
|
||||
|
||||
|
||||
def __maybe_download_file(destination: str, source: str):
|
||||
"""
|
||||
Downloads source to destination if it doesn't exist.
|
||||
If exists, skips download
|
||||
Args:
|
||||
destination: local filepath
|
||||
source: url of resource
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if not os.path.exists(destination):
|
||||
logging.info(f"{destination} does not exist. Downloading ...")
|
||||
urllib.request.urlretrieve(source, filename=destination + '.tmp')
|
||||
os.rename(destination + '.tmp', destination)
|
||||
logging.info(f"Downloaded {destination}.")
|
||||
else:
|
||||
logging.info(f"Destination {destination} exists. Skipping.")
|
||||
return destination
|
||||
|
||||
|
||||
def extract_file(filepath: str, data_dir: str):
|
||||
try:
|
||||
with tarfile.open(filepath) as tar:
|
||||
safe_extract(tar, data_dir)
|
||||
except Exception:
|
||||
logging.info('Not extracting. Maybe already there?')
|
||||
|
||||
|
||||
def __extract_all_files(filepath: str, data_root: str, data_dir: str):
|
||||
if not os.path.exists(data_dir):
|
||||
extract_file(filepath, data_dir)
|
||||
else:
|
||||
logging.info(f'Skipping extracting. Data already there {data_dir}')
|
||||
|
||||
|
||||
def split_train_val_test(data_dir, file_type, test_size=0.1, val_size=0.1, demo=False):
|
||||
X = []
|
||||
if file_type == "speech":
|
||||
for o in os.listdir(data_dir):
|
||||
if os.path.isdir(os.path.join(data_dir, o)) and o.split("/")[-1] != "_background_noise_":
|
||||
X.extend(glob.glob(os.path.join(data_dir, o) + '/*.wav'))
|
||||
|
||||
if demo:
|
||||
logging.info(
|
||||
f"For Demonstration, we use {int(len(X)/100)}/{len(X)} speech data. Make sure to remove --demo flag when you actually train your model!"
|
||||
)
|
||||
X = np.random.choice(X, int(len(X) / 100), replace=False)
|
||||
|
||||
else:
|
||||
for o in os.listdir(data_dir):
|
||||
if os.path.isdir(os.path.join(data_dir, o)):
|
||||
X.extend(glob.glob(os.path.join(data_dir, o) + '/*.wav'))
|
||||
else: # for using "_background_noise_" from google speech commands as background data
|
||||
if o.endswith(".wav"):
|
||||
X.append(os.path.join(data_dir, o))
|
||||
|
||||
X_train, X_test = train_test_split(X, test_size=test_size, random_state=1)
|
||||
val_size_tmp = val_size / (1 - test_size)
|
||||
X_train, X_val = train_test_split(X_train, test_size=val_size_tmp, random_state=1)
|
||||
|
||||
with open(os.path.join(data_dir, file_type + "_training_list.txt"), "w") as outfile:
|
||||
outfile.write("\n".join(X_train))
|
||||
with open(os.path.join(data_dir, file_type + "_testing_list.txt"), "w") as outfile:
|
||||
outfile.write("\n".join(X_test))
|
||||
with open(os.path.join(data_dir, file_type + "_validation_list.txt"), "w") as outfile:
|
||||
outfile.write("\n".join(X_val))
|
||||
|
||||
logging.info(f'Overall: {len(X)}, Train: {len(X_train)}, Validatoin: {len(X_val)}, Test: {len(X_test)}')
|
||||
logging.info(f"Finish spliting train, val and test for {file_type}. Write to files!")
|
||||
|
||||
|
||||
def process_google_speech_train(data_dir):
|
||||
X = []
|
||||
for o in os.listdir(data_dir):
|
||||
if os.path.isdir(os.path.join(data_dir, o)) and o.split("/")[-1] != "_background_noise_":
|
||||
X.extend(glob.glob(os.path.join(data_dir, o) + '/*.wav'))
|
||||
|
||||
short_files = [i.split(data_dir)[1] for i in files]
|
||||
|
||||
with open(os.path.join(data_dir, 'testing_list.txt'), 'r') as allfile:
|
||||
testing_list = allfile.read().splitlines()
|
||||
|
||||
with open(os.path.join(data_dir, 'validation_list.txt'), 'r') as allfile:
|
||||
validation_list = allfile.read().splitlines()
|
||||
|
||||
exist_set = set(testing_list).copy()
|
||||
exist_set.update(set(validation_list))
|
||||
|
||||
training_list = [i for i in short_files if i not in exist_set]
|
||||
|
||||
with open(os.path.join(data_dir, "training_list.txt"), "w") as outfile:
|
||||
outfile.write("\n".join(training_list))
|
||||
|
||||
logging.info(
|
||||
f'Overall: {len(files)}, Train: {len(training_list)}, Validatoin: {len(validation_list)}, Test: {len(testing_list)}'
|
||||
)
|
||||
|
||||
|
||||
def write_manifest(
|
||||
out_dir,
|
||||
files,
|
||||
prefix,
|
||||
manifest_name,
|
||||
start=0.0,
|
||||
end=None,
|
||||
duration_stride=1.0,
|
||||
duration_max=None,
|
||||
duration_limit=100.0,
|
||||
filter_long=False,
|
||||
):
|
||||
"""
|
||||
Given a list of files, segment each file and write them to manifest with restrictions.
|
||||
Args:
|
||||
out_dir: directory of generated manifest
|
||||
files: list of files to be processed
|
||||
prefix: label of samples
|
||||
manifest_name: name of generated manifest
|
||||
start: beginning of audio of generating segment
|
||||
end: end of audio of generating segment
|
||||
duration_stride: stride for segmenting audio samples
|
||||
duration_max: duration for each segment
|
||||
duration_limit: duration threshold for filtering out long audio samples
|
||||
filter_long: boolean to determine whether to filter out long audio samples
|
||||
Returns:
|
||||
"""
|
||||
seg_num = 0
|
||||
skip_num = 0
|
||||
if duration_max is None:
|
||||
duration_max = 1e9
|
||||
|
||||
if not os.path.exists(out_dir):
|
||||
logging.info(f'Outdir {out_dir} does not exist. Creat directory.')
|
||||
os.mkdir(out_dir)
|
||||
|
||||
output_path = os.path.join(out_dir, manifest_name + '.json')
|
||||
with open(output_path, 'w') as fout:
|
||||
for file in files:
|
||||
label = prefix
|
||||
|
||||
try:
|
||||
x, _sr = librosa.load(file, sr=sr)
|
||||
duration = librosa.get_duration(y=x, sr=sr)
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if filter_long and duration > duration_limit:
|
||||
skip_num += 1
|
||||
continue
|
||||
|
||||
offsets = []
|
||||
durations = []
|
||||
|
||||
if duration > duration_max:
|
||||
current_offset = start
|
||||
|
||||
while current_offset < duration:
|
||||
if end is not None and current_offset > end:
|
||||
break
|
||||
|
||||
difference = duration - current_offset
|
||||
|
||||
if difference < duration_max:
|
||||
break
|
||||
|
||||
offsets.append(current_offset)
|
||||
durations.append(duration_max)
|
||||
|
||||
current_offset += duration_stride
|
||||
|
||||
else:
|
||||
# Duration is not long enough! Skip
|
||||
skip_num += 1
|
||||
|
||||
for duration, offset in zip(durations, offsets):
|
||||
metadata = {
|
||||
'audio_filepath': file,
|
||||
'duration': duration,
|
||||
'label': label,
|
||||
'text': '_', # for compatibility with ASRAudioText
|
||||
'offset': offset,
|
||||
}
|
||||
json.dump(metadata, fout)
|
||||
fout.write('\n')
|
||||
fout.flush()
|
||||
seg_num += 1
|
||||
return skip_num, seg_num, output_path
|
||||
|
||||
|
||||
def load_list_write_manifest(
|
||||
data_dir,
|
||||
out_dir,
|
||||
filename,
|
||||
prefix,
|
||||
start,
|
||||
end,
|
||||
duration_stride=1.0,
|
||||
duration_max=1.0,
|
||||
duration_limit=100.0,
|
||||
filter_long=True,
|
||||
):
|
||||
|
||||
filename = prefix + '_' + filename
|
||||
file_path = os.path.join(data_dir, filename)
|
||||
|
||||
with open(file_path, 'r') as allfile:
|
||||
files = allfile.read().splitlines()
|
||||
|
||||
manifest_name = filename.split('_list.txt')[0] + '_manifest'
|
||||
skip_num, seg_num, output_path = write_manifest(
|
||||
out_dir,
|
||||
files,
|
||||
prefix,
|
||||
manifest_name,
|
||||
start,
|
||||
end,
|
||||
duration_stride,
|
||||
duration_max,
|
||||
duration_limit,
|
||||
filter_long=True,
|
||||
)
|
||||
return skip_num, seg_num, output_path
|
||||
|
||||
|
||||
def rebalance_json(data_dir, data_json, num, prefix):
|
||||
data = []
|
||||
seg = 0
|
||||
with open(data_json, 'r') as f:
|
||||
for line in f:
|
||||
data.append(json.loads(line))
|
||||
|
||||
filename = data_json.split('/')[-1]
|
||||
fout_path = os.path.join(data_dir, prefix + "_" + filename)
|
||||
|
||||
if len(data) >= num:
|
||||
selected_sample = np.random.choice(data, num, replace=False)
|
||||
else:
|
||||
selected_sample = np.random.choice(data, num, replace=True)
|
||||
|
||||
with open(fout_path, 'a') as fout:
|
||||
for i in selected_sample:
|
||||
seg += 1
|
||||
json.dump(i, fout)
|
||||
fout.write('\n')
|
||||
fout.flush()
|
||||
|
||||
logging.info(f'Get {seg}/{num} to {fout_path} from {data_json}')
|
||||
return fout_path
|
||||
|
||||
|
||||
def generate_variety_noise(data_dir, filename, prefix):
|
||||
|
||||
curr_dir = data_dir.split("_background_noise_")[0]
|
||||
silence_path = os.path.join(curr_dir, "_background_noise_more")
|
||||
|
||||
if not os.path.exists(silence_path):
|
||||
os.mkdir(silence_path)
|
||||
|
||||
silence_stride = 1000 # stride = 1/16 seconds
|
||||
sampling_rate = 16000
|
||||
|
||||
silence_files = []
|
||||
rng = np.random.RandomState(0)
|
||||
|
||||
filename = prefix + '_' + filename
|
||||
file_path = os.path.join(data_dir, filename)
|
||||
|
||||
with open(file_path, 'r') as allfile:
|
||||
files = allfile.read().splitlines()
|
||||
|
||||
for file in files:
|
||||
y, sr = librosa.load(path=file, sr=sampling_rate)
|
||||
|
||||
for i in range(
|
||||
0, len(y) - sampling_rate, silence_stride * 100
|
||||
): # stride * 100 to generate less samples for demo
|
||||
file_name = "{}_{}.wav".format(file.split("/")[-1], i)
|
||||
y_slice = y[i : i + sampling_rate]
|
||||
magnitude = rng.uniform(0.0, 1.0)
|
||||
y_slice *= magnitude
|
||||
out_file_path = os.path.join(silence_path, file_name)
|
||||
sf.write(out_file_path, y_slice, sr)
|
||||
|
||||
silence_files.append(out_file_path)
|
||||
|
||||
new_list_file = os.path.join(silence_path, filename)
|
||||
with open(new_list_file, "w") as outfile:
|
||||
outfile.write("\n".join(silence_files))
|
||||
|
||||
logging.info(f"Generate {len(out_file_path)} background files for {file_path}. => {new_list_file} !")
|
||||
return len(silence_files)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Speech and backgound data download and preprocess')
|
||||
parser.add_argument("--out_dir", required=False, default='./manifest/', type=str)
|
||||
parser.add_argument("--speech_data_root", required=True, default=None, type=str)
|
||||
parser.add_argument("--background_data_root", required=True, default=None, type=str)
|
||||
parser.add_argument('--test_size', required=False, default=0.1, type=float)
|
||||
parser.add_argument('--val_size', required=False, default=0.1, type=float)
|
||||
parser.add_argument('--window_length_in_sec', required=False, default=0.63, type=float)
|
||||
parser.add_argument('--log', required=False, action='store_true')
|
||||
parser.add_argument('--rebalance_method', required=False, default=None, type=str)
|
||||
parser.add_argument('--demo', required=False, action='store_true')
|
||||
parser.set_defaults(log=False, generate=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.rebalance_method:
|
||||
rebalance = False
|
||||
else:
|
||||
if args.rebalance_method != 'over' and args.rebalance_method != 'under' and args.rebalance_method != 'fixed':
|
||||
raise NameError("Please select a valid sampling method: over/under/fixed.")
|
||||
else:
|
||||
rebalance = True
|
||||
|
||||
if args.log:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# Download speech data
|
||||
speech_data_root = args.speech_data_root
|
||||
data_set = "google_speech_recognition_v2"
|
||||
speech_data_folder = os.path.join(speech_data_root, data_set)
|
||||
|
||||
background_data_folder = args.background_data_root
|
||||
logging.info(f"Working on: {data_set}")
|
||||
|
||||
# Download and extract speech data
|
||||
if not os.path.exists(speech_data_folder):
|
||||
file_path = os.path.join(speech_data_root, data_set + ".tar.bz2")
|
||||
logging.info(f"Getting {data_set}")
|
||||
__maybe_download_file(file_path, URL)
|
||||
logging.info(f"Extracting {data_set}")
|
||||
__extract_all_files(file_path, speech_data_root, speech_data_folder)
|
||||
|
||||
logging.info(f"Split speech data!")
|
||||
# dataset provide testing.txt and validation.txt feel free to split data using that with process_google_speech_train
|
||||
split_train_val_test(speech_data_folder, "speech", args.test_size, args.val_size, args.demo)
|
||||
|
||||
logging.info(f"Split background data!")
|
||||
split_train_val_test(background_data_folder, "background", args.test_size, args.val_size)
|
||||
|
||||
out_dir = args.out_dir
|
||||
|
||||
# Process Speech manifest
|
||||
logging.info(f"=== Write speech data to manifest!")
|
||||
skip_num_val, speech_seg_num_val, speech_val = load_list_write_manifest(
|
||||
speech_data_folder,
|
||||
out_dir,
|
||||
'validation_list.txt',
|
||||
'speech',
|
||||
0.2,
|
||||
0.8,
|
||||
args.window_length_in_sec,
|
||||
args.window_length_in_sec,
|
||||
)
|
||||
skip_num_test, speech_seg_num_test, speech_test = load_list_write_manifest(
|
||||
speech_data_folder, out_dir, 'testing_list.txt', 'speech', 0.2, 0.8, 0.01, args.window_length_in_sec
|
||||
)
|
||||
skip_num_train, speech_seg_num_train, speech_train = load_list_write_manifest(
|
||||
speech_data_folder,
|
||||
out_dir,
|
||||
'training_list.txt',
|
||||
'speech',
|
||||
0.2,
|
||||
0.8,
|
||||
args.window_length_in_sec,
|
||||
args.window_length_in_sec,
|
||||
)
|
||||
|
||||
logging.info(f'Val: Skip {skip_num_val} samples. Get {speech_seg_num_val} segments! => {speech_val} ')
|
||||
logging.info(f'Test: Skip {skip_num_test} samples. Get {speech_seg_num_test} segments! => {speech_test}')
|
||||
logging.info(f'Train: Skip {skip_num_train} samples. Get {speech_seg_num_train} segments!=> {speech_train}')
|
||||
|
||||
# Process background manifest
|
||||
# if we select to generate more background noise data
|
||||
if args.demo:
|
||||
logging.info("Start generating more background noise data")
|
||||
generate_variety_noise(background_data_folder, 'validation_list.txt', 'background')
|
||||
generate_variety_noise(background_data_folder, 'training_list.txt', 'background')
|
||||
generate_variety_noise(background_data_folder, 'testing_list.txt', 'background')
|
||||
background_data_folder = os.path.join(
|
||||
background_data_folder.split("_background_noise_")[0], "_background_noise_more"
|
||||
)
|
||||
|
||||
logging.info(f"=== Write background data to manifest!")
|
||||
skip_num_val, background_seg_num_val, background_val = load_list_write_manifest(
|
||||
background_data_folder, out_dir, 'validation_list.txt', 'background', 0, None, 0.15, args.window_length_in_sec
|
||||
)
|
||||
skip_num_test, background_seg_num_test, background_test = load_list_write_manifest(
|
||||
background_data_folder, out_dir, 'testing_list.txt', 'background', 0, None, 0.01, args.window_length_in_sec
|
||||
)
|
||||
skip_num_train, background_seg_num_train, background_train = load_list_write_manifest(
|
||||
background_data_folder, out_dir, 'training_list.txt', 'background', 0, None, 0.15, args.window_length_in_sec
|
||||
)
|
||||
|
||||
logging.info(f'Val: Skip {skip_num_val} samples. Get {background_seg_num_val} segments! => {background_val}')
|
||||
logging.info(f'Test: Skip {skip_num_test} samples. Get {background_seg_num_test} segments! => {background_test}')
|
||||
logging.info(
|
||||
f'Train: Skip {skip_num_train} samples. Get {background_seg_num_train} segments! => {background_train}'
|
||||
)
|
||||
min_val, max_val = min(speech_seg_num_val, background_seg_num_val), max(speech_seg_num_val, background_seg_num_val)
|
||||
min_test, max_test = (
|
||||
min(speech_seg_num_test, background_seg_num_test),
|
||||
max(speech_seg_num_test, background_seg_num_test),
|
||||
)
|
||||
min_train, max_train = (
|
||||
min(speech_seg_num_train, background_seg_num_train),
|
||||
max(speech_seg_num_train, background_seg_num_train),
|
||||
)
|
||||
|
||||
logging.info('Finish generating manifest!')
|
||||
|
||||
if rebalance:
|
||||
# Random Oversampling: Randomly duplicate examples in the minority class.
|
||||
# Random Undersampling: Randomly delete examples in the majority class.
|
||||
if args.rebalance_method == 'under':
|
||||
logging.info(f"Rebalancing number of samples in classes using {args.rebalance_method} sampling.")
|
||||
logging.info(f'Val: {min_val} Test: {min_test} Train: {min_train}!')
|
||||
|
||||
rebalance_json(out_dir, background_val, min_val, 'balanced')
|
||||
rebalance_json(out_dir, background_test, min_test, 'balanced')
|
||||
rebalance_json(out_dir, background_train, min_train, 'balanced')
|
||||
|
||||
rebalance_json(out_dir, speech_val, min_val, 'balanced')
|
||||
rebalance_json(out_dir, speech_test, min_test, 'balanced')
|
||||
rebalance_json(out_dir, speech_train, min_train, 'balanced')
|
||||
|
||||
if args.rebalance_method == 'over':
|
||||
logging.info(f"Rebalancing number of samples in classes using {args.rebalance_method} sampling.")
|
||||
logging.info(f'Val: {max_val} Test: {max_test} Train: {max_train}!')
|
||||
|
||||
rebalance_json(out_dir, background_val, max_val, 'balanced')
|
||||
rebalance_json(out_dir, background_test, max_test, 'balanced')
|
||||
rebalance_json(out_dir, background_train, max_train, 'balanced')
|
||||
|
||||
rebalance_json(out_dir, speech_val, max_val, 'balanced')
|
||||
rebalance_json(out_dir, speech_test, max_test, 'balanced')
|
||||
rebalance_json(out_dir, speech_train, max_train, 'balanced')
|
||||
|
||||
if args.rebalance_method == 'fixed':
|
||||
fixed_test, fixed_val, fixed_train = 200, 100, 500
|
||||
logging.info(f"Rebalancing number of samples in classes using {args.rebalance_method} sampling.")
|
||||
logging.info(f'Val: {fixed_val} Test: {fixed_test} Train: {fixed_train}!')
|
||||
|
||||
rebalance_json(out_dir, background_val, fixed_val, 'balanced')
|
||||
rebalance_json(out_dir, background_test, fixed_test, 'balanced')
|
||||
rebalance_json(out_dir, background_train, fixed_train, 'balanced')
|
||||
|
||||
rebalance_json(out_dir, speech_val, fixed_val, 'balanced')
|
||||
rebalance_json(out_dir, speech_test, fixed_test, 'balanced')
|
||||
rebalance_json(out_dir, speech_train, fixed_train, 'balanced')
|
||||
else:
|
||||
logging.info("Don't rebalance number of samples in classes.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,5 @@
|
||||
# Speaker Tasks Dataset Scripts
|
||||
|
||||
In this folder are scripts to download speaker tasks (mainly for diarization) datasets. These scripts will return NeMo format manifest files to use with Diarization.
|
||||
|
||||
We also have scripts for CallHome and DIHARD3, however the data has to be downloaded separately. If you require the scripts please leave an issue.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. 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.
|
||||
|
||||
# downloads the training/eval set for AISHELL Diarization.
|
||||
# the training dataset is around 170GiB, to skip pass the --skip_train flag.
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import tarfile
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import create_manifest
|
||||
from nemo.utils.tar_utils import safe_extract
|
||||
|
||||
train_url = "https://www.openslr.org/resources/111/train_{}.tar.gz"
|
||||
train_datasets = ["S", "M", "L"]
|
||||
|
||||
eval_url = "https://www.openslr.org/resources/111/test.tar.gz"
|
||||
|
||||
|
||||
def _load_sox_transformer():
|
||||
try:
|
||||
from sox import Transformer
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Optional dependency 'sox' is required by this script. Install it with: pip install sox"
|
||||
) from None
|
||||
|
||||
return Transformer
|
||||
|
||||
|
||||
def extract_file(filepath: str, data_dir: str):
|
||||
try:
|
||||
with tarfile.open(filepath) as tar:
|
||||
safe_extract(tar, data_dir)
|
||||
except Exception:
|
||||
logging.info("Not extracting. Maybe already there?")
|
||||
|
||||
|
||||
def __process_data(dataset_url: str, dataset_path: Path, manifest_output_path: Path):
|
||||
os.makedirs(dataset_path, exist_ok=True)
|
||||
tar_file_path = os.path.join(dataset_path, os.path.basename(dataset_url))
|
||||
if not os.path.exists(tar_file_path):
|
||||
urllib.request.urlretrieve(dataset_url, filename=tar_file_path)
|
||||
extract_file(tar_file_path, str(dataset_path))
|
||||
wav_path = dataset_path / 'converted_wav/'
|
||||
extracted_dir = Path(tar_file_path).stem.replace('.tar', '')
|
||||
flac_path = dataset_path / (extracted_dir + '/wav/')
|
||||
__process_flac_audio(flac_path, wav_path)
|
||||
|
||||
audio_files = [os.path.join(os.path.abspath(wav_path), file) for file in os.listdir(str(wav_path))]
|
||||
rttm_files = glob.glob(str(dataset_path / (extracted_dir + '/TextGrid/*.rttm')))
|
||||
rttm_files = [os.path.abspath(file) for file in rttm_files]
|
||||
|
||||
audio_list = dataset_path / 'audio_files.txt'
|
||||
rttm_list = dataset_path / 'rttm_files.txt'
|
||||
with open(audio_list, 'w') as f:
|
||||
f.write('\n'.join(audio_files))
|
||||
with open(rttm_list, 'w') as f:
|
||||
f.write('\n'.join(rttm_files))
|
||||
create_manifest(
|
||||
str(audio_list),
|
||||
manifest_output_path,
|
||||
rttm_path=str(rttm_list),
|
||||
)
|
||||
|
||||
|
||||
def __process_flac_audio(flac_path, wav_path):
|
||||
Transformer = _load_sox_transformer()
|
||||
os.makedirs(wav_path, exist_ok=True)
|
||||
flac_files = os.listdir(flac_path)
|
||||
for flac_file in flac_files:
|
||||
# Convert FLAC file to WAV
|
||||
id = Path(flac_file).stem
|
||||
wav_file = os.path.join(wav_path, id + ".wav")
|
||||
if not os.path.exists(wav_file):
|
||||
Transformer().build(os.path.join(flac_path, flac_file), wav_file)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Aishell Data download")
|
||||
parser.add_argument("--data_root", default='./', type=str)
|
||||
parser.add_argument("--output_manifest_path", default='aishell_diar_manifest.json', type=str)
|
||||
parser.add_argument("--skip_train", help="skip downloading the training dataset", action="store_true")
|
||||
args = parser.parse_args()
|
||||
data_root = Path(args.data_root)
|
||||
data_root.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
if not args.skip_train:
|
||||
for tag in train_datasets:
|
||||
dataset_url = train_url.format(tag)
|
||||
dataset_path = data_root / f'{tag}/'
|
||||
manifest_output_path = data_root / f'train_{tag}_manifest.json'
|
||||
__process_data(
|
||||
dataset_url=dataset_url, dataset_path=dataset_path, manifest_output_path=manifest_output_path
|
||||
)
|
||||
# create test dataset
|
||||
dataset_path = data_root / f'eval/'
|
||||
manifest_output_path = data_root / f'eval_manifest.json'
|
||||
__process_data(dataset_url=eval_url, dataset_path=dataset_path, manifest_output_path=manifest_output_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,102 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. 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.
|
||||
#
|
||||
# Download the AMI test dataset used to evaluate Speaker Diarization
|
||||
# More information here: https://groups.inf.ed.ac.uk/ami/corpus/
|
||||
# USAGE: python get_ami_data.py
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import create_manifest
|
||||
|
||||
rttm_url = "https://raw.githubusercontent.com/BUTSpeechFIT/AMI-diarization-setup/main/only_words/rttms/{}/{}.rttm"
|
||||
uem_url = "https://raw.githubusercontent.com/BUTSpeechFIT/AMI-diarization-setup/main/uems/{}/{}.uem"
|
||||
list_url = "https://raw.githubusercontent.com/BUTSpeechFIT/AMI-diarization-setup/main/lists/{}.meetings.txt"
|
||||
|
||||
|
||||
audio_types = ['Mix-Headset', 'Array1-01']
|
||||
|
||||
# these two IDs in the train set are missing download links for Array1-01.
|
||||
# We exclude them as a result.
|
||||
not_found_ids = ['IS1007d', 'IS1003b']
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Download the AMI Corpus Dataset for Speaker Diarization")
|
||||
parser.add_argument(
|
||||
"--test_manifest_filepath",
|
||||
help="path to output test manifest file",
|
||||
type=str,
|
||||
default='AMI_test_manifest.json',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dev_manifest_filepath",
|
||||
help="path to output dev manifest file",
|
||||
type=str,
|
||||
default='AMI_dev_manifest.json',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--train_manifest_filepath",
|
||||
help="path to output train manifest file",
|
||||
type=str,
|
||||
default='AMI_train_manifest.json',
|
||||
)
|
||||
parser.add_argument("--data_root", help="path to output data directory", type=str, default="ami_dataset")
|
||||
args = parser.parse_args()
|
||||
|
||||
data_path = os.path.abspath(args.data_root)
|
||||
os.makedirs(data_path, exist_ok=True)
|
||||
|
||||
for manifest_path, split in (
|
||||
(args.test_manifest_filepath, 'test'),
|
||||
(args.dev_manifest_filepath, 'dev'),
|
||||
(args.train_manifest_filepath, 'train'),
|
||||
):
|
||||
split_path = os.path.join(data_path, split)
|
||||
audio_path = os.path.join(split_path, "audio")
|
||||
os.makedirs(split_path, exist_ok=True)
|
||||
rttm_path = os.path.join(split_path, "rttm")
|
||||
uem_path = os.path.join(split_path, "uem")
|
||||
|
||||
subprocess.run(["wget", "-P", split_path, list_url.format(split)])
|
||||
with open(os.path.join(split_path, f"{split}.meetings.txt")) as f:
|
||||
ids = f.read().strip().split('\n')
|
||||
for id in [file_id for file_id in ids if file_id not in not_found_ids]:
|
||||
for audio_type in audio_types:
|
||||
audio_type_path = os.path.join(audio_path, audio_type)
|
||||
os.makedirs(audio_type_path, exist_ok=True)
|
||||
audio_download = (
|
||||
f"https://groups.inf.ed.ac.uk/ami/AMICorpusMirror//amicorpus/{id}/audio/" f"{id}.{audio_type}.wav"
|
||||
)
|
||||
subprocess.run(["wget", "-P", audio_type_path, audio_download])
|
||||
rttm_download = rttm_url.format(split, id)
|
||||
subprocess.run(["wget", "-P", rttm_path, rttm_download])
|
||||
uem_download = uem_url.format(split, id)
|
||||
subprocess.run(["wget", "-P", uem_path, uem_download])
|
||||
|
||||
rttm_files_path = os.path.join(split_path, 'rttm_files.txt')
|
||||
with open(rttm_files_path, 'w') as f:
|
||||
f.write('\n'.join(os.path.join(rttm_path, p) for p in os.listdir(rttm_path)))
|
||||
uem_files_path = os.path.join(split_path, 'uem_files.txt')
|
||||
with open(uem_files_path, 'w') as f:
|
||||
f.write('\n'.join(os.path.join(uem_path, p) for p in os.listdir(uem_path)))
|
||||
for audio_type in audio_types:
|
||||
audio_type_path = os.path.join(audio_path, audio_type)
|
||||
audio_files_path = os.path.join(split_path, f'audio_files_{audio_type}.txt')
|
||||
with open(audio_files_path, 'w') as f:
|
||||
f.write('\n'.join(os.path.join(audio_type_path, p) for p in os.listdir(audio_type_path)))
|
||||
audio_type_manifest_path = manifest_path.replace('.json', f'.{audio_type}.json')
|
||||
create_manifest(
|
||||
audio_files_path, audio_type_manifest_path, rttm_path=rttm_files_path, uem_path=uem_files_path
|
||||
)
|
||||
@@ -0,0 +1,208 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. 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.
|
||||
|
||||
# USAGE: python get_hi-mia_data.py --data_root=<where to put data>
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging as _logging
|
||||
import os
|
||||
import tarfile
|
||||
import urllib.request
|
||||
from glob import glob
|
||||
|
||||
import librosa as l
|
||||
from sklearn.model_selection import StratifiedShuffleSplit
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.utils.tar_utils import safe_extract
|
||||
|
||||
parser = argparse.ArgumentParser(description="HI-MIA Data download")
|
||||
parser.add_argument("--data_root", required=True, default=None, type=str)
|
||||
parser.add_argument("--log_level", default=20, type=int)
|
||||
args = parser.parse_args()
|
||||
logging = _logging.getLogger(__name__)
|
||||
logging.addHandler(_logging.StreamHandler())
|
||||
logging.setLevel(args.log_level)
|
||||
|
||||
URL = {
|
||||
"dev": "http://www.openslr.org/resources/85/dev.tar.gz",
|
||||
"test": "http://www.openslr.org/resources/85/test.tar.gz",
|
||||
"train": "http://www.openslr.org/resources/85/train.tar.gz",
|
||||
}
|
||||
|
||||
|
||||
def __retrieve_with_progress(source: str, filename: str):
|
||||
"""
|
||||
Downloads source to destination
|
||||
Displays progress bar
|
||||
Args:
|
||||
source: url of resource
|
||||
destination: local filepath
|
||||
Returns:
|
||||
"""
|
||||
with open(filename, "wb") as f:
|
||||
response = urllib.request.urlopen(source)
|
||||
total = response.length
|
||||
|
||||
if total is None:
|
||||
f.write(response.content)
|
||||
else:
|
||||
with tqdm(total=total, unit="B", unit_scale=True, unit_divisor=1024) as pbar:
|
||||
for data in response:
|
||||
f.write(data)
|
||||
pbar.update(len(data))
|
||||
|
||||
|
||||
def __maybe_download_file(destination: str, source: str):
|
||||
"""
|
||||
Downloads source to destination if it doesn't exist.
|
||||
If exists, skips download
|
||||
Args:
|
||||
destination: local filepath
|
||||
source: url of resource
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
source = URL[source]
|
||||
if not os.path.exists(destination) and not os.path.exists(os.path.splitext(destination)[0]):
|
||||
logging.info("{0} does not exist. Downloading ...".format(destination))
|
||||
__retrieve_with_progress(source, filename=destination + ".tmp")
|
||||
os.rename(destination + ".tmp", destination)
|
||||
logging.info("Downloaded {0}.".format(destination))
|
||||
elif os.path.exists(destination):
|
||||
logging.info("Destination {0} exists. Skipping.".format(destination))
|
||||
elif os.path.exists(os.path.splitext(destination)[0]):
|
||||
logging.warning(
|
||||
"Assuming extracted folder %s contains the extracted files from %s. Will not download.",
|
||||
os.path.basename(destination),
|
||||
destination,
|
||||
)
|
||||
return destination
|
||||
|
||||
|
||||
def __extract_all_files(filepath: str, data_root: str, data_dir: str):
|
||||
if not os.path.exists(data_dir):
|
||||
extract_file(filepath, data_root)
|
||||
audio_dir = os.path.join(data_dir, "wav")
|
||||
for subfolder, _, filelist in os.walk(audio_dir):
|
||||
for ftar in filelist:
|
||||
extract_file(os.path.join(subfolder, ftar), subfolder)
|
||||
else:
|
||||
logging.info("Skipping extracting. Data already there %s" % data_dir)
|
||||
|
||||
|
||||
def extract_file(filepath: str, data_dir: str):
|
||||
try:
|
||||
with tarfile.open(filepath, encoding='utf-8') as tar:
|
||||
safe_extract(tar, data_dir)
|
||||
except Exception:
|
||||
logging.info("Not extracting. Maybe already there?")
|
||||
|
||||
|
||||
def __remove_tarred_files(filepath: str, data_dir: str):
|
||||
if os.path.exists(data_dir) and os.path.isfile(filepath):
|
||||
logging.info("Deleting %s" % filepath)
|
||||
os.remove(filepath)
|
||||
|
||||
|
||||
def write_file(name, lines, idx):
|
||||
with open(name, "w") as fout:
|
||||
for i in idx:
|
||||
dic = lines[i]
|
||||
json.dump(dic, fout)
|
||||
fout.write("\n")
|
||||
logging.info("wrote %s", name)
|
||||
|
||||
|
||||
def __process_data(data_folder: str, data_set: str):
|
||||
"""
|
||||
To generate manifest
|
||||
Args:
|
||||
data_folder: source with wav files
|
||||
Returns:
|
||||
|
||||
"""
|
||||
fullpath = os.path.abspath(data_folder)
|
||||
filelist = glob(fullpath + "/**/*.wav", recursive=True)
|
||||
out = os.path.join(fullpath, data_set + "_all.json")
|
||||
utt2spk = os.path.join(fullpath, "utt2spk")
|
||||
utt2spk_file = open(utt2spk, "w")
|
||||
id = -2 # speaker id
|
||||
|
||||
if os.path.exists(out):
|
||||
logging.warning(
|
||||
"%s already exists and is assumed to be processed. If not, please delete %s and rerun this script",
|
||||
out,
|
||||
out,
|
||||
)
|
||||
return
|
||||
|
||||
speakers = []
|
||||
lines = []
|
||||
with open(out, "w") as outfile:
|
||||
for line in tqdm(filelist):
|
||||
line = line.strip()
|
||||
y, sr = l.load(line, sr=None)
|
||||
if sr != 16000:
|
||||
y, sr = l.load(line, sr=16000)
|
||||
l.output.write_wav(line, y, sr)
|
||||
dur = l.get_duration(y=y, sr=sr)
|
||||
if data_set == "test":
|
||||
speaker = line.split("/")[-1].split(".")[0].split("_")[0]
|
||||
else:
|
||||
speaker = line.split("/")[id]
|
||||
speaker = list(speaker)
|
||||
speaker = "".join(speaker)
|
||||
speakers.append(speaker)
|
||||
meta = {"audio_filepath": line, "duration": float(dur), "label": speaker}
|
||||
lines.append(meta)
|
||||
json.dump(meta, outfile)
|
||||
outfile.write("\n")
|
||||
utt2spk_file.write(line.split("/")[-1] + "\t" + speaker + "\n")
|
||||
|
||||
utt2spk_file.close()
|
||||
|
||||
if data_set != "test":
|
||||
sss = StratifiedShuffleSplit(n_splits=1, test_size=0.1, random_state=42)
|
||||
for train_idx, test_idx in sss.split(speakers, speakers):
|
||||
print(len(train_idx))
|
||||
|
||||
out = os.path.join(fullpath, "train.json")
|
||||
write_file(out, lines, train_idx)
|
||||
out = os.path.join(fullpath, "dev.json")
|
||||
write_file(out, lines, test_idx)
|
||||
|
||||
|
||||
def main():
|
||||
data_root = args.data_root
|
||||
for data_set in URL.keys():
|
||||
|
||||
# data_set = 'data_aishell'
|
||||
logging.info("\n\nWorking on: {0}".format(data_set))
|
||||
file_path = os.path.join(data_root, data_set + ".tgz")
|
||||
logging.info("Getting {0}".format(data_set))
|
||||
__maybe_download_file(file_path, data_set)
|
||||
logging.info("Extracting {0}".format(data_set))
|
||||
data_folder = os.path.join(data_root, data_set)
|
||||
__extract_all_files(file_path, data_root, data_folder)
|
||||
__remove_tarred_files(file_path, data_folder)
|
||||
logging.info("Processing {0}".format(data_set))
|
||||
__process_data(data_folder, data_set)
|
||||
logging.info("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,95 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. 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.
|
||||
|
||||
# downloads the training/eval set for VoxConverse.
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import create_manifest
|
||||
|
||||
dev_url = "https://www.robots.ox.ac.uk/~vgg/data/voxconverse/data/voxconverse_dev_wav.zip"
|
||||
test_url = "https://www.robots.ox.ac.uk/~vgg/data/voxconverse/data/voxconverse_test_wav.zip"
|
||||
rttm_annotations_url = "https://github.com/joonson/voxconverse/archive/refs/heads/master.zip"
|
||||
|
||||
|
||||
def extract_file(filepath: Path, data_dir: Path):
|
||||
try:
|
||||
with zipfile.ZipFile(str(filepath), 'r') as zip_ref:
|
||||
zip_ref.extractall(str(data_dir))
|
||||
except Exception:
|
||||
logging.info("Not extracting. Maybe already there?")
|
||||
|
||||
|
||||
def download_file(url: str, destination: Path) -> Path:
|
||||
urllib.request.urlretrieve(url, filename=str(destination))
|
||||
return destination
|
||||
|
||||
|
||||
def _generate_manifest(data_root: Path, audio_path: Path, rttm_path: Path, manifest_output_path: Path):
|
||||
audio_list = str(data_root / 'audio_file.txt')
|
||||
rttm_list = str(data_root / 'rttm_file.txt')
|
||||
with open(audio_list, 'w') as f:
|
||||
f.write('\n'.join([str(os.path.join(rttm_path, x)) for x in os.listdir(audio_path)]))
|
||||
with open(rttm_list, 'w') as f:
|
||||
f.write('\n'.join([str(os.path.join(rttm_path, x)) for x in os.listdir(rttm_path)]))
|
||||
create_manifest(
|
||||
audio_list,
|
||||
str(manifest_output_path),
|
||||
rttm_path=rttm_list,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="VoxConverse Data download")
|
||||
parser.add_argument("--data_root", default='./', type=str)
|
||||
args = parser.parse_args()
|
||||
data_root = Path(args.data_root)
|
||||
data_root.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
test_path = data_root / os.path.basename(test_url)
|
||||
dev_path = data_root / os.path.basename(dev_url)
|
||||
rttm_path = data_root / os.path.basename(rttm_annotations_url)
|
||||
|
||||
if not os.path.exists(test_path):
|
||||
test_path = download_file(test_url, test_path)
|
||||
if not os.path.exists(dev_path):
|
||||
dev_path = download_file(dev_url, dev_path)
|
||||
if not os.path.exists(rttm_path):
|
||||
rttm_path = download_file(rttm_annotations_url, rttm_path)
|
||||
|
||||
extract_file(test_path, data_root / 'test/')
|
||||
extract_file(dev_path, data_root / 'dev/')
|
||||
extract_file(rttm_path, data_root)
|
||||
|
||||
_generate_manifest(
|
||||
data_root=data_root,
|
||||
audio_path=os.path.abspath(data_root / 'test/voxconverse_test_wav/'),
|
||||
rttm_path=os.path.abspath(data_root / 'voxconverse-master/test/'),
|
||||
manifest_output_path=data_root / 'test_manifest.json',
|
||||
)
|
||||
_generate_manifest(
|
||||
data_root=data_root,
|
||||
audio_path=os.path.abspath(data_root / 'dev/audio/'),
|
||||
rttm_path=os.path.abspath(data_root / 'voxconverse-master/dev/'),
|
||||
manifest_output_path=data_root / 'dev_manifest.json',
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,204 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. 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.
|
||||
|
||||
|
||||
"""
|
||||
This script can be used to preprocess Spoken Wikipedia corpus before running ctc-segmentation.
|
||||
The input folder consists of subfolders with following stricture
|
||||
├── <Name of Wikipedia article>
|
||||
│ ├── aligned.swc
|
||||
│ ├── audiometa.txt
|
||||
│ ├── audio.ogg
|
||||
│ ├── info.json
|
||||
│ ├── wiki.html
|
||||
│ ├── wiki.txt
|
||||
│ └── wiki.xml
|
||||
|
||||
|
||||
## The destination folder will contain look enumerated .ogg and .txt files like this:
|
||||
├── audio
|
||||
| ├── 1.ogg
|
||||
| ├── 2.ogg
|
||||
| ...
|
||||
└── text
|
||||
├── 1.txt
|
||||
├── 2.txt
|
||||
...
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--input_folder", required=True, type=str, help="Input folder in which each subfolder contains an article"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--destination_folder", required=True, type=str, help="Destination folder with audio and text subfolder"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def replace_diacritics(text):
|
||||
text = re.sub(r"[éèëēêęěė]", "e", text)
|
||||
text = re.sub(r"[ãâāáäăâàąåạả]", "a", text)
|
||||
text = re.sub(r"[úūüùưûů]", "u", text)
|
||||
text = re.sub(r"[ôōóöõòő]", "o", text)
|
||||
text = re.sub(r"[ćçč]", "c", text)
|
||||
text = re.sub(r"[ïīíîıì]", "i", text)
|
||||
text = re.sub(r"[ñńňņ]", "n", text)
|
||||
text = re.sub(r"[țť]", "t", text)
|
||||
text = re.sub(r"[łľ]", "l", text)
|
||||
text = re.sub(r"[żžź]", "z", text)
|
||||
text = re.sub(r"[ğ]", "g", text)
|
||||
text = re.sub(r"[ř]", "r", text)
|
||||
text = re.sub(r"[ý]", "y", text)
|
||||
text = re.sub(r"[æ]", "ae", text)
|
||||
text = re.sub(r"[œ]", "oe", text)
|
||||
text = re.sub(r"[șşšś]", "s", text)
|
||||
return text
|
||||
|
||||
|
||||
def get_audio(name, n):
|
||||
"""
|
||||
Copies .ogg file. If there are several .ogg files, concatenates them.
|
||||
|
||||
Args:
|
||||
name - name of folder within Spoken Wikipedia
|
||||
n - integer that will serve as output file name, e.g. if n=1, file 1.ogg will be created
|
||||
"""
|
||||
audio_path = os.path.join(args.input_folder, name, "audio.ogg")
|
||||
if not os.path.exists(audio_path):
|
||||
## Some folders have multiple .ogg files, so we need to first combine them into one file. Example:
|
||||
## |── Universe
|
||||
## │ ├── aligned.swc
|
||||
## │ ├── audio1.ogg
|
||||
## │ ├── audio2.ogg
|
||||
## │ ├── audio3.ogg
|
||||
## │ ├── audio4.ogg
|
||||
## │ ├── audiometa.txt
|
||||
## │ ├── info.json
|
||||
## │ ├── wiki.html
|
||||
## │ ├── wiki.txt
|
||||
## │ └── wiki.xml
|
||||
|
||||
multiple_ogg_files = []
|
||||
for i in range(1, 5):
|
||||
path = os.path.join(args.input_folder, name, "audio" + str(i) + ".ogg")
|
||||
if os.path.exists(path):
|
||||
multiple_ogg_files.append(path)
|
||||
else:
|
||||
break
|
||||
if len(multiple_ogg_files) == 0:
|
||||
return
|
||||
elif len(multiple_ogg_files) == 1:
|
||||
shutil.copy(multiple_ogg_files[0], audio_path)
|
||||
else:
|
||||
tmp_file_name = "ffmeg_inputs.txt"
|
||||
print("tmp_file_name=", tmp_file_name)
|
||||
with open(tmp_file_name, "w", encoding="utf-8") as tmp_file:
|
||||
for path in multiple_ogg_files:
|
||||
tmp_file.write("file '" + path + "'\n")
|
||||
ffmpeg_cmd = ["ffmpeg", "-f", "concat", "-i", tmp_file_name, "-c", "copy", audio_path]
|
||||
print("ffmpeg command:", ffmpeg_cmd)
|
||||
subprocess.run(ffmpeg_cmd, check=True)
|
||||
|
||||
output_audio_path = args.destination_folder + "/audio/" + str(n) + ".ogg"
|
||||
shutil.copy(audio_path, output_audio_path)
|
||||
|
||||
|
||||
def get_text(name, n):
|
||||
"""
|
||||
Cleans wiki.txt.
|
||||
|
||||
Args:
|
||||
name - name of folder within Spoken Wikipedia
|
||||
n - integer that will serve as output file name, e.g. if n=1, file 1.txt will be created
|
||||
"""
|
||||
|
||||
# Then we need to clean the text
|
||||
out_text = open(args.destination_folder + "/text/" + str(n) + ".txt", "w", encoding="utf-8")
|
||||
with open(args.input_folder + "/" + name + "/wiki.txt", "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
do_break = False
|
||||
line2 = line.strip()
|
||||
ref_parts = line2.split("<ref")
|
||||
for idx, s in enumerate(ref_parts):
|
||||
if idx != 0:
|
||||
s = "<ref" + s
|
||||
if s.startswith("[[Image") and s.endswith("]]"):
|
||||
continue
|
||||
if s.startswith("[[File") and s.endswith("]]"):
|
||||
continue
|
||||
if s.startswith(":"):
|
||||
continue
|
||||
if s.startswith("{|") or s.startswith("|}") or s.startswith("|") or s.startswith("!"):
|
||||
continue
|
||||
if s.startswith("{{") and (s.endswith("}}") or "}}" not in s):
|
||||
continue
|
||||
if s.startswith("{{pp-move"):
|
||||
continue
|
||||
s = re.sub(r"\[\[Image\:[^\]]+\]\]", r"", s)
|
||||
s = re.sub(r"\[\[File\:[^\]]+\]\]", r"", s)
|
||||
s = re.sub(r"\[http[^\]]+\]", r"", s)
|
||||
s = re.sub(r"<math>[^<>]+</math>", r"", s)
|
||||
s = re.sub(r"<!\-\-.+\-\->", r"", s) # <!--DashBot--> can be inside <ref>
|
||||
s = re.sub(r"<ref>.+</ref>", r"", s)
|
||||
s = re.sub(r"<ref .+</ref>", r"", s)
|
||||
s = re.sub(r"<ref[^<>]+/>", r"", s)
|
||||
s = re.sub(r"<[^ <>]+>", r"", s) # <sub>, <sup>, </u>
|
||||
if (
|
||||
re.match(r"== *Notes *==", s)
|
||||
or re.match(r"== *References *==", s)
|
||||
or re.match(r"== *External links *==", s)
|
||||
or re.match(r"== *See also *==", s)
|
||||
):
|
||||
do_break = True
|
||||
break
|
||||
s = re.sub(r"{{convert\|(\d+)\|(\w+)\|[^}]+}}", r"\g<1> \g<2>", s) # {{convert|7600|lb|kg}}
|
||||
s = re.sub(r"{{cquote\|", r"", s)
|
||||
s = re.sub(r"{{[^{}]+}}", r"", s)
|
||||
s = s.replace("{{", "").replace("}}", "")
|
||||
s = re.sub(r"(lang[^()]+)", r"", s) # (lang-bn...)
|
||||
s = re.sub(r"==+", r"", s)
|
||||
s = re.sub(r"''+", r" ", s) # remove multiple quotes
|
||||
s = re.sub(r" '", r" ", s) # remove quote at the beginning
|
||||
s = re.sub(r"' ", r" ", s) # remove quote at the end
|
||||
s = re.sub(r"[…\*]", r" ", s)
|
||||
s = re.sub(r"\\u....", r" ", s) # remove unicode
|
||||
s = re.sub(r"&[^ ;&]+;", r"", s) # —
|
||||
|
||||
s = replace_diacritics(s)
|
||||
|
||||
s = re.sub(r"\[\[[^\]]+\|([^\]]+)\]\]", r"\g<1>", s) # if several variants, take the last one
|
||||
s = re.sub(r"\[\[([^\]]+)\]\]", r"\g<1>", s)
|
||||
|
||||
out_text.write(s + "\n")
|
||||
if do_break:
|
||||
break
|
||||
out_text.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
n = 0
|
||||
for name in os.listdir(args.input_folder):
|
||||
n += 1
|
||||
if not os.path.exists(args.input_folder + "/" + name + "/wiki.txt"):
|
||||
print("wiki.txt does not exist in " + name)
|
||||
continue
|
||||
get_audio(name, n)
|
||||
get_text(name, n)
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. 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.
|
||||
|
||||
## Download the Spoken Wikipedia corpus for English
|
||||
## Note, that there are some other languages available
|
||||
## @InProceedings{KHN16.518,
|
||||
## author = {Arne K{\"o}hn and Florian Stegen and Timo Baumann},
|
||||
## title = {Mining the Spoken Wikipedia for Speech Data and Beyond},
|
||||
## booktitle = {Proceedings of the Tenth International Conference on Language Resources and Evaluation (LREC 2016)},
|
||||
## year = {2016},
|
||||
## month = {may},
|
||||
## date = {23-28},
|
||||
## location = {Portorož, Slovenia},
|
||||
## editor = {Nicoletta Calzolari (Conference Chair) and Khalid Choukri and Thierry Declerck and Marko Grobelnik and Bente Maegaard and Joseph Mariani and Asuncion Moreno and Jan Odijk and Stelios Piperidis},
|
||||
## publisher = {European Language Resources Association (ELRA)},
|
||||
## address = {Paris, France},
|
||||
## isbn = {978-2-9517408-9-1},
|
||||
## islrn = {684-927-624-257-3/},
|
||||
## language = {english}
|
||||
## }
|
||||
|
||||
wget https://corpora.uni-hamburg.de/hzsk/de/islandora/object/file:swc-2.0_en-with-audio/datastream/TAR/en-with-audio.tar .
|
||||
tar -xvf en-with-audio.tar
|
||||
|
||||
## We get a folder English with 1339 subfolders, each subfolder corresponds to a Wikipedia article. Example:
|
||||
## ├── Universal_suffrage
|
||||
## │ ├── aligned.swc
|
||||
## │ ├── audiometa.txt
|
||||
## │ ├── audio.ogg
|
||||
## │ ├── info.json
|
||||
## │ ├── wiki.html
|
||||
## │ ├── wiki.txt
|
||||
## │ └── wiki.xml
|
||||
|
||||
## We will use two files: audio.ogg and wiki.txt
|
||||
|
||||
## Some folders have multiple .ogg files, this will be handled during preprocess.py. Example:
|
||||
## |── Universe
|
||||
## │ ├── aligned.swc
|
||||
## │ ├── audio1.ogg
|
||||
## │ ├── audio2.ogg
|
||||
## │ ├── audio3.ogg
|
||||
## │ ├── audio4.ogg
|
||||
## │ ├── audiometa.txt
|
||||
## │ ├── info.json
|
||||
## │ ├── wiki.html
|
||||
## │ ├── wiki.txt
|
||||
## │ └── wiki.xml
|
||||
|
||||
## Some rare folders are incomplete, these will be skipped during preprocessing.
|
||||
|
||||
## Rename some folders with special symbols because they cause problems to ffmpeg when concatening multiple .ogg files
|
||||
mv "english/The_Hitchhiker%27s_Guide_to_the_Galaxy" "english/The_Hitchhikers_guide_to_the_Galaxy"
|
||||
mv "english/SummerSlam_(2003)" "english/SummerSlam_2003"
|
||||
mv "english/Over_the_Edge_(1999)" "english/Over_the_Edge_1999"
|
||||
mv "english/Lost_(TV_series)" "english/Lost_TV_series"
|
||||
mv "english/S._A._Andr%c3%a9e%27s_Arctic_Balloon_Expedition_of_1897" "english/S_A_Andres_Arctic_Balloon_Expedition_of_1897"
|
||||
|
||||
## path to NeMo repository, e.g. /home/user/NeMo
|
||||
NEMO_PATH=
|
||||
|
||||
INPUT_DIR="english"
|
||||
OUTPUT_DIR=${INPUT_DIR}_result
|
||||
|
||||
rm -rf $OUTPUT_DIR
|
||||
rm -rf ${INPUT_DIR}_prepared
|
||||
mkdir ${INPUT_DIR}_prepared
|
||||
mkdir ${INPUT_DIR}_prepared/audio
|
||||
mkdir ${INPUT_DIR}_prepared/text
|
||||
python ${NEMO_PATH}/scripts/dataset_processing/spoken_wikipedia/preprocess.py --input_folder ${INPUT_DIR} --destination_folder ${INPUT_DIR}_prepared
|
||||
|
||||
## Now we have ${INPUT_DIR}_prepared folder with the following structure:
|
||||
## ├── audio
|
||||
## | ├── 1.ogg
|
||||
## | ├── 2.ogg
|
||||
## | ...
|
||||
## └── text
|
||||
## ├── 1.txt
|
||||
## ├── 2.txt
|
||||
## ...
|
||||
|
||||
MODEL_FOR_SEGMENTATION="stt_en_fastconformer_ctc_large"
|
||||
MODEL_FOR_RECOGNITION="stt_en_conformer_ctc_large"
|
||||
## We set this threshold as very permissive, later we will use other metrics for filtering
|
||||
THRESHOLD=-10
|
||||
|
||||
${NEMO_PATH}/tools/ctc_segmentation/run_segmentation.sh \
|
||||
--SCRIPTS_DIR=${NEMO_PATH}/tools/ctc_segmentation/scripts \
|
||||
--MODEL_NAME_OR_PATH=${MODEL_FOR_SEGMENTATION} \
|
||||
--DATA_DIR=${INPUT_DIR}_prepared \
|
||||
--OUTPUT_DIR=${OUTPUT_DIR} \
|
||||
--MIN_SCORE=${THRESHOLD}
|
||||
|
||||
# Thresholds for filtering
|
||||
CER_THRESHOLD=20
|
||||
WER_THRESHOLD=30
|
||||
CER_EDGE_THRESHOLD=30
|
||||
LEN_DIFF_RATIO_THRESHOLD=0.15
|
||||
EDGE_LEN=25
|
||||
BATCH_SIZE=1
|
||||
|
||||
${NEMO_PATH}/tools/ctc_segmentation/run_filter.sh \
|
||||
--SCRIPTS_DIR=${NEMO_PATH}/tools/ctc_segmentation/scripts \
|
||||
--MODEL_NAME_OR_PATH=${MODEL_FOR_RECOGNITION} \
|
||||
--BATCH_SIZE=${BATCH_SIZE} \
|
||||
--MANIFEST=$OUTPUT_DIR/manifests/manifest.json \
|
||||
--INPUT_AUDIO_DIR=${INPUT_DIR}_prepared/audio/ \
|
||||
--EDGE_LEN=${EDGE_LEN} \
|
||||
--CER_THRESHOLD=${CER_THRESHOLD} \
|
||||
--WER_THRESHOLD=${WER_THRESHOLD} \
|
||||
--CER_EDGE_THRESHOLD=${CER_EDGE_THRESHOLD} \
|
||||
--LEN_DIFF_RATIO_THRESHOLD=${LEN_DIFF_RATIO_THRESHOLD}
|
||||
|
||||
python ${NEMO_PATH}/examples/asr/speech_to_text_eval.py \
|
||||
dataset_manifest=${OUTPUT_DIR}/manifests/manifest_transcribed_metrics_filtered.json \
|
||||
use_cer=True \
|
||||
only_score_manifest=True
|
||||
|
||||
python ${NEMO_PATH}/examples/asr/speech_to_text_eval.py \
|
||||
dataset_manifest=${OUTPUT_DIR}/manifests/manifest_transcribed_metrics_filtered.json \
|
||||
use_cer=False \
|
||||
only_score_manifest=True
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
name: "ds_for_fastpitch_align"
|
||||
|
||||
manifest_filepath: "train_manifest.json"
|
||||
sup_data_path: "sup_data"
|
||||
sup_data_types: [ "align_prior_matrix", "pitch", "speaker_id"]
|
||||
phoneme_dict_path: "scripts/tts_dataset_files/zh/24finals/pinyin_dict_nv_22.10.txt"
|
||||
|
||||
dataset:
|
||||
_target_: nemo.collections.tts.data.dataset.TTSDataset
|
||||
manifest_filepath: ${manifest_filepath}
|
||||
sample_rate: 22050
|
||||
sup_data_path: ${sup_data_path}
|
||||
sup_data_types: ${sup_data_types}
|
||||
n_fft: 1024
|
||||
win_length: 1024
|
||||
hop_length: 256
|
||||
window: "hann"
|
||||
n_mels: 80
|
||||
lowfreq: 0
|
||||
highfreq: null
|
||||
max_duration: null
|
||||
min_duration: 0.1
|
||||
ignore_file: null
|
||||
trim: true
|
||||
trim_top_db: 50
|
||||
trim_frame_length: 1024
|
||||
trim_hop_length: 256
|
||||
pitch_fmin: 65.40639132514966
|
||||
pitch_fmax: 2093.004522404789
|
||||
|
||||
text_normalizer:
|
||||
_target_: nemo_text_processing.text_normalization.normalize.Normalizer
|
||||
lang: zh
|
||||
input_case: cased
|
||||
|
||||
text_normalizer_call_kwargs:
|
||||
verbose: false
|
||||
punct_pre_process: true
|
||||
punct_post_process: true
|
||||
|
||||
text_tokenizer:
|
||||
_target_: nemo.collections.common.tokenizers.text_to_speech.tts_tokenizers.ChinesePhonemesTokenizer
|
||||
punct: true
|
||||
apostrophe: true
|
||||
pad_with_space: true
|
||||
g2p:
|
||||
_target_: nemo.collections.tts.g2p.models.zh_cn_pinyin.ChineseG2p
|
||||
phoneme_dict: ${phoneme_dict_path}
|
||||
word_segmenter: jieba # Only jieba is supported now.
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. 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.
|
||||
|
||||
# Disclaimer:
|
||||
# Each user is responsible for checking the content of datasets and the applicable licenses and determining if suitable for the intended use.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import tarfile
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from nemo_text_processing.text_normalization.normalize import Normalizer
|
||||
from opencc import OpenCC
|
||||
|
||||
from nemo.utils.tar_utils import safe_extract
|
||||
|
||||
URL = "https://www.openslr.org/resources/93/data_aishell3.tgz"
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Prepare SF_bilingual dataset and create manifests with predefined split'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--data-root",
|
||||
type=Path,
|
||||
help="where the dataset will reside",
|
||||
default="./DataChinese/sf_bilingual_speech_zh_en_vv1/SF_bilingual/",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifests-path", type=Path, help="where the resulting manifests files will reside", default="./"
|
||||
)
|
||||
parser.add_argument("--val-size", default=0.01, type=float, help="eval set split")
|
||||
parser.add_argument("--test-size", default=0.01, type=float, help="test set split")
|
||||
parser.add_argument(
|
||||
"--seed-for-ds-split",
|
||||
default=100,
|
||||
type=float,
|
||||
help="Seed for deterministic split of train/dev/test, NVIDIA's default is 100",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def __maybe_download_file(source_url, destination_path):
|
||||
if not destination_path.exists():
|
||||
tmp_file_path = destination_path.with_suffix('.tmp')
|
||||
urllib.request.urlretrieve(source_url, filename=str(tmp_file_path))
|
||||
tmp_file_path.rename(destination_path)
|
||||
|
||||
|
||||
def __extract_file(filepath, data_dir):
|
||||
try:
|
||||
with tarfile.open(filepath) as tar:
|
||||
safe_extract(tar, str(data_dir))
|
||||
except Exception:
|
||||
print(f"Error while extracting {filepath}. Already extracted?")
|
||||
|
||||
|
||||
def __process_transcript(file_path: str):
|
||||
# Create directory for processed wav files
|
||||
Path(file_path / "processed").mkdir(parents=True, exist_ok=True)
|
||||
# Create zh-TW to zh-simplify converter
|
||||
cc = OpenCC('t2s')
|
||||
# Create normalizer
|
||||
text_normalizer = Normalizer(
|
||||
lang="zh",
|
||||
input_case="cased",
|
||||
overwrite_cache=True,
|
||||
cache_dir=str(file_path / "cache_dir"),
|
||||
)
|
||||
text_normalizer_call_kwargs = {"punct_pre_process": True, "punct_post_process": True}
|
||||
normalizer_call = lambda x: text_normalizer.normalize(x, **text_normalizer_call_kwargs)
|
||||
entries = []
|
||||
SPEAKER_LEN = 7
|
||||
|
||||
candidates = []
|
||||
speakers = set()
|
||||
with open(file_path / "train" / "content.txt", encoding="utf-8") as fin:
|
||||
for line in fin:
|
||||
content = line.split()
|
||||
wav_name, text = content[0], "".join(content[1::2]) + "。"
|
||||
wav_name = wav_name.replace(u'\ufeff', '')
|
||||
speaker = wav_name[:SPEAKER_LEN]
|
||||
speakers.add(speaker)
|
||||
wav_file = file_path / "train" / "wav" / speaker / wav_name
|
||||
assert os.path.exists(wav_file), f"{wav_file} not found!"
|
||||
duration = subprocess.check_output(["soxi", "-D", str(wav_file)])
|
||||
if float(duration) <= 3.0: # filter out wav files shorter than 3 seconds
|
||||
continue
|
||||
processed_file = file_path / "processed" / wav_name
|
||||
# convert wav to mono 22050HZ, 16 bit (as SFSpeech dataset)
|
||||
subprocess.run(["sox", str(wav_file), "-r", "22050", "-c", "1", "-b", "16", str(processed_file)])
|
||||
candidates.append((processed_file, duration, text, speaker))
|
||||
|
||||
# remapping the speakder to speaker_id (start from 1)
|
||||
remapping = {}
|
||||
for index, speaker in enumerate(sorted(speakers)):
|
||||
remapping[speaker] = index + 1
|
||||
|
||||
for processed_file, duration, text, speaker in candidates:
|
||||
simplified_text = cc.convert(text)
|
||||
normalized_text = normalizer_call(simplified_text)
|
||||
entry = {
|
||||
'audio_filepath': os.path.abspath(processed_file),
|
||||
'duration': float(duration),
|
||||
'text': text,
|
||||
'normalized_text': normalized_text,
|
||||
'speaker_raw': speaker,
|
||||
'speaker': remapping[speaker],
|
||||
}
|
||||
|
||||
entries.append(entry)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def __process_data(dataset_path, val_size, test_size, seed_for_ds_split, manifests_dir):
|
||||
entries = __process_transcript(dataset_path)
|
||||
|
||||
random.Random(seed_for_ds_split).shuffle(entries)
|
||||
|
||||
train_size = 1.0 - val_size - test_size
|
||||
train_entries, validate_entries, test_entries = np.split(
|
||||
entries, [int(len(entries) * train_size), int(len(entries) * (train_size + val_size))]
|
||||
)
|
||||
|
||||
assert len(train_entries) > 0, "Not enough data for train, val and test"
|
||||
|
||||
def save(p, data):
|
||||
with open(p, 'w') as f:
|
||||
for d in data:
|
||||
f.write(json.dumps(d) + '\n')
|
||||
|
||||
save(manifests_dir / "train_manifest.json", train_entries)
|
||||
save(manifests_dir / "val_manifest.json", validate_entries)
|
||||
save(manifests_dir / "test_manifest.json", test_entries)
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
|
||||
tarred_data_path = args.data_root / "data_aishell3.tgz"
|
||||
|
||||
__maybe_download_file(URL, tarred_data_path)
|
||||
__extract_file(str(tarred_data_path), str(args.data_root))
|
||||
|
||||
__process_data(
|
||||
args.data_root,
|
||||
args.val_size,
|
||||
args.test_size,
|
||||
args.seed_for_ds_split,
|
||||
args.manifests_path,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,230 @@
|
||||
# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. 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.
|
||||
|
||||
"""
|
||||
This script is to compute global and speaker-level feature statistics for a given TTS training manifest.
|
||||
|
||||
This script should be run after compute_features.py as it loads the precomputed feature data.
|
||||
|
||||
$ python <nemo_root_path>/scripts/dataset_processing/tts/compute_feature_stats.py \
|
||||
--feature_config_path=<nemo_root_path>/examples/tts/conf/features/feature_22050.yaml
|
||||
--manifest_path=<data_root_path>/manifest1.json \
|
||||
--manifest_path=<data_root_path>/manifest2.json \
|
||||
--audio_dir=<data_root_path>/audio1 \
|
||||
--audio_dir=<data_root_path>/audio2 \
|
||||
--feature_dir=<data_root_path>/features1 \
|
||||
--feature_dir=<data_root_path>/features2 \
|
||||
--stats_path=<data_root_path>/feature_stats.json
|
||||
|
||||
The output dictionary will contain the feature statistics for every speaker, as well as a "default" entry
|
||||
with the global statistics.
|
||||
|
||||
For example:
|
||||
|
||||
{
|
||||
"default": {
|
||||
"pitch_mean": 100.0,
|
||||
"pitch_std": 50.0,
|
||||
"energy_mean": 7.5,
|
||||
"energy_std": 4.5
|
||||
},
|
||||
"speaker1": {
|
||||
"pitch_mean": 105.0,
|
||||
"pitch_std": 45.0,
|
||||
"energy_mean": 7.0,
|
||||
"energy_std": 5.0
|
||||
},
|
||||
"speaker2": {
|
||||
"pitch_mean": 110.0,
|
||||
"pitch_std": 30.0,
|
||||
"energy_mean": 5.0,
|
||||
"energy_std": 2.5
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
import torch
|
||||
from omegaconf import OmegaConf
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import read_manifest
|
||||
from nemo.core.classes.common import safe_instantiate
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
description="Compute TTS feature statistics.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--feature_config_path",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to feature config file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifest_path",
|
||||
required=True,
|
||||
type=Path,
|
||||
action="append",
|
||||
help="Path(s) to training manifest.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--audio_dir",
|
||||
required=True,
|
||||
type=Path,
|
||||
action="append",
|
||||
help="Path(s) to base directory with audio data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--feature_dir",
|
||||
required=True,
|
||||
type=Path,
|
||||
action="append",
|
||||
help="Path(s) to directory where feature data was stored.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--feature_names",
|
||||
default="pitch,energy",
|
||||
type=str,
|
||||
help="Comma separated list of features to process.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mask_field",
|
||||
default="voiced_mask",
|
||||
type=str,
|
||||
help="If provided, stat computation will ignore non-masked frames.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stats_path",
|
||||
default=Path("feature_stats.json"),
|
||||
type=Path,
|
||||
help="Path to output JSON file with dataset feature statistics.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Whether to overwrite the output stats file if it exists.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def _compute_stats(values: List[torch.Tensor]) -> Tuple[float, float]:
|
||||
values_tensor = torch.cat(values, dim=0)
|
||||
mean = values_tensor.mean().item()
|
||||
std = values_tensor.std(dim=0).item()
|
||||
return mean, std
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
|
||||
feature_config_path = args.feature_config_path
|
||||
manifest_paths = args.manifest_path
|
||||
audio_dirs = args.audio_dir
|
||||
feature_dirs = args.feature_dir
|
||||
feature_name_str = args.feature_names
|
||||
mask_field = args.mask_field
|
||||
stats_path = args.stats_path
|
||||
overwrite = args.overwrite
|
||||
|
||||
if not (len(manifest_paths) == len(audio_dirs) == len(feature_dirs)):
|
||||
raise ValueError(
|
||||
f"Need same number of manifest, audio_dir, and feature_dir. Received: "
|
||||
f"{len(manifest_paths)}, "
|
||||
f"{len(audio_dirs)}, "
|
||||
f"{len(feature_dirs)}"
|
||||
)
|
||||
|
||||
for manifest_path, audio_dir, feature_dir in zip(manifest_paths, audio_dirs, feature_dirs):
|
||||
if not manifest_path.exists():
|
||||
raise ValueError(f"Manifest {manifest_path} does not exist.")
|
||||
|
||||
if not audio_dir.exists():
|
||||
raise ValueError(f"Audio directory {audio_dir} does not exist.")
|
||||
|
||||
if not feature_dir.exists():
|
||||
raise ValueError(
|
||||
f"Feature directory {feature_dir} does not exist. "
|
||||
f"Please check that the path is correct and that you ran compute_features.py"
|
||||
)
|
||||
|
||||
if stats_path.exists():
|
||||
if overwrite:
|
||||
print(f"Will overwrite existing stats path: {stats_path}")
|
||||
else:
|
||||
raise ValueError(f"Stats path already exists: {stats_path}")
|
||||
|
||||
feature_config = OmegaConf.load(feature_config_path)
|
||||
feature_config = safe_instantiate(feature_config)
|
||||
featurizer_dict = feature_config.featurizers
|
||||
|
||||
print(f"Found featurizers for {list(featurizer_dict.keys())}.")
|
||||
featurizers = featurizer_dict.values()
|
||||
|
||||
feature_names = feature_name_str.split(",")
|
||||
# For each feature, we have a dictionary mapping speaker IDs to a list containing all features
|
||||
# for that speaker
|
||||
feature_stats = {name: defaultdict(list) for name in feature_names}
|
||||
|
||||
for manifest_path, audio_dir, feature_dir in zip(manifest_paths, audio_dirs, feature_dirs):
|
||||
entries = read_manifest(manifest_path)
|
||||
|
||||
for entry in tqdm(entries):
|
||||
speaker = entry["speaker"]
|
||||
|
||||
entry_dict = {}
|
||||
for featurizer in featurizers:
|
||||
feature_dict = featurizer.load(manifest_entry=entry, audio_dir=audio_dir, feature_dir=feature_dir)
|
||||
entry_dict.update(feature_dict)
|
||||
|
||||
if mask_field:
|
||||
mask = entry_dict[mask_field]
|
||||
else:
|
||||
mask = None
|
||||
|
||||
for feature_name in feature_names:
|
||||
values = entry_dict[feature_name]
|
||||
if mask is not None:
|
||||
values = values[mask]
|
||||
|
||||
feature_stat_dict = feature_stats[feature_name]
|
||||
feature_stat_dict["default"].append(values)
|
||||
feature_stat_dict[speaker].append(values)
|
||||
|
||||
stat_dict = defaultdict(dict)
|
||||
for feature_name in feature_names:
|
||||
mean_key = f"{feature_name}_mean"
|
||||
std_key = f"{feature_name}_std"
|
||||
feature_stat_dict = feature_stats[feature_name]
|
||||
for speaker_id, values in feature_stat_dict.items():
|
||||
speaker_mean, speaker_std = _compute_stats(values)
|
||||
stat_dict[speaker_id][mean_key] = speaker_mean
|
||||
stat_dict[speaker_id][std_key] = speaker_std
|
||||
|
||||
with open(stats_path, 'w', encoding="utf-8") as stats_f:
|
||||
json.dump(stat_dict, stats_f, indent=4)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,130 @@
|
||||
# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. 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.
|
||||
|
||||
"""
|
||||
This script computes features for TTS models prior to training, such as pitch and energy.
|
||||
The resulting features will be stored in the provided 'feature_dir'.
|
||||
|
||||
$ python <nemo_root_path>/scripts/dataset_processing/tts/compute_features.py \
|
||||
--feature_config_path=<nemo_root_path>/examples/tts/conf/features/feature_22050.yaml \
|
||||
--manifest_path=<data_root_path>/manifest.json \
|
||||
--audio_dir=<data_root_path>/audio \
|
||||
--feature_dir=<data_root_path>/features \
|
||||
--overwrite \
|
||||
--num_workers=1
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from joblib import Parallel, delayed
|
||||
from omegaconf import OmegaConf
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import read_manifest
|
||||
from nemo.core.classes.common import safe_instantiate
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
description="Compute TTS features.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--feature_config_path",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to feature config file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifest_path",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to training manifest.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--audio_dir",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to base directory with audio data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--feature_dir",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to directory where feature data will be stored.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dedupe_files",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="If given, will only process the first manifest entry found for each audio file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Whether to overwrite existing feature files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num_workers", default=1, type=int, help="Number of parallel threads to use. If -1 all CPUs are used."
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
feature_config_path = args.feature_config_path
|
||||
manifest_path = args.manifest_path
|
||||
audio_dir = args.audio_dir
|
||||
feature_dir = args.feature_dir
|
||||
dedupe_files = args.dedupe_files
|
||||
overwrite = args.overwrite
|
||||
num_workers = args.num_workers
|
||||
|
||||
if not manifest_path.exists():
|
||||
raise ValueError(f"Manifest {manifest_path} does not exist.")
|
||||
|
||||
if not audio_dir.exists():
|
||||
raise ValueError(f"Audio directory {audio_dir} does not exist.")
|
||||
|
||||
feature_config = OmegaConf.load(feature_config_path)
|
||||
feature_config = safe_instantiate(feature_config)
|
||||
featurizers = feature_config.featurizers
|
||||
|
||||
entries = read_manifest(manifest_path)
|
||||
|
||||
if dedupe_files:
|
||||
final_entries = []
|
||||
audio_filepath_set = set()
|
||||
for entry in entries:
|
||||
audio_filepath = entry["audio_filepath"]
|
||||
if audio_filepath in audio_filepath_set:
|
||||
continue
|
||||
final_entries.append(entry)
|
||||
audio_filepath_set.add(audio_filepath)
|
||||
entries = final_entries
|
||||
|
||||
for feature_name, featurizer in featurizers.items():
|
||||
print(f"Computing: {feature_name}")
|
||||
Parallel(n_jobs=num_workers)(
|
||||
delayed(featurizer.save)(
|
||||
manifest_entry=entry, audio_dir=audio_dir, feature_dir=feature_dir, overwrite=overwrite
|
||||
)
|
||||
for entry in tqdm(entries)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,144 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. 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.
|
||||
|
||||
"""
|
||||
This script is to compute speaker-level statistics, such as pitch mean & standard deviation, for a given
|
||||
TTS training manifest.
|
||||
|
||||
This script should be run after extract_sup_data.py as it uses the precomputed supplemental features.
|
||||
|
||||
$ python <nemo_root_path>/scripts/dataset_processing/tts/compute_speaker_stats.py \
|
||||
--manifest_path=<data_root_path>/fastpitch_manifest.json \
|
||||
--sup_data_path=<data_root_path>/sup_data \
|
||||
--pitch_stats_path=<data_root_path>/pitch_stats.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
import torch
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import read_manifest
|
||||
from nemo.collections.tts.parts.utils.tts_dataset_utils import get_base_dir
|
||||
from nemo.collections.tts.torch.tts_data_types import Pitch
|
||||
from nemo.utils import logging
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
description="Compute speaker level pitch statistics.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifest_path",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to training manifest.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sup_data_path",
|
||||
default=Path("sup_data"),
|
||||
type=Path,
|
||||
help="Path to base directory with supplementary data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pitch_stats_path",
|
||||
default=Path("pitch_stats.json"),
|
||||
type=Path,
|
||||
help="Path to output JSON file with speaker pitch statistics.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def _compute_stats(values: List[torch.Tensor]) -> Tuple[float, float]:
|
||||
values_tensor = torch.cat(values, dim=0)
|
||||
mean = values_tensor.mean().item()
|
||||
std = values_tensor.std(dim=0).item()
|
||||
return mean, std
|
||||
|
||||
|
||||
def _get_sup_data_filepath(manifest_entry: dict, audio_dir: Path, sup_data_dir: Path) -> Path:
|
||||
"""
|
||||
Get the absolute path of a supplementary data type for the input manifest entry.
|
||||
|
||||
Example: audio_filepath "<audio_dir>/speaker1/audio1.wav" becomes "<sup_data_dir>/speaker1_audio1.pt"
|
||||
|
||||
Args:
|
||||
manifest_entry: Manifest entry dictionary.
|
||||
audio_dir: base directory where audio is stored.
|
||||
sup_data_dir: base directory where supplementary data is stored.
|
||||
|
||||
Returns:
|
||||
Path to the supplementary data file.
|
||||
"""
|
||||
audio_path = Path(manifest_entry["audio_filepath"])
|
||||
rel_audio_path = audio_path.relative_to(audio_dir)
|
||||
rel_sup_data_path = rel_audio_path.with_suffix(".pt")
|
||||
sup_data_filename = str(rel_sup_data_path).replace(os.sep, "_")
|
||||
sup_data_filepath = sup_data_dir / sup_data_filename
|
||||
return sup_data_filepath
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
manifest_path = args.manifest_path
|
||||
sup_data_path = args.sup_data_path
|
||||
pitch_stats_path = args.pitch_stats_path
|
||||
|
||||
pitch_data_path = Path(os.path.join(sup_data_path, Pitch.name))
|
||||
if not os.path.exists(pitch_data_path):
|
||||
raise ValueError(
|
||||
f"Pitch directory {pitch_data_path} does not exist. Make sure 'sup_data_path' is correct "
|
||||
f"and that you have computed the pitch using extract_sup_data.py"
|
||||
)
|
||||
|
||||
entries = read_manifest(manifest_path)
|
||||
|
||||
audio_paths = [entry["audio_filepath"] for entry in entries]
|
||||
base_dir = get_base_dir(audio_paths)
|
||||
|
||||
global_pitch_values = []
|
||||
speaker_pitch_values = defaultdict(list)
|
||||
for entry in tqdm(entries):
|
||||
pitch_path = _get_sup_data_filepath(manifest_entry=entry, audio_dir=base_dir, sup_data_dir=pitch_data_path)
|
||||
if not os.path.exists(pitch_path):
|
||||
logging.warning(f"Unable to find pitch file for {entry}")
|
||||
continue
|
||||
|
||||
pitch = torch.load(pitch_path)
|
||||
# Filter out non-speech frames
|
||||
pitch = pitch[pitch != 0]
|
||||
global_pitch_values.append(pitch)
|
||||
if "speaker" in entry:
|
||||
speaker_id = entry["speaker"]
|
||||
speaker_pitch_values[speaker_id].append(pitch)
|
||||
|
||||
global_pitch_mean, global_pitch_std = _compute_stats(global_pitch_values)
|
||||
pitch_stats = {"default": {"pitch_mean": global_pitch_mean, "pitch_std": global_pitch_std}}
|
||||
for speaker_id, pitch_values in speaker_pitch_values.items():
|
||||
pitch_mean, pitch_std = _compute_stats(pitch_values)
|
||||
pitch_stats[speaker_id] = {"pitch_mean": pitch_mean, "pitch_std": pitch_std}
|
||||
|
||||
with open(pitch_stats_path, 'w', encoding="utf-8") as stats_f:
|
||||
json.dump(pitch_stats, stats_f, indent=4)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,104 @@
|
||||
# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. 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.
|
||||
|
||||
"""
|
||||
This script takes a list of TTS manifests and creates a JSON mapping the input speaker names to
|
||||
unique indices for multi-speaker TTS training.
|
||||
|
||||
To ensure that speaker names are unique across datasets, it is recommended that you prepend the speaker
|
||||
names in your manifest with the name of the dataset.
|
||||
|
||||
$ python <nemo_root_path>/scripts/dataset_processing/tts/create_speaker_map.py \
|
||||
--manifest_path=manifest1.json \
|
||||
--manifest_path=manifest2.json \
|
||||
--speaker_map_path=speakers.json
|
||||
|
||||
Example output:
|
||||
|
||||
{
|
||||
"vctk_p225": 0,
|
||||
"vctk_p226": 1,
|
||||
"vctk_p227": 2,
|
||||
...
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import read_manifest
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
description="Create mapping from speaker names to numerical speaker indices.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifest_path",
|
||||
required=True,
|
||||
type=Path,
|
||||
action="append",
|
||||
help="Path to training manifest(s).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--speaker_map_path",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path for output speaker index JSON",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Whether to overwrite the output speaker file if it exists.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
manifest_paths = args.manifest_path
|
||||
speaker_map_path = args.speaker_map_path
|
||||
overwrite = args.overwrite
|
||||
|
||||
for manifest_path in manifest_paths:
|
||||
if not manifest_path.exists():
|
||||
raise ValueError(f"Manifest {manifest_path} does not exist.")
|
||||
|
||||
if speaker_map_path.exists():
|
||||
if overwrite:
|
||||
print(f"Will overwrite existing speaker path: {speaker_map_path}")
|
||||
else:
|
||||
raise ValueError(f"Speaker path already exists: {speaker_map_path}")
|
||||
|
||||
speaker_set = set()
|
||||
for manifest_path in manifest_paths:
|
||||
entries = read_manifest(manifest_path)
|
||||
for entry in entries:
|
||||
speaker = str(entry["speaker"])
|
||||
speaker_set.add(speaker)
|
||||
|
||||
speaker_list = list(speaker_set)
|
||||
speaker_list.sort()
|
||||
speaker_index_map = {speaker_list[i]: i for i in range(len(speaker_list))}
|
||||
|
||||
with open(speaker_map_path, 'w', encoding="utf-8") as stats_f:
|
||||
json.dump(speaker_index_map, stats_f, indent=4)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,62 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. 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 torch
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.core.classes.common import safe_instantiate
|
||||
from nemo.core.config import hydra_runner
|
||||
|
||||
|
||||
def get_pitch_stats(pitch_list):
|
||||
pitch_tensor = torch.cat(pitch_list)
|
||||
pitch_mean, pitch_std = pitch_tensor.mean().item(), pitch_tensor.std().item()
|
||||
pitch_min, pitch_max = pitch_tensor.min().item(), pitch_tensor.max().item()
|
||||
print(f"PITCH_MEAN={pitch_mean}, PITCH_STD={pitch_std}")
|
||||
print(f"PITCH_MIN={pitch_min}, PITCH_MAX={pitch_max}")
|
||||
|
||||
|
||||
def preprocess_ds_for_fastpitch_align(dataloader):
|
||||
pitch_list = []
|
||||
for batch in tqdm(dataloader, total=len(dataloader)):
|
||||
audios, audio_lengths, tokens, tokens_lengths, align_prior_matrices, pitches, pitches_lengths, *_ = batch
|
||||
pitch = pitches.squeeze(0)
|
||||
pitch_list.append(pitch[pitch != 0])
|
||||
|
||||
get_pitch_stats(pitch_list)
|
||||
|
||||
|
||||
CFG_NAME2FUNC = {
|
||||
"ds_for_fastpitch_align": preprocess_ds_for_fastpitch_align,
|
||||
"ds_for_mixer_tts": preprocess_ds_for_fastpitch_align,
|
||||
}
|
||||
|
||||
|
||||
@hydra_runner(config_path='ljspeech/ds_conf', config_name='ds_for_fastpitch_align')
|
||||
def main(cfg):
|
||||
dataset = safe_instantiate(cfg.dataset)
|
||||
dataloader = torch.utils.data.DataLoader(
|
||||
dataset=dataset,
|
||||
batch_size=1,
|
||||
collate_fn=dataset._collate_fn,
|
||||
num_workers=cfg.get("dataloader_params", {}).get("num_workers", 4),
|
||||
)
|
||||
|
||||
print(f"Processing {cfg.manifest_filepath}:")
|
||||
CFG_NAME2FUNC[cfg.name](dataloader)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main() # noqa pylint: disable=no-value-for-parameter
|
||||
@@ -0,0 +1,185 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. 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.
|
||||
|
||||
"""
|
||||
This script is to generate mel spectrograms from a Fastpitch model checkpoint. Please see general usage below. It runs
|
||||
on GPUs by default, but you can add `--num-workers 5 --cpu` as an option to run on CPUs.
|
||||
|
||||
$ python scripts/dataset_processing/tts/generate_mels.py \
|
||||
--fastpitch-model-ckpt ./models/fastpitch/multi_spk/FastPitch--val_loss\=1.4473-epoch\=209.ckpt \
|
||||
--input-json-manifests /home/xueyang/HUI-Audio-Corpus-German-clean/test_manifest_text_normed_phonemes.json
|
||||
--output-json-manifest-root /home/xueyang/experiments/multi_spk_tts_de
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
import torch
|
||||
from joblib import Parallel, delayed
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.collections.tts.models import FastPitchModel
|
||||
from nemo.collections.tts.parts.utils.tts_dataset_utils import (
|
||||
BetaBinomialInterpolator,
|
||||
beta_binomial_prior_distribution,
|
||||
)
|
||||
from nemo.utils import logging
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
description="Generate mel spectrograms with pretrained FastPitch model, and create manifests for finetuning Hifigan.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fastpitch-model-ckpt",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Specify a full path of a fastpitch model checkpoint with the suffix of either .ckpt or .nemo.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-json-manifests",
|
||||
nargs="+",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Specify a full path of a JSON manifest. You could add multiple manifests.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-json-manifest-root",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Specify a full path of output root that would contain new manifests.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-workers",
|
||||
default=-1,
|
||||
type=int,
|
||||
help="Specify the max number of concurrently Python workers processes. "
|
||||
"If -1 all CPUs are used. If 1 no parallel computing is used.",
|
||||
)
|
||||
parser.add_argument("--cpu", action='store_true', default=False, help="Generate mel spectrograms using CPUs.")
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def __load_wav(audio_file):
|
||||
with sf.SoundFile(audio_file, 'r') as f:
|
||||
samples = f.read(dtype='float32')
|
||||
return samples.transpose()
|
||||
|
||||
|
||||
def __generate_mels(entry, spec_model, device, use_beta_binomial_interpolator, mel_root):
|
||||
# Generate a spectrograms (we need to use ground truth alignment for correct matching between audio and mels)
|
||||
audio = __load_wav(entry["audio_filepath"])
|
||||
audio = torch.from_numpy(audio).unsqueeze(0).to(device)
|
||||
audio_len = torch.tensor(audio.shape[1], dtype=torch.long, device=device).unsqueeze(0)
|
||||
|
||||
if spec_model.fastpitch.speaker_emb is not None and "speaker" in entry:
|
||||
speaker = torch.tensor([entry['speaker']]).to(device)
|
||||
else:
|
||||
speaker = None
|
||||
|
||||
with torch.no_grad():
|
||||
if "normalized_text" in entry:
|
||||
text = spec_model.parse(entry["normalized_text"], normalize=False)
|
||||
else:
|
||||
text = spec_model.parse(entry['text'])
|
||||
|
||||
text_len = torch.tensor(text.shape[-1], dtype=torch.long, device=device).unsqueeze(0)
|
||||
spect, spect_len = spec_model.preprocessor(input_signal=audio, length=audio_len)
|
||||
|
||||
# Generate attention prior and spectrogram inputs for HiFi-GAN
|
||||
if use_beta_binomial_interpolator:
|
||||
beta_binomial_interpolator = BetaBinomialInterpolator()
|
||||
attn_prior = (
|
||||
torch.from_numpy(beta_binomial_interpolator(spect_len.item(), text_len.item()))
|
||||
.unsqueeze(0)
|
||||
.to(text.device)
|
||||
)
|
||||
else:
|
||||
attn_prior = (
|
||||
torch.from_numpy(beta_binomial_prior_distribution(text_len.item(), spect_len.item()))
|
||||
.unsqueeze(0)
|
||||
.to(text.device)
|
||||
)
|
||||
|
||||
spectrogram = spec_model.forward(
|
||||
text=text,
|
||||
input_lens=text_len,
|
||||
spec=spect,
|
||||
mel_lens=spect_len,
|
||||
attn_prior=attn_prior,
|
||||
speaker=speaker,
|
||||
)[0]
|
||||
|
||||
save_path = mel_root / f"{Path(entry['audio_filepath']).stem}.npy"
|
||||
np.save(save_path, spectrogram[0].to('cpu').numpy())
|
||||
entry["mel_filepath"] = str(save_path)
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
ckpt_path = args.fastpitch_model_ckpt
|
||||
input_manifest_filepaths = args.input_json_manifests
|
||||
output_json_manifest_root = args.output_json_manifest_root
|
||||
|
||||
mel_root = output_json_manifest_root / "mels"
|
||||
mel_root.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# load pretrained FastPitch model checkpoint
|
||||
suffix = ckpt_path.suffix
|
||||
if suffix == ".nemo":
|
||||
spec_model = FastPitchModel.restore_from(ckpt_path).eval()
|
||||
elif suffix == ".ckpt":
|
||||
spec_model = FastPitchModel.load_from_checkpoint(ckpt_path).eval()
|
||||
else:
|
||||
raise ValueError(f"Unsupported suffix: {suffix}")
|
||||
if not args.cpu:
|
||||
spec_model.cuda()
|
||||
device = spec_model.device
|
||||
|
||||
use_beta_binomial_interpolator = spec_model.cfg.train_ds.dataset.get("use_beta_binomial_interpolator", False)
|
||||
|
||||
for manifest in input_manifest_filepaths:
|
||||
logging.info(f"Processing {manifest}.")
|
||||
entries = []
|
||||
with open(manifest, "r") as fjson:
|
||||
for line in fjson:
|
||||
entries.append(json.loads(line.strip()))
|
||||
|
||||
if device == "cpu":
|
||||
new_entries = Parallel(n_jobs=args.num_workers)(
|
||||
delayed(__generate_mels)(entry, spec_model, device, use_beta_binomial_interpolator, mel_root)
|
||||
for entry in entries
|
||||
)
|
||||
else:
|
||||
new_entries = []
|
||||
for entry in tqdm(entries):
|
||||
new_entry = __generate_mels(entry, spec_model, device, use_beta_binomial_interpolator, mel_root)
|
||||
new_entries.append(new_entry)
|
||||
|
||||
mel_manifest_path = output_json_manifest_root / f"{manifest.stem}_mel{manifest.suffix}"
|
||||
with open(mel_manifest_path, "w") as fmel:
|
||||
for entry in new_entries:
|
||||
fmel.write(json.dumps(entry) + "\n")
|
||||
logging.info(f"Processing {manifest} is complete --> {mel_manifest_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,123 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. 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
|
||||
import glob
|
||||
import json
|
||||
import re
|
||||
import tarfile
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.utils.tar_utils import safe_extract
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(description='Download HiFiTTS and create manifests with predefined split')
|
||||
parser.add_argument(
|
||||
"--data-root",
|
||||
required=True,
|
||||
type=Path,
|
||||
help='Directory into which to download and extract dataset. \{data-root\}/hi_fi_tts_v0 will be created.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--split',
|
||||
type=str,
|
||||
default='all',
|
||||
help='Choose to generate manifest for all or one of (train, test, split), note that this will still download the full dataset.',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
URL = "https://us.openslr.org/resources/109/hi_fi_tts_v0.tar.gz"
|
||||
|
||||
|
||||
def __maybe_download_file(source_url, destination_path):
|
||||
if not destination_path.exists():
|
||||
tmp_file_path = destination_path.with_suffix('.tmp')
|
||||
urllib.request.urlretrieve(source_url, filename=str(tmp_file_path))
|
||||
tmp_file_path.rename(destination_path)
|
||||
|
||||
|
||||
def __extract_file(filepath, data_dir):
|
||||
try:
|
||||
with tarfile.open(filepath) as tar:
|
||||
safe_extract(tar, str(data_dir))
|
||||
except Exception:
|
||||
print(f"Error while extracting {filepath}. Already extracted?")
|
||||
|
||||
|
||||
def __process_data(data_root, filelists):
|
||||
# Create manifests (based on predefined NVIDIA's split)
|
||||
for split in tqdm(filelists):
|
||||
manifest_target = data_root / f"{split}_manifest.json"
|
||||
print(f"Creating manifest for {split}.")
|
||||
|
||||
entries = []
|
||||
for manifest_src in glob.glob(str(data_root / f"*_{split}.json")):
|
||||
try:
|
||||
search_res = re.search('.*\/([0-9]+)_manifest_([a-z]+)_.*.json', manifest_src)
|
||||
speaker_id = search_res.group(1)
|
||||
audio_quality = search_res.group(2)
|
||||
except Exception:
|
||||
print(f"Failed to find speaker id or audio quality for {manifest_src}, check formatting.")
|
||||
continue
|
||||
|
||||
with open(manifest_src, 'r') as f_in:
|
||||
for input_json_entry in f_in:
|
||||
data = json.loads(input_json_entry)
|
||||
|
||||
# Make sure corresponding wavfile exists
|
||||
wav_path = data_root / data['audio_filepath']
|
||||
assert wav_path.exists(), f"{wav_path} does not exist!"
|
||||
|
||||
entry = {
|
||||
'audio_filepath': data['audio_filepath'],
|
||||
'duration': data['duration'],
|
||||
'text': data['text'],
|
||||
'normalized_text': data['text_normalized'],
|
||||
'speaker': int(speaker_id),
|
||||
# Audio_quality is either clean or other.
|
||||
# The clean set includes recordings with high sound-to-noise ratio and wide bandwidth.
|
||||
# The books with noticeable noise or narrow bandwidth are included in the other subset.
|
||||
# Note: some speaker_id's have both clean and other audio quality.
|
||||
'audio_quality': audio_quality,
|
||||
}
|
||||
entries.append(entry)
|
||||
|
||||
with open(manifest_target, 'w') as f_out:
|
||||
for m in entries:
|
||||
f_out.write(json.dumps(m) + '\n')
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
|
||||
split = ['train', 'dev', 'test'] if args.split == 'all' else list(args.split)
|
||||
|
||||
tarred_data_path = args.data_root / "hi_fi_tts_v0.tar.gz"
|
||||
|
||||
__maybe_download_file(URL, tarred_data_path)
|
||||
__extract_file(str(tarred_data_path), str(args.data_root))
|
||||
|
||||
data_root = args.data_root / "hi_fi_tts_v0"
|
||||
__process_data(data_root, split)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,45 @@
|
||||
name: "ds_for_fastpitch_align"
|
||||
|
||||
manifest_filepath: ???
|
||||
sup_data_path: ???
|
||||
sup_data_types: [ "align_prior_matrix", "pitch" ]
|
||||
|
||||
dataset:
|
||||
_target_: nemo.collections.tts.data.dataset.TTSDataset
|
||||
manifest_filepath: ${manifest_filepath}
|
||||
sample_rate: 44100
|
||||
sup_data_path: ${sup_data_path}
|
||||
sup_data_types: ${sup_data_types}
|
||||
n_fft: 2048
|
||||
win_length: 2048
|
||||
hop_length: 512
|
||||
window: "hann"
|
||||
n_mels: 80
|
||||
lowfreq: 0
|
||||
highfreq: null
|
||||
max_duration: 15
|
||||
min_duration: 0.1
|
||||
ignore_file: null
|
||||
trim: false
|
||||
pitch_fmin: 65.40639132514966
|
||||
pitch_fmax: 2093.004522404789
|
||||
use_beta_binomial_interpolator: false
|
||||
|
||||
text_normalizer:
|
||||
_target_: nemo_text_processing.text_normalization.normalize.Normalizer
|
||||
lang: de
|
||||
input_case: cased
|
||||
|
||||
text_normalizer_call_kwargs:
|
||||
verbose: false
|
||||
punct_pre_process: true
|
||||
punct_post_process: true
|
||||
|
||||
text_tokenizer:
|
||||
_target_: nemo.collections.common.tokenizers.text_to_speech.tts_tokenizers.GermanPhonemesTokenizer
|
||||
punct: true
|
||||
apostrophe: true
|
||||
pad_with_space: true
|
||||
|
||||
dataloader_params:
|
||||
num_workers: 12
|
||||
@@ -0,0 +1,334 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. 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
|
||||
import json
|
||||
import random
|
||||
import shutil
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
from joblib import Parallel, delayed
|
||||
from tqdm import tqdm
|
||||
|
||||
try:
|
||||
from nemo_text_processing.text_normalization.normalize import Normalizer
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
raise ModuleNotFoundError(
|
||||
"The package `nemo_text_processing` was not installed in this environment. Please refer to"
|
||||
" https://github.com/NVIDIA/NeMo-text-processing and install this package before using "
|
||||
"this script"
|
||||
)
|
||||
|
||||
from nemo.utils import logging
|
||||
|
||||
# full corpus.
|
||||
URLS_FULL = {
|
||||
"Bernd_Ungerer": "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/dataset_full/Bernd_Ungerer.zip",
|
||||
"Eva_K": "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/dataset_full/Eva_K.zip",
|
||||
"Friedrich": "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/dataset_full/Friedrich.zip",
|
||||
"Hokuspokus": "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/dataset_full/Hokuspokus.zip",
|
||||
"Karlsson": "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/dataset_full/Karlsson.zip",
|
||||
"others": "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/dataset_full/others.zip",
|
||||
}
|
||||
URL_STATS_FULL = "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/datasetStatistic.zip"
|
||||
|
||||
# the clean subset of the full corpus.
|
||||
URLS_CLEAN = {
|
||||
"Bernd_Ungerer": "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/dataset_clean/Bernd_Ungerer_Clean.zip",
|
||||
"Eva_K": "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/dataset_clean/Eva_K_Clean.zip",
|
||||
"Friedrich": "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/dataset_clean/Friedrich_Clean.zip",
|
||||
"Hokuspokus": "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/dataset_clean/Hokuspokus_Clean.zip",
|
||||
"Karlsson": "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/dataset_clean/Karlsson_Clean.zip",
|
||||
"others": "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/dataset_clean/others_Clean.zip",
|
||||
}
|
||||
URL_STATS_CLEAN = "https://opendata.iisys.de/opendata/Datasets/HUI-Audio-Corpus-German/datasetStatisticClean.zip"
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
description="Download HUI-Audio-Corpus-German and create manifests with predefined split. "
|
||||
"Please check details about the corpus in https://github.com/iisys-hof/HUI-Audio-Corpus-German.",
|
||||
)
|
||||
parser.add_argument("--data-root", required=True, type=Path, help="where the resulting dataset will reside.")
|
||||
parser.add_argument("--manifests-root", required=True, type=Path, help="where the manifests files will reside.")
|
||||
parser.add_argument("--set-type", default="clean", choices=["full", "clean"], type=str)
|
||||
parser.add_argument("--min-duration", default=0.1, type=float)
|
||||
parser.add_argument("--max-duration", default=15, type=float)
|
||||
parser.add_argument(
|
||||
"--num-workers",
|
||||
default=-1,
|
||||
type=int,
|
||||
help="Specify the max number of concurrently Python workers processes. "
|
||||
"If -1 all CPUs are used. If 1 no parallel computing is used.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--normalize-text",
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="Normalize original text and add a new entry 'normalized_text' to .json file if True.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--val-num-utts-per-speaker",
|
||||
default=1,
|
||||
type=int,
|
||||
help="Specify the number of utterances for each speaker in val split. All speakers are covered.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--test-num-utts-per-speaker",
|
||||
default=1,
|
||||
type=int,
|
||||
help="Specify the number of utterances for each speaker in test split. All speakers are covered.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seed-for-ds-split",
|
||||
default=100,
|
||||
type=float,
|
||||
help="Seed for deterministic split of train/dev/test, NVIDIA's default is 100",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def __maybe_download_file(source_url, destination_path):
|
||||
if not destination_path.exists():
|
||||
logging.info(f"Downloading data: {source_url} --> {destination_path}")
|
||||
tmp_file_path = destination_path.with_suffix(".tmp")
|
||||
urllib.request.urlretrieve(source_url, filename=tmp_file_path)
|
||||
tmp_file_path.rename(destination_path)
|
||||
else:
|
||||
logging.info(f"Skipped downloading data because it exists: {destination_path}")
|
||||
|
||||
|
||||
def __extract_file(filepath, data_dir):
|
||||
logging.info(f"Unzipping data: {filepath} --> {data_dir}")
|
||||
shutil.unpack_archive(filepath, data_dir)
|
||||
logging.info(f"Unzipping data is complete: {filepath}.")
|
||||
|
||||
|
||||
def __save_json(json_file, dict_list):
|
||||
logging.info(f"Saving JSON split to {json_file}.")
|
||||
with open(json_file, "w") as f:
|
||||
for d in dict_list:
|
||||
f.write(json.dumps(d) + "\n")
|
||||
|
||||
|
||||
def __process_data(
|
||||
dataset_path,
|
||||
stat_path_root,
|
||||
speaker_id,
|
||||
min_duration,
|
||||
max_duration,
|
||||
val_size,
|
||||
test_size,
|
||||
seed_for_ds_split,
|
||||
):
|
||||
logging.info(f"Preparing JSON split for speaker {speaker_id}.")
|
||||
# parse statistic.txt
|
||||
stat_path = stat_path_root / "statistic.txt"
|
||||
with open(stat_path, 'r') as fstat:
|
||||
lines = fstat.readlines()
|
||||
num_utts = int(lines[4].strip().split()[-1])
|
||||
hours = round(float(lines[9].strip().split()[-1]) / 3600.0, 2)
|
||||
|
||||
# parse overview.csv to generate JSON splits.
|
||||
overview_path = stat_path_root / "overview.csv"
|
||||
entries = []
|
||||
with open(overview_path, 'r') as foverview:
|
||||
# Let's skip the header
|
||||
foverview.readline()
|
||||
for line in tqdm(foverview):
|
||||
file_stem, duration, *_, text = line.strip().split("|")
|
||||
duration = float(duration)
|
||||
|
||||
# file_stem -> dir_name (e.g. maerchen_01_f000051 -> maerchen)
|
||||
dir_name = "_".join(file_stem.split("_")[:-2])
|
||||
audio_path = dataset_path / dir_name / "wavs" / f"{file_stem}.wav"
|
||||
|
||||
if min_duration <= duration <= max_duration:
|
||||
entry = {
|
||||
"audio_filepath": str(audio_path),
|
||||
"duration": duration,
|
||||
"text": text,
|
||||
"speaker": speaker_id,
|
||||
}
|
||||
entries.append(entry)
|
||||
|
||||
random.Random(seed_for_ds_split).shuffle(entries)
|
||||
train_size = len(entries) - val_size - test_size
|
||||
if train_size <= 0:
|
||||
logging.warning(f"Skipped speaker {speaker_id}. Not enough data for train, val and test.")
|
||||
train, val, test, is_skipped = [], [], [], True
|
||||
else:
|
||||
logging.info(f"Preparing JSON split for speaker {speaker_id} is complete.")
|
||||
train, val, test, is_skipped = (
|
||||
entries[:train_size],
|
||||
entries[train_size : train_size + val_size],
|
||||
entries[train_size + val_size :],
|
||||
False,
|
||||
)
|
||||
|
||||
return {
|
||||
"train": train,
|
||||
"val": val,
|
||||
"test": test,
|
||||
"is_skipped": is_skipped,
|
||||
"hours": hours,
|
||||
"num_utts": num_utts,
|
||||
}
|
||||
|
||||
|
||||
def __text_normalization(json_file, num_workers=-1):
|
||||
text_normalizer_call_kwargs = {
|
||||
"punct_pre_process": True,
|
||||
"punct_post_process": True,
|
||||
}
|
||||
text_normalizer = Normalizer(
|
||||
lang="de",
|
||||
input_case="cased",
|
||||
overwrite_cache=True,
|
||||
cache_dir=str(json_file.parent / "cache_dir"),
|
||||
)
|
||||
|
||||
def normalizer_call(x):
|
||||
return text_normalizer.normalize(x, **text_normalizer_call_kwargs)
|
||||
|
||||
def add_normalized_text(line_dict):
|
||||
normalized_text = normalizer_call(line_dict["text"])
|
||||
line_dict.update({"normalized_text": normalized_text})
|
||||
return line_dict
|
||||
|
||||
logging.info(f"Normalizing text for {json_file}.")
|
||||
with open(json_file, 'r', encoding='utf-8') as fjson:
|
||||
lines = fjson.readlines()
|
||||
# Note: you need to verify which backend works well on your cluster.
|
||||
# backend="loky" is fine on multi-core Ubuntu OS; backend="threading" on Slurm.
|
||||
dict_list = Parallel(n_jobs=num_workers)(
|
||||
delayed(add_normalized_text)(json.loads(line)) for line in tqdm(lines)
|
||||
)
|
||||
|
||||
json_file_text_normed = json_file.parent / f"{json_file.stem}_text_normed{json_file.suffix}"
|
||||
with open(json_file_text_normed, 'w', encoding="utf-8") as fjson_norm:
|
||||
for dct in dict_list:
|
||||
fjson_norm.write(json.dumps(dct) + "\n")
|
||||
logging.info(f"Normalizing text is complete: {json_file} --> {json_file_text_normed}")
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
data_root = args.data_root
|
||||
manifests_root = args.manifests_root
|
||||
set_type = args.set_type
|
||||
|
||||
dataset_root = data_root / f"HUI-Audio-Corpus-German-{set_type}"
|
||||
dataset_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if set_type == "full":
|
||||
data_source = URLS_FULL
|
||||
stats_source = URL_STATS_FULL
|
||||
elif set_type == "clean":
|
||||
data_source = URLS_CLEAN
|
||||
stats_source = URL_STATS_CLEAN
|
||||
else:
|
||||
raise ValueError(f"Unknown {set_type}. Please choose either clean or full.")
|
||||
|
||||
# download and unzip dataset stats
|
||||
zipped_stats_path = dataset_root / Path(stats_source).name
|
||||
__maybe_download_file(stats_source, zipped_stats_path)
|
||||
__extract_file(zipped_stats_path, dataset_root)
|
||||
|
||||
# download datasets
|
||||
# Note: you need to verify which backend works well on your cluster.
|
||||
# backend="loky" is fine on multi-core Ubuntu OS; backend="threading" on Slurm.
|
||||
Parallel(n_jobs=args.num_workers)(
|
||||
delayed(__maybe_download_file)(data_url, dataset_root / Path(data_url).name)
|
||||
for _, data_url in data_source.items()
|
||||
)
|
||||
|
||||
# unzip datasets
|
||||
# Note: you need to verify which backend works well on your cluster.
|
||||
# backend="loky" is fine on multi-core Ubuntu OS; backend="threading" on Slurm.
|
||||
Parallel(n_jobs=args.num_workers)(
|
||||
delayed(__extract_file)(dataset_root / Path(data_url).name, dataset_root)
|
||||
for _, data_url in data_source.items()
|
||||
)
|
||||
|
||||
# generate json files for train/val/test splits
|
||||
stats_path_root = dataset_root / Path(stats_source).stem / "speacker"
|
||||
entries_train, entries_val, entries_test = [], [], []
|
||||
speaker_entries = []
|
||||
num_speakers = 0
|
||||
for child in stats_path_root.iterdir():
|
||||
if child.is_dir():
|
||||
speaker = child.name
|
||||
num_speakers += 1
|
||||
speaker_stats_root = stats_path_root / speaker
|
||||
speaker_data_path = dataset_root / speaker
|
||||
|
||||
logging.info(f"Processing Speaker: {speaker}")
|
||||
results = __process_data(
|
||||
speaker_data_path,
|
||||
speaker_stats_root,
|
||||
num_speakers,
|
||||
args.min_duration,
|
||||
args.max_duration,
|
||||
args.val_num_utts_per_speaker,
|
||||
args.test_num_utts_per_speaker,
|
||||
args.seed_for_ds_split,
|
||||
)
|
||||
|
||||
entries_train.extend(results["train"])
|
||||
entries_val.extend(results["val"])
|
||||
entries_test.extend(results["test"])
|
||||
|
||||
speaker_entry = {
|
||||
"speaker_name": speaker,
|
||||
"speaker_id": num_speakers,
|
||||
"hours": results["hours"],
|
||||
"num_utts": results["num_utts"],
|
||||
"is_skipped": results["is_skipped"],
|
||||
}
|
||||
speaker_entries.append(speaker_entry)
|
||||
|
||||
# shuffle in place across multiple speakers
|
||||
random.Random(args.seed_for_ds_split).shuffle(entries_train)
|
||||
random.Random(args.seed_for_ds_split).shuffle(entries_val)
|
||||
random.Random(args.seed_for_ds_split).shuffle(entries_test)
|
||||
|
||||
# save speaker stats.
|
||||
df = pd.DataFrame.from_records(speaker_entries)
|
||||
df.sort_values(by="hours", ascending=False, inplace=True)
|
||||
spk2id_file_path = manifests_root / "spk2id.csv"
|
||||
df.to_csv(spk2id_file_path, index=False)
|
||||
logging.info(f"Saving Speaker to ID mapping to {spk2id_file_path}.")
|
||||
|
||||
# save json splits.
|
||||
train_json = manifests_root / "train_manifest.json"
|
||||
val_json = manifests_root / "val_manifest.json"
|
||||
test_json = manifests_root / "test_manifest.json"
|
||||
__save_json(train_json, entries_train)
|
||||
__save_json(val_json, entries_val)
|
||||
__save_json(test_json, entries_test)
|
||||
|
||||
# normalize text if requested. New json file, train_manifest_text_normed.json, will be generated.
|
||||
if args.normalize_text:
|
||||
__text_normalization(train_json, args.num_workers)
|
||||
__text_normalization(val_json, args.num_workers)
|
||||
__text_normalization(test_json, args.num_workers)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,134 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. 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.
|
||||
#
|
||||
# USAGE: python get_data.py --data-root=<where to put data> --data-set=<datasets_to_download> --num-workers=<number of parallel workers>
|
||||
# where <datasets_to_download> can be: dev_clean, dev_other, test_clean,
|
||||
# test_other, train_clean_100, train_clean_360, train_other_500 or ALL
|
||||
# You can also put more than one data_set comma-separated:
|
||||
# --data-set=dev_clean,train_clean_100
|
||||
import argparse
|
||||
import fnmatch
|
||||
import functools
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import subprocess
|
||||
import tarfile
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.utils.tar_utils import safe_extract
|
||||
|
||||
parser = argparse.ArgumentParser(description='Download LibriTTS and create manifests')
|
||||
parser.add_argument("--data-root", required=True, type=Path)
|
||||
parser.add_argument("--data-sets", default="dev_clean", type=str)
|
||||
parser.add_argument("--num-workers", default=4, type=int)
|
||||
args = parser.parse_args()
|
||||
|
||||
URLS = {
|
||||
'TRAIN_CLEAN_100': "https://www.openslr.org/resources/60/train-clean-100.tar.gz",
|
||||
'TRAIN_CLEAN_360': "https://www.openslr.org/resources/60/train-clean-360.tar.gz",
|
||||
'TRAIN_OTHER_500': "https://www.openslr.org/resources/60/train-other-500.tar.gz",
|
||||
'DEV_CLEAN': "https://www.openslr.org/resources/60/dev-clean.tar.gz",
|
||||
'DEV_OTHER': "https://www.openslr.org/resources/60/dev-other.tar.gz",
|
||||
'TEST_CLEAN': "https://www.openslr.org/resources/60/test-clean.tar.gz",
|
||||
'TEST_OTHER': "https://www.openslr.org/resources/60/test-other.tar.gz",
|
||||
}
|
||||
|
||||
|
||||
def __maybe_download_file(source_url, destination_path):
|
||||
if not destination_path.exists():
|
||||
tmp_file_path = destination_path.with_suffix('.tmp')
|
||||
urllib.request.urlretrieve(source_url, filename=str(tmp_file_path))
|
||||
tmp_file_path.rename(destination_path)
|
||||
|
||||
|
||||
def __extract_file(filepath, data_dir):
|
||||
try:
|
||||
with tarfile.open(filepath) as tar:
|
||||
safe_extract(tar, str(data_dir))
|
||||
except Exception:
|
||||
print(f"Error while extracting {filepath}. Already extracted?")
|
||||
|
||||
|
||||
def __process_transcript(file_path: str):
|
||||
entries = []
|
||||
with open(file_path, encoding="utf-8") as fin:
|
||||
text = fin.readlines()[0].strip()
|
||||
|
||||
# TODO(oktai15): add normalized text via Normalizer/NormalizerWithAudio
|
||||
wav_file = file_path.replace(".normalized.txt", ".wav")
|
||||
speaker_id = file_path.split('/')[-3]
|
||||
assert os.path.exists(wav_file), f"{wav_file} not found!"
|
||||
duration = subprocess.check_output(["soxi", "-D", wav_file])
|
||||
entry = {
|
||||
'audio_filepath': os.path.abspath(wav_file),
|
||||
'duration': float(duration),
|
||||
'text': text,
|
||||
'speaker': int(speaker_id),
|
||||
}
|
||||
|
||||
entries.append(entry)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def __process_data(data_folder, manifest_file, num_workers):
|
||||
files = []
|
||||
entries = []
|
||||
|
||||
for root, dirnames, filenames in os.walk(data_folder):
|
||||
# we will use normalized text provided by the original dataset
|
||||
for filename in fnmatch.filter(filenames, '*.normalized.txt'):
|
||||
files.append(os.path.join(root, filename))
|
||||
|
||||
with multiprocessing.Pool(num_workers) as p:
|
||||
processing_func = functools.partial(__process_transcript)
|
||||
results = p.imap(processing_func, files)
|
||||
for result in tqdm(results, total=len(files)):
|
||||
entries.extend(result)
|
||||
|
||||
with open(manifest_file, 'w') as fout:
|
||||
for m in entries:
|
||||
fout.write(json.dumps(m) + '\n')
|
||||
|
||||
|
||||
def main():
|
||||
data_root = args.data_root
|
||||
data_sets = args.data_sets
|
||||
num_workers = args.num_workers
|
||||
|
||||
if data_sets == "ALL":
|
||||
data_sets = "dev_clean,dev_other,train_clean_100,train_clean_360,train_other_500,test_clean,test_other"
|
||||
if data_sets == "mini":
|
||||
data_sets = "dev_clean,train_clean_100"
|
||||
for data_set in data_sets.split(','):
|
||||
filepath = data_root / f"{data_set}.tar.gz"
|
||||
print(f"Downloading data for {data_set}...")
|
||||
__maybe_download_file(URLS[data_set.upper()], filepath)
|
||||
print("Extracting...")
|
||||
__extract_file(str(filepath), str(data_root))
|
||||
|
||||
print("Processing and building manifest.")
|
||||
__process_data(
|
||||
str(data_root / "LibriTTS" / data_set.replace("_", "-")),
|
||||
str(data_root / "LibriTTS" / f"{data_set}.json"),
|
||||
num_workers=num_workers,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,49 @@
|
||||
name: "ds_for_fastpitch_align"
|
||||
|
||||
manifest_filepath: "train_manifest.json"
|
||||
sup_data_path: "sup_data"
|
||||
sup_data_types: [ "align_prior_matrix", "pitch" ]
|
||||
phoneme_dict_path: "scripts/tts_dataset_files/cmudict-0.7b_nv22.10"
|
||||
heteronyms_path: "scripts/tts_dataset_files/heteronyms-052722"
|
||||
|
||||
dataset:
|
||||
_target_: nemo.collections.tts.data.dataset.TTSDataset
|
||||
manifest_filepath: ${manifest_filepath}
|
||||
sample_rate: 22050
|
||||
sup_data_path: ${sup_data_path}
|
||||
sup_data_types: ${sup_data_types}
|
||||
n_fft: 1024
|
||||
win_length: 1024
|
||||
hop_length: 256
|
||||
window: "hann"
|
||||
n_mels: 80
|
||||
lowfreq: 0
|
||||
highfreq: 8000
|
||||
max_duration: null
|
||||
min_duration: 0.1
|
||||
ignore_file: null
|
||||
trim: false
|
||||
pitch_fmin: 65.40639132514966
|
||||
pitch_fmax: 2093.004522404789
|
||||
|
||||
text_normalizer:
|
||||
_target_: nemo_text_processing.text_normalization.normalize.Normalizer
|
||||
lang: en
|
||||
input_case: cased
|
||||
|
||||
text_normalizer_call_kwargs:
|
||||
verbose: false
|
||||
punct_pre_process: true
|
||||
punct_post_process: true
|
||||
|
||||
text_tokenizer:
|
||||
_target_: nemo.collections.common.tokenizers.text_to_speech.tts_tokenizers.EnglishPhonemesTokenizer
|
||||
punct: true
|
||||
stresses: true
|
||||
chars: true
|
||||
apostrophe: true
|
||||
pad_with_space: true
|
||||
g2p:
|
||||
_target_: nemo.collections.tts.g2p.models.en_us_arpabet.EnglishG2p
|
||||
phoneme_dict: ${phoneme_dict_path}
|
||||
heteronyms: ${heteronyms_path}
|
||||
@@ -0,0 +1,49 @@
|
||||
name: "ds_for_mixer_tts"
|
||||
|
||||
manifest_filepath: "train_manifest.json"
|
||||
sup_data_path: "sup_data"
|
||||
sup_data_types: [ "align_prior_matrix", "pitch" ]
|
||||
phoneme_dict_path: "scripts/tts_dataset_files/cmudict-0.7b_nv22.10"
|
||||
heteronyms_path: "scripts/tts_dataset_files/heteronyms-052722"
|
||||
|
||||
dataset:
|
||||
_target_: nemo.collections.tts.data.dataset.TTSDataset
|
||||
manifest_filepath: ${manifest_filepath}
|
||||
sample_rate: 22050
|
||||
sup_data_path: ${sup_data_path}
|
||||
sup_data_types: ${sup_data_types}
|
||||
n_fft: 1024
|
||||
win_length: 1024
|
||||
hop_length: 256
|
||||
window: "hann"
|
||||
n_mels: 80
|
||||
lowfreq: 0
|
||||
highfreq: 8000
|
||||
max_duration: null
|
||||
min_duration: 0.1
|
||||
ignore_file: null
|
||||
trim: false
|
||||
pitch_fmin: 65.40639132514966
|
||||
pitch_fmax: 2093.004522404789
|
||||
|
||||
text_normalizer:
|
||||
_target_: nemo_text_processing.text_normalization.normalize.Normalizer
|
||||
lang: en
|
||||
input_case: cased
|
||||
|
||||
text_normalizer_call_kwargs:
|
||||
verbose: false
|
||||
punct_pre_process: true
|
||||
punct_post_process: true
|
||||
|
||||
text_tokenizer:
|
||||
_target_: nemo.collections.common.tokenizers.text_to_speech.tts_tokenizers.EnglishPhonemesTokenizer
|
||||
punct: true
|
||||
stresses: true
|
||||
chars: true
|
||||
apostrophe: true
|
||||
pad_with_space: true
|
||||
g2p:
|
||||
_target_: nemo.collections.tts.g2p.models.en_us_arpabet.EnglishG2p
|
||||
phoneme_dict: ${phoneme_dict_path}
|
||||
heteronyms: ${heteronyms_path}
|
||||
@@ -0,0 +1,134 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. 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
|
||||
import json
|
||||
import tarfile
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.utils.tar_utils import safe_extract
|
||||
|
||||
try:
|
||||
from nemo_text_processing.text_normalization.normalize import Normalizer
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
raise ModuleNotFoundError(
|
||||
"The package `nemo_text_processing` was not installed in this environment. Please refer to"
|
||||
" https://github.com/NVIDIA/NeMo-text-processing and install this package before using "
|
||||
"this script"
|
||||
)
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(description='Download LJSpeech and create manifests with predefined split')
|
||||
parser.add_argument("--data-root", required=True, type=Path)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
URL = "https://data.keithito.com/data/speech/LJSpeech-1.1.tar.bz2"
|
||||
FILELIST_BASE = 'https://raw.githubusercontent.com/NVIDIA/tacotron2/master/filelists'
|
||||
|
||||
|
||||
def _load_sox():
|
||||
try:
|
||||
import sox
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Optional dependency 'sox' is required by this script. Install it with: pip install sox"
|
||||
) from None
|
||||
|
||||
return sox
|
||||
|
||||
|
||||
def __maybe_download_file(source_url, destination_path):
|
||||
if not destination_path.exists():
|
||||
tmp_file_path = destination_path.with_suffix('.tmp')
|
||||
urllib.request.urlretrieve(source_url, filename=str(tmp_file_path))
|
||||
tmp_file_path.rename(destination_path)
|
||||
|
||||
|
||||
def __extract_file(filepath, data_dir):
|
||||
try:
|
||||
with tarfile.open(filepath) as tar:
|
||||
safe_extract(tar, str(data_dir))
|
||||
except Exception:
|
||||
print(f"Error while extracting {filepath}. Already extracted?")
|
||||
|
||||
|
||||
def __process_data(data_root):
|
||||
sox = _load_sox()
|
||||
text_normalizer = Normalizer(
|
||||
lang="en",
|
||||
input_case="cased",
|
||||
overwrite_cache=True,
|
||||
cache_dir=data_root / "cache_dir",
|
||||
)
|
||||
text_normalizer_call_kwargs = {"punct_pre_process": True, "punct_post_process": True}
|
||||
normalizer_call = lambda x: text_normalizer.normalize(x, **text_normalizer_call_kwargs)
|
||||
|
||||
# Create manifests (based on predefined NVIDIA's split)
|
||||
filelists = ['train', 'val', 'test']
|
||||
for split in tqdm(filelists):
|
||||
# Download file list if necessary
|
||||
filelist_path = data_root / f"ljs_audio_text_{split}_filelist.txt"
|
||||
|
||||
if not filelist_path.exists():
|
||||
urllib.request.urlretrieve(
|
||||
f"{FILELIST_BASE}/ljs_audio_text_{split}_filelist.txt",
|
||||
filename=str(filelist_path),
|
||||
)
|
||||
|
||||
manifest_target = data_root / f"{split}_manifest.json"
|
||||
with open(manifest_target, 'w') as f_out:
|
||||
with open(filelist_path, 'r') as filelist:
|
||||
print(f"\nCreating {manifest_target}...")
|
||||
for line in tqdm(filelist):
|
||||
basename = line[6:16]
|
||||
|
||||
text = line[21:].strip()
|
||||
norm_text = normalizer_call(text)
|
||||
|
||||
# Make sure corresponding wavfile exists
|
||||
wav_path = data_root / 'wavs' / f"{basename}.wav"
|
||||
assert wav_path.exists(), f"{wav_path} does not exist!"
|
||||
|
||||
entry = {
|
||||
'audio_filepath': str(wav_path),
|
||||
'duration': sox.file_info.duration(wav_path),
|
||||
'text': text,
|
||||
'normalized_text': norm_text,
|
||||
}
|
||||
|
||||
f_out.write(json.dumps(entry) + '\n')
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
|
||||
tarred_data_path = args.data_root / "LJSpeech-1.1.tar.bz2"
|
||||
|
||||
__maybe_download_file(URL, tarred_data_path)
|
||||
__extract_file(str(tarred_data_path), str(args.data_root))
|
||||
|
||||
data_root = args.data_root / "LJSpeech-1.1"
|
||||
|
||||
__process_data(data_root)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
Mr. mister
|
||||
Mrs. misses
|
||||
Dr. doctor
|
||||
Drs. doctors
|
||||
Co. company
|
||||
Lt. lieutenant
|
||||
Sgt. sergeant
|
||||
St. saint
|
||||
Jr. junior
|
||||
Maj. major
|
||||
Hon. honorable
|
||||
Gov. governor
|
||||
Capt. captain
|
||||
Esq. esquire
|
||||
Gen. general
|
||||
Ltd. limited
|
||||
Rev. reverend
|
||||
Col. colonel
|
||||
Mt. mount
|
||||
Ft. fort
|
||||
etc. et cetera
|
||||
|
@@ -0,0 +1,280 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. 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.
|
||||
|
||||
"""
|
||||
This script is used to preprocess audio before TTS model training.
|
||||
|
||||
It can be configured to do several processing steps such as silence trimming, volume normalization,
|
||||
and duration filtering.
|
||||
|
||||
These can be done separately through multiple executions of the script, or all at once to avoid saving
|
||||
too many copies of the same audio.
|
||||
|
||||
Most of these can also be done by the TTS data loader at training time, but doing them ahead of time
|
||||
lets us implement more complex processing, validate the correctness of the output, and save on compute time.
|
||||
|
||||
$ python <nemo_root_path>/scripts/dataset_processing/tts/preprocess_audio.py \
|
||||
--input_manifest="<data_root_path>/manifest.json" \
|
||||
--output_manifest="<data_root_path>/manifest_processed.json" \
|
||||
--input_audio_dir="<data_root_path>/audio" \
|
||||
--output_audio_dir="<data_root_path>/audio_processed" \
|
||||
--num_workers=1 \
|
||||
--trim_config_path="<nemo_root_path>/examples/tts/conf/trim/energy.yaml" \
|
||||
--output_sample_rate=22050 \
|
||||
--output_format=flac \
|
||||
--volume_level=0.95 \
|
||||
--min_duration=0.5 \
|
||||
--max_duration=20.0 \
|
||||
--filter_file="filtered.txt"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import librosa
|
||||
import soundfile as sf
|
||||
from joblib import Parallel, delayed
|
||||
from omegaconf import OmegaConf
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import read_manifest, write_manifest
|
||||
from nemo.collections.tts.parts.preprocessing.audio_trimming import AudioTrimmer
|
||||
from nemo.collections.tts.parts.utils.tts_dataset_utils import get_abs_rel_paths, normalize_volume
|
||||
from nemo.core.classes.common import safe_instantiate
|
||||
from nemo.utils import logging
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
description="Compute speaker level pitch statistics.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input_manifest",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to input training manifest.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input_audio_dir",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to base directory with audio files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output_manifest",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to output training manifest with processed audio.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output_audio_dir",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to output directory for audio files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite_audio",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Whether to reprocess and overwrite existing audio files in output_audio_dir.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite_manifest",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Whether to overwrite the output manifest file if it exists.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num_workers", default=1, type=int, help="Number of parallel threads to use. If -1 all CPUs are used."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--trim_config_path",
|
||||
required=False,
|
||||
type=Path,
|
||||
help="Path to config file for nemo.collections.tts.data.audio_trimming.AudioTrimmer",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max_entries", default=0, type=int, help="If provided, maximum number of entries in the manifest to process."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output_sample_rate", default=0, type=int, help="If provided, rate to resample the audio to."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output_format",
|
||||
default="wav",
|
||||
type=str,
|
||||
help="If provided, format output audio will be saved as. If not provided, will keep original format.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--volume_level", default=0.0, type=float, help="If provided, peak volume to normalize audio to."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--min_duration", default=0.0, type=float, help="If provided, filter out utterances shorter than min_duration."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max_duration", default=0.0, type=float, help="If provided, filter out utterances longer than max_duration."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--filter_file",
|
||||
required=False,
|
||||
type=Path,
|
||||
help="If provided, output filter_file will contain list of " "utterances filtered out.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def _process_entry(
|
||||
entry: dict,
|
||||
input_audio_dir: Path,
|
||||
output_audio_dir: Path,
|
||||
overwrite_audio: bool,
|
||||
audio_trimmer: AudioTrimmer,
|
||||
output_sample_rate: int,
|
||||
output_format: str,
|
||||
volume_level: float,
|
||||
) -> Tuple[dict, float, float]:
|
||||
audio_filepath = Path(entry["audio_filepath"])
|
||||
|
||||
audio_path, audio_path_rel = get_abs_rel_paths(input_path=audio_filepath, base_path=input_audio_dir)
|
||||
|
||||
if not output_format:
|
||||
output_format = audio_path.suffix
|
||||
|
||||
output_path = output_audio_dir / audio_path_rel
|
||||
output_path = output_path.with_suffix(output_format)
|
||||
output_path.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
if output_path.exists() and not overwrite_audio:
|
||||
original_duration = librosa.get_duration(path=audio_path)
|
||||
output_duration = librosa.get_duration(path=output_path)
|
||||
else:
|
||||
audio, sample_rate = librosa.load(audio_path, sr=None)
|
||||
original_duration = librosa.get_duration(y=audio, sr=sample_rate)
|
||||
if audio_trimmer is not None:
|
||||
audio, start_i, end_i = audio_trimmer.trim_audio(
|
||||
audio=audio, sample_rate=int(sample_rate), audio_id=str(audio_path)
|
||||
)
|
||||
|
||||
if output_sample_rate:
|
||||
audio = librosa.resample(y=audio, orig_sr=sample_rate, target_sr=output_sample_rate)
|
||||
sample_rate = output_sample_rate
|
||||
|
||||
if volume_level:
|
||||
audio = normalize_volume(audio, volume_level=volume_level)
|
||||
|
||||
if audio.size > 0:
|
||||
sf.write(file=output_path, data=audio, samplerate=sample_rate)
|
||||
output_duration = librosa.get_duration(y=audio, sr=sample_rate)
|
||||
else:
|
||||
output_duration = 0.0
|
||||
|
||||
entry["duration"] = round(output_duration, 2)
|
||||
|
||||
if os.path.isabs(audio_filepath):
|
||||
entry["audio_filepath"] = str(output_path)
|
||||
else:
|
||||
output_filepath = audio_path_rel.with_suffix(output_format)
|
||||
entry["audio_filepath"] = str(output_filepath)
|
||||
|
||||
return entry, original_duration, output_duration
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
|
||||
input_manifest_path = args.input_manifest
|
||||
output_manifest_path = args.output_manifest
|
||||
input_audio_dir = args.input_audio_dir
|
||||
output_audio_dir = args.output_audio_dir
|
||||
overwrite_audio = args.overwrite_audio
|
||||
overwrite_manifest = args.overwrite_manifest
|
||||
num_workers = args.num_workers
|
||||
max_entries = args.max_entries
|
||||
output_sample_rate = args.output_sample_rate
|
||||
output_format = args.output_format
|
||||
volume_level = args.volume_level
|
||||
min_duration = args.min_duration
|
||||
max_duration = args.max_duration
|
||||
filter_file = args.filter_file
|
||||
|
||||
if output_manifest_path.exists():
|
||||
if overwrite_manifest:
|
||||
print(f"Will overwrite existing manifest path: {output_manifest_path}")
|
||||
else:
|
||||
raise ValueError(f"Manifest path already exists: {output_manifest_path}")
|
||||
|
||||
if args.trim_config_path:
|
||||
audio_trimmer_config = OmegaConf.load(args.trim_config_path)
|
||||
audio_trimmer = safe_instantiate(audio_trimmer_config)
|
||||
else:
|
||||
audio_trimmer = None
|
||||
|
||||
if output_format:
|
||||
if output_format.upper() not in sf.available_formats():
|
||||
raise ValueError(f"Unsupported output audio format: {output_format}")
|
||||
output_format = f".{output_format}"
|
||||
|
||||
output_audio_dir.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
entries = read_manifest(input_manifest_path)
|
||||
if max_entries:
|
||||
entries = entries[:max_entries]
|
||||
|
||||
# 'threading' backend is required when parallelizing torch models.
|
||||
job_outputs = Parallel(n_jobs=num_workers, backend='threading')(
|
||||
delayed(_process_entry)(
|
||||
entry=entry,
|
||||
input_audio_dir=input_audio_dir,
|
||||
output_audio_dir=output_audio_dir,
|
||||
overwrite_audio=overwrite_audio,
|
||||
audio_trimmer=audio_trimmer,
|
||||
output_sample_rate=output_sample_rate,
|
||||
output_format=output_format,
|
||||
volume_level=volume_level,
|
||||
)
|
||||
for entry in tqdm(entries)
|
||||
)
|
||||
|
||||
output_entries = []
|
||||
filtered_entries = []
|
||||
original_durations = 0.0
|
||||
output_durations = 0.0
|
||||
for output_entry, original_duration, output_duration in job_outputs:
|
||||
original_durations += original_duration
|
||||
|
||||
if (
|
||||
output_duration == 0.0
|
||||
or (min_duration and output_duration < min_duration)
|
||||
or (max_duration and output_duration > max_duration)
|
||||
):
|
||||
if output_duration != original_duration:
|
||||
output_entry["original_duration"] = original_duration
|
||||
filtered_entries.append(output_entry)
|
||||
continue
|
||||
|
||||
output_durations += output_duration
|
||||
output_entries.append(output_entry)
|
||||
|
||||
write_manifest(output_path=output_manifest_path, target_manifest=output_entries, ensure_ascii=False)
|
||||
if filter_file:
|
||||
write_manifest(output_path=str(filter_file), target_manifest=filtered_entries, ensure_ascii=False)
|
||||
|
||||
logging.info(f"Duration of original audio: {original_durations / 3600:.2f} hours")
|
||||
logging.info(f"Duration of processed audio: {output_durations / 3600:.2f} hours")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,183 @@
|
||||
# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. 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.
|
||||
|
||||
"""
|
||||
This script is used to preprocess text before TTS model training. This is needed mainly for text normalization,
|
||||
which is slow to rerun during training.
|
||||
|
||||
The output manifest will be the same as the input manifest but with final text stored in the 'normalized_text' field.
|
||||
|
||||
$ python <nemo_root_path>/scripts/dataset_processing/tts/preprocess_text.py \
|
||||
--input_manifest="<data_root_path>/manifest.json" \
|
||||
--output_manifest="<data_root_path>/manifest_processed.json" \
|
||||
--normalizer_config_path="<nemo_root_path>/examples/tts/conf/text/normalizer_en.yaml" \
|
||||
--lower_case \
|
||||
--num_workers=4 \
|
||||
--joblib_batch_size=16
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from joblib import Parallel, delayed
|
||||
from omegaconf import OmegaConf
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.core.classes.common import safe_instantiate
|
||||
|
||||
try:
|
||||
from nemo_text_processing.text_normalization.normalize import Normalizer
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
raise ModuleNotFoundError(
|
||||
"The package `nemo_text_processing` was not installed in this environment. Please refer to"
|
||||
" https://github.com/NVIDIA/NeMo-text-processing and install this package before using "
|
||||
"this script"
|
||||
)
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import read_manifest, write_manifest
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
description="Process and normalize text data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input_manifest",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to input training manifest.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output_manifest",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to output training manifest with processed text.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Whether to overwrite the output manifest file if it exists.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--text_key",
|
||||
default="text",
|
||||
type=str,
|
||||
help="Input text field to normalize.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--normalized_text_key",
|
||||
default="normalized_text",
|
||||
type=str,
|
||||
help="Output field to save normalized text to.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lower_case",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Whether to convert the final text to lower case.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--normalizer_config_path",
|
||||
required=False,
|
||||
type=Path,
|
||||
help="Path to config file for nemo_text_processing.text_normalization.normalize.Normalizer.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num_workers", default=1, type=int, help="Number of parallel threads to use. If -1 all CPUs are used."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--joblib_batch_size", type=int, help="Batch size for joblib workers. Defaults to 'auto' if not provided."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max_entries", default=0, type=int, help="If provided, maximum number of entries in the manifest to process."
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def _process_entry(
|
||||
entry: dict,
|
||||
normalizer: Normalizer,
|
||||
text_key: str,
|
||||
normalized_text_key: str,
|
||||
lower_case: bool,
|
||||
lower_case_norm: bool,
|
||||
) -> dict:
|
||||
text = entry[text_key]
|
||||
|
||||
if normalizer is not None:
|
||||
if lower_case_norm:
|
||||
text = text.lower()
|
||||
text = normalizer.normalize(text, punct_pre_process=True, punct_post_process=True)
|
||||
|
||||
if lower_case:
|
||||
text = text.lower()
|
||||
|
||||
entry[normalized_text_key] = text
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
|
||||
input_manifest_path = args.input_manifest
|
||||
output_manifest_path = args.output_manifest
|
||||
text_key = args.text_key
|
||||
normalized_text_key = args.normalized_text_key
|
||||
lower_case = args.lower_case
|
||||
num_workers = args.num_workers
|
||||
batch_size = args.joblib_batch_size
|
||||
max_entries = args.max_entries
|
||||
overwrite = args.overwrite
|
||||
|
||||
if output_manifest_path.exists():
|
||||
if overwrite:
|
||||
print(f"Will overwrite existing manifest path: {output_manifest_path}")
|
||||
else:
|
||||
raise ValueError(f"Manifest path already exists: {output_manifest_path}")
|
||||
|
||||
if args.normalizer_config_path:
|
||||
normalizer_config = OmegaConf.load(args.normalizer_config_path)
|
||||
normalizer = safe_instantiate(normalizer_config)
|
||||
lower_case_norm = normalizer.input_case == "lower_cased"
|
||||
else:
|
||||
normalizer = None
|
||||
lower_case_norm = False
|
||||
|
||||
entries = read_manifest(input_manifest_path)
|
||||
if max_entries:
|
||||
entries = entries[:max_entries]
|
||||
|
||||
if not batch_size:
|
||||
batch_size = 'auto'
|
||||
|
||||
output_entries = Parallel(n_jobs=num_workers, batch_size=batch_size)(
|
||||
delayed(_process_entry)(
|
||||
entry=entry,
|
||||
normalizer=normalizer,
|
||||
text_key=text_key,
|
||||
normalized_text_key=normalized_text_key,
|
||||
lower_case=lower_case,
|
||||
lower_case_norm=lower_case_norm,
|
||||
)
|
||||
for entry in tqdm(entries)
|
||||
)
|
||||
|
||||
write_manifest(output_path=output_manifest_path, target_manifest=output_entries, ensure_ascii=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,252 @@
|
||||
# Copyright (c) 2023, NVIDIA CORPORATION. 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.
|
||||
|
||||
"""
|
||||
This script is a helper for resynthesizing TTS dataset using a pretrained text-to-spectrogram model.
|
||||
Goal of resynthesis (as opposed to text-to-speech) is to use the largest amount of ground-truth features from existing speech data.
|
||||
For example, for resynthesis we want to have the same pitch and durations instead of ones predicted by the model.
|
||||
The results are to be used for some other task: vocoder finetuning, spectrogram enhancer training, etc.
|
||||
|
||||
Let's say we have the following toy dataset:
|
||||
/dataset/manifest.json
|
||||
/dataset/1/foo.wav
|
||||
/dataset/2/bar.wav
|
||||
/dataset/sup_data/pitch/1_foo.pt
|
||||
/dataset/sup_data/pitch/2_bar.pt
|
||||
|
||||
manifest.json has two entries for "/dataset/1/foo.wav" and "/dataset/2/bar.wav"
|
||||
(sup_data folder contains pitch files precomputed during training a FastPitch model on this dataset.)
|
||||
(If you lost your sup_data - don't worry, we use TTSDataset class so they would be created on-the-fly)
|
||||
|
||||
Our script call is
|
||||
$ python scripts/dataset_processing/tts/resynthesize_dataset.py \
|
||||
--model-path ./models/fastpitch/multi_spk/FastPitch--val_loss\=1.4473-epoch\=209.ckpt \
|
||||
--input-json-manifest "/dataset/manifest.json" \
|
||||
--input-sup-data-path "/dataset/sup_data/" \
|
||||
--output-folder "/output/" \
|
||||
--device "cuda:0" \
|
||||
--batch-size 1 \
|
||||
--num-workers 1
|
||||
|
||||
Then we get output dataset with following directory structure:
|
||||
/output/manifest_mel.json
|
||||
/output/mels/foo.npy
|
||||
/output/mels/foo_gt.npy
|
||||
/output/mels/bar.npy
|
||||
/output/mels/bar_gt.npy
|
||||
|
||||
/output/manifest_mel.json has the same entries as /dataset/manifest.json but with new fields for spectrograms.
|
||||
"mel_filepath" is path to the resynthesized spectrogram .npy, "mel_gt_filepath" is path to ground-truth spectrogram .npy
|
||||
|
||||
The output structure is similar to generate_mels.py script for compatibility reasons.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, Iterator, List
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from omegaconf import DictConfig, OmegaConf
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import read_manifest, write_manifest
|
||||
from nemo.collections.tts.models import FastPitchModel
|
||||
from nemo.collections.tts.models.base import SpectrogramGenerator
|
||||
from nemo.collections.tts.parts.utils.helpers import process_batch, to_device_recursive
|
||||
|
||||
|
||||
def chunks(iterable: Iterable, size: int) -> Iterator[List]:
|
||||
# chunks([1, 2, 3, 4, 5], size=2) -> [[1, 2], [3, 4], [5]]
|
||||
# assumes iterable does not have any `None`s
|
||||
args = [iter(iterable)] * size
|
||||
for chunk in itertools.zip_longest(*args, fillvalue=None):
|
||||
chunk = list(item for item in chunk if item is not None)
|
||||
if chunk:
|
||||
yield chunk
|
||||
|
||||
|
||||
def load_model(path: Path, device: torch.device) -> SpectrogramGenerator:
|
||||
model = None
|
||||
if path.suffix == ".nemo":
|
||||
model = SpectrogramGenerator.restore_from(path, map_location=device)
|
||||
elif path.suffix == ".ckpt":
|
||||
model = SpectrogramGenerator.load_from_checkpoint(path, map_location=device)
|
||||
else:
|
||||
raise ValueError(f"Unknown checkpoint type {path.suffix} ({path})")
|
||||
|
||||
return model.eval().to(device)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TTSDatasetResynthesizer:
|
||||
"""
|
||||
Reuses internals of a SpectrogramGenerator to resynthesize dataset using ground truth features.
|
||||
Default setup is FastPitch with learned alignment.
|
||||
If your use case requires different setup, you can either contribute to this script or subclass this class.
|
||||
"""
|
||||
|
||||
model: SpectrogramGenerator
|
||||
device: torch.device
|
||||
|
||||
@torch.no_grad()
|
||||
def resynthesize_batch(self, batch: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Resynthesizes a single batch.
|
||||
Takes a dict with main data and sup data.
|
||||
Outputs a dict with model outputs.
|
||||
"""
|
||||
if not isinstance(self.model, FastPitchModel):
|
||||
raise NotImplementedError(
|
||||
"This script supports only FastPitch. Please implement resynthesizing routine for your desired model."
|
||||
)
|
||||
|
||||
batch = to_device_recursive(batch, self.device)
|
||||
|
||||
mels, mel_lens = self.model.preprocessor(input_signal=batch["audio"], length=batch["audio_lens"])
|
||||
|
||||
reference_audio = batch.get("reference_audio", None)
|
||||
reference_audio_len = batch.get("reference_audio_lens", None)
|
||||
reference_spec, reference_spec_len = None, None
|
||||
if reference_audio is not None:
|
||||
reference_spec, reference_spec_len = self.model.preprocessor(
|
||||
input_signal=reference_audio, length=reference_audio_len
|
||||
)
|
||||
|
||||
outputs_tuple = self.model.forward(
|
||||
text=batch["text"],
|
||||
durs=None,
|
||||
pitch=batch["pitch"],
|
||||
speaker=batch.get("speaker"),
|
||||
pace=1.0,
|
||||
spec=mels,
|
||||
attn_prior=batch.get("attn_prior"),
|
||||
mel_lens=mel_lens,
|
||||
input_lens=batch["text_lens"],
|
||||
reference_spec=reference_spec,
|
||||
reference_spec_lens=reference_spec_len,
|
||||
)
|
||||
names = self.model.fastpitch.output_types.keys()
|
||||
return {"spec": mels, "mel_lens": mel_lens, **dict(zip(names, outputs_tuple))}
|
||||
|
||||
def resynthesized_batches(self) -> Iterator[Dict[str, Any]]:
|
||||
"""
|
||||
Returns a generator of resynthesized batches.
|
||||
Each returned batch is a dict containing main data, sup data, and model output
|
||||
"""
|
||||
self.model.setup_training_data(self.model._cfg["train_ds"])
|
||||
|
||||
for batch_tuple in iter(self.model._train_dl):
|
||||
batch = process_batch(batch_tuple, sup_data_types_set=self.model._train_dl.dataset.sup_data_types)
|
||||
yield self.resynthesize_batch(batch)
|
||||
|
||||
|
||||
def prepare_paired_mel_spectrograms(
|
||||
model_path: Path,
|
||||
input_json_manifest: Path,
|
||||
input_sup_data_path: Path,
|
||||
output_folder: Path,
|
||||
device: torch.device,
|
||||
batch_size: int,
|
||||
num_workers: int,
|
||||
):
|
||||
model = load_model(model_path, device)
|
||||
|
||||
dataset_config_overrides = {
|
||||
"dataset": {
|
||||
"manifest_filepath": str(input_json_manifest.absolute()),
|
||||
"sup_data_path": str(input_sup_data_path.absolute()),
|
||||
},
|
||||
"dataloader_params": {"batch_size": batch_size, "num_workers": num_workers, "shuffle": False},
|
||||
}
|
||||
model._cfg.train_ds = OmegaConf.merge(model._cfg.train_ds, DictConfig(dataset_config_overrides))
|
||||
resynthesizer = TTSDatasetResynthesizer(model, device)
|
||||
|
||||
input_manifest = read_manifest(input_json_manifest)
|
||||
|
||||
output_manifest = []
|
||||
output_json_manifest = output_folder / f"{input_json_manifest.stem}_mel{input_json_manifest.suffix}"
|
||||
output_mels_folder = output_folder / "mels"
|
||||
output_mels_folder.mkdir(exist_ok=True, parents=True)
|
||||
for batch, batch_manifest in tqdm(
|
||||
zip(resynthesizer.resynthesized_batches(), chunks(input_manifest, size=batch_size)), desc="Batch #"
|
||||
):
|
||||
pred_mels = batch["spect"].cpu() # key from fastpitch.output_types
|
||||
true_mels = batch["spec"].cpu() # key from code above
|
||||
mel_lens = batch["mel_lens"].cpu().flatten() # key from code above
|
||||
|
||||
for i, (manifest_entry, length) in enumerate(zip(batch_manifest, mel_lens.tolist())):
|
||||
print(manifest_entry["audio_filepath"])
|
||||
filename = Path(manifest_entry["audio_filepath"]).stem
|
||||
|
||||
# note that lengths match
|
||||
pred_mel = pred_mels[i, :, :length].clone().numpy()
|
||||
true_mel = true_mels[i, :, :length].clone().numpy()
|
||||
|
||||
pred_mel_path = output_mels_folder / f"{filename}.npy"
|
||||
true_mel_path = output_mels_folder / f"{filename}_gt.npy"
|
||||
|
||||
np.save(pred_mel_path, pred_mel)
|
||||
np.save(true_mel_path, true_mel)
|
||||
|
||||
new_manifest_entry = {
|
||||
**manifest_entry,
|
||||
"mel_filepath": str(pred_mel_path),
|
||||
"mel_gt_filepath": str(true_mel_path),
|
||||
}
|
||||
output_manifest.append(new_manifest_entry)
|
||||
|
||||
write_manifest(output_json_manifest, output_manifest, ensure_ascii=False)
|
||||
|
||||
|
||||
def argument_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
description="Resynthesize TTS dataset using a pretrained text-to-spectrogram model",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model-path",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to a checkpoint (either .nemo or .ckpt)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-json-manifest",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to the input JSON manifest",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-sup-data-path",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="sup_data_path for the JSON manifest",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-folder",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to the output folder. Will contain updated manifest and mels/ folder with spectrograms in .npy files",
|
||||
)
|
||||
parser.add_argument("--device", required=True, type=torch.device, help="Device ('cpu', 'cuda:0', ...)")
|
||||
parser.add_argument("--batch-size", required=True, type=int, help="Batch size in the DataLoader")
|
||||
parser.add_argument("--num-workers", required=True, type=int, help="Num workers in the DataLoader")
|
||||
return parser
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arguments = argument_parser().parse_args()
|
||||
prepare_paired_mel_spectrograms(**vars(arguments))
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
name: "ds_for_fastpitch_align"
|
||||
|
||||
manifest_filepath: "train_manifest.json"
|
||||
sup_data_path: "sup_data"
|
||||
sup_data_types: [ "align_prior_matrix", "pitch" ]
|
||||
phoneme_dict_path: "scripts/tts_dataset_files/zh/24finals/pinyin_dict_nv_22.10.txt"
|
||||
|
||||
dataset:
|
||||
_target_: nemo.collections.tts.data.dataset.TTSDataset
|
||||
manifest_filepath: ${manifest_filepath}
|
||||
sample_rate: 22050
|
||||
sup_data_path: ${sup_data_path}
|
||||
sup_data_types: ${sup_data_types}
|
||||
n_fft: 1024
|
||||
win_length: 1024
|
||||
hop_length: 256
|
||||
window: "hann"
|
||||
n_mels: 80
|
||||
lowfreq: 0
|
||||
highfreq: null
|
||||
max_duration: null
|
||||
min_duration: 0.1
|
||||
ignore_file: null
|
||||
trim: true
|
||||
trim_top_db: 50
|
||||
trim_frame_length: 1024
|
||||
trim_hop_length: 256
|
||||
pitch_fmin: 65.40639132514966
|
||||
pitch_fmax: 2093.004522404789
|
||||
|
||||
text_normalizer:
|
||||
_target_: nemo_text_processing.text_normalization.normalize.Normalizer
|
||||
lang: zh
|
||||
input_case: cased
|
||||
|
||||
text_normalizer_call_kwargs:
|
||||
verbose: false
|
||||
punct_pre_process: true
|
||||
punct_post_process: true
|
||||
|
||||
text_tokenizer:
|
||||
_target_: nemo.collections.common.tokenizers.text_to_speech.tts_tokenizers.ChinesePhonemesTokenizer
|
||||
punct: true
|
||||
apostrophe: true
|
||||
pad_with_space: true
|
||||
g2p:
|
||||
_target_: nemo.collections.tts.g2p.models.zh_cn_pinyin.ChineseG2p
|
||||
phoneme_dict: ${phoneme_dict_path}
|
||||
word_segmenter: jieba # Only jieba is supported now.
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. 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
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from opencc import OpenCC
|
||||
|
||||
try:
|
||||
from nemo_text_processing.text_normalization.normalize import Normalizer
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
raise ModuleNotFoundError(
|
||||
"The package `nemo_text_processing` was not installed in this environment. Please refer to"
|
||||
" https://github.com/NVIDIA/NeMo-text-processing and install this package before using "
|
||||
"this script"
|
||||
)
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Prepare SF_bilingual dataset and create manifests with predefined split'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--data-root",
|
||||
type=Path,
|
||||
help="where the dataset will reside",
|
||||
default="./DataChinese/sf_bilingual_speech_zh_en_vv1/SF_bilingual/",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifests-path", type=Path, help="where the resulting manifests files will reside", default="./"
|
||||
)
|
||||
parser.add_argument("--val-size", default=0.01, type=float, help="eval set split")
|
||||
parser.add_argument("--test-size", default=0.01, type=float, help="test set split")
|
||||
parser.add_argument(
|
||||
"--seed-for-ds-split",
|
||||
default=100,
|
||||
type=float,
|
||||
help="Seed for deterministic split of train/dev/test, NVIDIA's default is 100",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def __process_transcript(file_path: str):
|
||||
# Create zh-TW to zh-simplify converter
|
||||
cc = OpenCC('t2s')
|
||||
# Create normalizer
|
||||
text_normalizer = Normalizer(
|
||||
lang="zh",
|
||||
input_case="cased",
|
||||
overwrite_cache=True,
|
||||
cache_dir=str(file_path / "cache_dir"),
|
||||
)
|
||||
text_normalizer_call_kwargs = {"punct_pre_process": True, "punct_post_process": True}
|
||||
normalizer_call = lambda x: text_normalizer.normalize(x, **text_normalizer_call_kwargs)
|
||||
entries = []
|
||||
i = 0
|
||||
with open(file_path / "text_SF.txt", encoding="utf-8") as fin:
|
||||
for line in fin:
|
||||
content = line.split()
|
||||
wav_name, text = content[0], "".join(content[1:])
|
||||
wav_name = wav_name.replace(u'\ufeff', '')
|
||||
# WAR: change DL to SF, e.g. real wave file com_SF_ce2727.wav, wav name in text_SF
|
||||
# com_DL_ce2727. It would be fixed through the dataset in the future.
|
||||
wav_name = wav_name.replace('DL', 'SF')
|
||||
wav_file = file_path / "wavs" / (wav_name + ".wav")
|
||||
assert os.path.exists(wav_file), f"{wav_file} not found!"
|
||||
duration = subprocess.check_output(["soxi", "-D", str(wav_file)])
|
||||
simplified_text = cc.convert(text)
|
||||
normalized_text = normalizer_call(simplified_text)
|
||||
entry = {
|
||||
'audio_filepath': os.path.abspath(wav_file),
|
||||
'duration': float(duration),
|
||||
'text': text,
|
||||
'normalized_text': normalized_text,
|
||||
}
|
||||
|
||||
i += 1
|
||||
entries.append(entry)
|
||||
return entries
|
||||
|
||||
|
||||
def __process_data(dataset_path, val_size, test_size, seed_for_ds_split, manifests_dir):
|
||||
entries = __process_transcript(dataset_path)
|
||||
|
||||
random.Random(seed_for_ds_split).shuffle(entries)
|
||||
|
||||
train_size = 1.0 - val_size - test_size
|
||||
train_entries, validate_entries, test_entries = np.split(
|
||||
entries, [int(len(entries) * train_size), int(len(entries) * (train_size + val_size))]
|
||||
)
|
||||
|
||||
assert len(train_entries) > 0, "Not enough data for train, val and test"
|
||||
|
||||
def save(p, data):
|
||||
with open(p, 'w') as f:
|
||||
for d in data:
|
||||
f.write(json.dumps(d) + '\n')
|
||||
|
||||
save(manifests_dir / "train_manifest.json", train_entries)
|
||||
save(manifests_dir / "val_manifest.json", validate_entries)
|
||||
save(manifests_dir / "test_manifest.json", test_entries)
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
dataset_root = args.data_root
|
||||
dataset_root.mkdir(parents=True, exist_ok=True)
|
||||
__process_data(
|
||||
dataset_root,
|
||||
args.val_size,
|
||||
args.test_size,
|
||||
args.seed_for_ds_split,
|
||||
args.manifests_path,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,44 @@
|
||||
name: "ds_for_fastpitch_align"
|
||||
|
||||
manifest_filepath: "train_manifest.json"
|
||||
sup_data_path: "sup_data"
|
||||
sup_data_types: [ "align_prior_matrix", "pitch" ]
|
||||
|
||||
dataset:
|
||||
_target_: nemo.collections.tts.data.dataset.TTSDataset
|
||||
manifest_filepath: ${manifest_filepath}
|
||||
sample_rate: 22050
|
||||
sup_data_path: ${sup_data_path}
|
||||
sup_data_types: ${sup_data_types}
|
||||
n_fft: 1024
|
||||
win_length: 1024
|
||||
hop_length: 256
|
||||
window: "hann"
|
||||
n_mels: 80
|
||||
lowfreq: 0
|
||||
highfreq: null
|
||||
max_duration: null
|
||||
min_duration: 0.1
|
||||
ignore_file: null
|
||||
trim: true
|
||||
trim_top_db: 50
|
||||
trim_frame_length: ${dataset.win_length}
|
||||
trim_hop_length: ${dataset.hop_length}
|
||||
pitch_fmin: 65.40639132514966
|
||||
pitch_fmax: 2093.004522404789
|
||||
|
||||
text_normalizer:
|
||||
_target_: nemo_text_processing.text_normalization.normalize.Normalizer
|
||||
lang: de
|
||||
input_case: cased
|
||||
|
||||
text_normalizer_call_kwargs:
|
||||
verbose: false
|
||||
punct_pre_process: true
|
||||
punct_post_process: true
|
||||
|
||||
text_tokenizer:
|
||||
_target_: nemo.collections.common.tokenizers.text_to_speech.tts_tokenizers.GermanCharsTokenizer
|
||||
punct: true
|
||||
apostrophe: true
|
||||
pad_with_space: true
|
||||
@@ -0,0 +1,274 @@
|
||||
# Copyright (c) 2023, NVIDIA CORPORATION. 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.
|
||||
|
||||
"""
|
||||
This script is used to generate JSON manifests for mel-generator model training. The usage is below.
|
||||
|
||||
$ python scripts/dataset_processing/tts/thorsten_neutral/get_data.py \
|
||||
--data-root ~/experiments/thorsten_neutral \
|
||||
--manifests-root ~/experiments/thorsten_neutral \
|
||||
--data-version "22_10" \
|
||||
--min-duration 0.1 \
|
||||
--normalize-text
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from joblib import Parallel, delayed
|
||||
from tqdm import tqdm
|
||||
|
||||
try:
|
||||
from nemo_text_processing.text_normalization.normalize import Normalizer
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
raise ModuleNotFoundError(
|
||||
"The package `nemo_text_processing` was not installed in this environment. Please refer to"
|
||||
" https://github.com/NVIDIA/NeMo-text-processing and install this package before using "
|
||||
"this script"
|
||||
)
|
||||
|
||||
from nemo.utils import logging
|
||||
|
||||
# Thorsten Müller published two neural voice datasets, 21.02 and 22.10.
|
||||
THORSTEN_NEUTRAL = {
|
||||
"21_02": {
|
||||
"url": "https://zenodo.org/record/5525342/files/thorsten-neutral_v03.tgz?download=1",
|
||||
"dir_name": "thorsten-de_v03",
|
||||
"metadata": ["metadata.csv"],
|
||||
},
|
||||
"22_10": {
|
||||
"url": "https://zenodo.org/record/7265581/files/ThorstenVoice-Dataset_2022.10.zip?download=1",
|
||||
"dir_name": "ThorstenVoice-Dataset_2022.10",
|
||||
"metadata": ["metadata_train.csv", "metadata_dev.csv", "metadata_test.csv"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
description="Download Thorsten Müller's neutral voice dataset and create manifests with predefined split. "
|
||||
"Thorsten Müller published two neural voice datasets, 21.02 and 22.10, where 22.10 provides better "
|
||||
"audio quality. Please choose one of the two for your TTS models. Details about the dataset are "
|
||||
"in https://github.com/thorstenMueller/Thorsten-Voice.",
|
||||
)
|
||||
parser.add_argument("--data-root", required=True, type=Path, help="where the resulting dataset will reside.")
|
||||
parser.add_argument("--manifests-root", required=True, type=Path, help="where the manifests files will reside.")
|
||||
parser.add_argument("--data-version", default="22_10", choices=["21_02", "22_10"], type=str)
|
||||
parser.add_argument("--min-duration", default=0.1, type=float)
|
||||
parser.add_argument("--max-duration", default=float('inf'), type=float)
|
||||
parser.add_argument("--val-size", default=100, type=int)
|
||||
parser.add_argument("--test-size", default=100, type=int)
|
||||
parser.add_argument(
|
||||
"--num-workers",
|
||||
default=-1,
|
||||
type=int,
|
||||
help="Specify the max number of concurrent Python worker processes. "
|
||||
"If -1 all CPUs are used. If 1 no parallel computing is used.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--normalize-text",
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="Normalize original text and add a new entry 'normalized_text' to .json file if True.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seed-for-ds-split",
|
||||
default=100,
|
||||
type=float,
|
||||
help="Seed for deterministic split of train/dev/test, NVIDIA's default is 100.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def __maybe_download_file(source_url, destination_path):
|
||||
if not destination_path.exists():
|
||||
logging.info(f"Downloading data: {source_url} --> {destination_path}")
|
||||
tmp_file_path = destination_path.with_suffix(".tmp")
|
||||
urllib.request.urlretrieve(source_url, filename=tmp_file_path)
|
||||
tmp_file_path.rename(destination_path)
|
||||
else:
|
||||
logging.info(f"Skipped downloading data because it exists: {destination_path}")
|
||||
|
||||
|
||||
def __extract_file(filepath, data_dir):
|
||||
logging.info(f"Unzipping data: {filepath} --> {data_dir}")
|
||||
shutil.unpack_archive(filepath, data_dir)
|
||||
logging.info(f"Unzipping data is complete: {filepath}.")
|
||||
|
||||
|
||||
def __save_json(json_file, dict_list):
|
||||
logging.info(f"Saving JSON split to {json_file}.")
|
||||
with open(json_file, "w") as f:
|
||||
for d in dict_list:
|
||||
f.write(json.dumps(d) + "\n")
|
||||
|
||||
|
||||
def __text_normalization(json_file, num_workers=-1):
|
||||
text_normalizer_call_kwargs = {
|
||||
"punct_pre_process": True,
|
||||
"punct_post_process": True,
|
||||
}
|
||||
text_normalizer = Normalizer(
|
||||
lang="de",
|
||||
input_case="cased",
|
||||
overwrite_cache=True,
|
||||
cache_dir=str(json_file.parent / "cache_dir"),
|
||||
)
|
||||
|
||||
def normalizer_call(x):
|
||||
return text_normalizer.normalize(x, **text_normalizer_call_kwargs)
|
||||
|
||||
def add_normalized_text(line_dict):
|
||||
normalized_text = normalizer_call(line_dict["text"])
|
||||
line_dict.update({"normalized_text": normalized_text})
|
||||
return line_dict
|
||||
|
||||
logging.info(f"Normalizing text for {json_file}.")
|
||||
with open(json_file, 'r', encoding='utf-8') as fjson:
|
||||
lines = fjson.readlines()
|
||||
# Note: you need to verify which backend works well on your cluster.
|
||||
# backend="loky" is fine on multi-core Ubuntu OS; backend="threading" on Slurm.
|
||||
dict_list = Parallel(n_jobs=num_workers)(
|
||||
delayed(add_normalized_text)(json.loads(line)) for line in tqdm(lines)
|
||||
)
|
||||
|
||||
json_file_text_normed = json_file.parent / f"{json_file.stem}_text_normed{json_file.suffix}"
|
||||
with open(json_file_text_normed, 'w', encoding="utf-8") as fjson_norm:
|
||||
for dct in dict_list:
|
||||
fjson_norm.write(json.dumps(dct) + "\n")
|
||||
logging.info(f"Normalizing text is complete: {json_file} --> {json_file_text_normed}")
|
||||
|
||||
|
||||
def __process_data(
|
||||
unzipped_dataset_path, metadata, min_duration, max_duration, val_size, test_size, seed_for_ds_split
|
||||
):
|
||||
logging.info("Preparing JSON train/val/test splits.")
|
||||
|
||||
entries = list()
|
||||
not_found_wavs = list()
|
||||
wrong_duration_wavs = list()
|
||||
|
||||
for metadata_fname in metadata:
|
||||
meta_file = unzipped_dataset_path / metadata_fname
|
||||
with open(meta_file, 'r') as fmeta:
|
||||
for line in tqdm(fmeta):
|
||||
items = line.strip().split('|')
|
||||
wav_file_stem, text = items[0], items[1]
|
||||
wav_file = unzipped_dataset_path / "wavs" / f"{wav_file_stem}.wav"
|
||||
|
||||
# skip audios if they do not exist.
|
||||
if not wav_file.exists():
|
||||
not_found_wavs.append(wav_file)
|
||||
logging.warning(f"Skipping {wav_file}: it is not found.")
|
||||
continue
|
||||
|
||||
# skip audios if their duration is out of range.
|
||||
duration = subprocess.check_output(["soxi", "-D", str(wav_file)])
|
||||
duration = float(duration)
|
||||
if min_duration <= duration <= max_duration:
|
||||
entry = {
|
||||
'audio_filepath': str(wav_file),
|
||||
'duration': duration,
|
||||
'text': text,
|
||||
}
|
||||
entries.append(entry)
|
||||
elif duration < min_duration:
|
||||
wrong_duration_wavs.append(wav_file)
|
||||
logging.warning(f"Skipping {wav_file}: it is too short, less than {min_duration} seconds.")
|
||||
continue
|
||||
else:
|
||||
wrong_duration_wavs.append(wav_file)
|
||||
logging.warning(f"Skipping {wav_file}: it is too long, greater than {max_duration} seconds.")
|
||||
continue
|
||||
|
||||
random.Random(seed_for_ds_split).shuffle(entries)
|
||||
train_size = len(entries) - val_size - test_size
|
||||
if train_size <= 0:
|
||||
raise ValueError("Not enough data for the train split.")
|
||||
|
||||
logging.info("Preparing JSON train/val/test splits is complete.")
|
||||
train, val, test = (
|
||||
entries[:train_size],
|
||||
entries[train_size : train_size + val_size],
|
||||
entries[train_size + val_size :],
|
||||
)
|
||||
|
||||
return train, val, test, not_found_wavs, wrong_duration_wavs
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
data_root = args.data_root
|
||||
manifests_root = args.manifests_root
|
||||
data_version = args.data_version
|
||||
|
||||
dataset_root = data_root / f"ThorstenVoice-Dataset-{data_version}"
|
||||
dataset_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# download and extract dataset
|
||||
dataset_url = THORSTEN_NEUTRAL[data_version]["url"]
|
||||
zipped_dataset_path = dataset_root / Path(dataset_url).name.split("?")[0]
|
||||
__maybe_download_file(dataset_url, zipped_dataset_path)
|
||||
__extract_file(zipped_dataset_path, dataset_root)
|
||||
|
||||
# generate train/dev/test splits
|
||||
unzipped_dataset_path = dataset_root / THORSTEN_NEUTRAL[data_version]["dir_name"]
|
||||
entries_train, entries_val, entries_test, not_found_wavs, wrong_duration_wavs = __process_data(
|
||||
unzipped_dataset_path=unzipped_dataset_path,
|
||||
metadata=THORSTEN_NEUTRAL[data_version]["metadata"],
|
||||
min_duration=args.min_duration,
|
||||
max_duration=args.max_duration,
|
||||
val_size=args.val_size,
|
||||
test_size=args.test_size,
|
||||
seed_for_ds_split=args.seed_for_ds_split,
|
||||
)
|
||||
|
||||
# save json splits.
|
||||
train_json = manifests_root / "train_manifest.json"
|
||||
val_json = manifests_root / "val_manifest.json"
|
||||
test_json = manifests_root / "test_manifest.json"
|
||||
__save_json(train_json, entries_train)
|
||||
__save_json(val_json, entries_val)
|
||||
__save_json(test_json, entries_test)
|
||||
|
||||
# save skipped audios that are not found into a file.
|
||||
if len(not_found_wavs) > 0:
|
||||
skipped_not_found_file = manifests_root / "skipped_not_found_wavs.list"
|
||||
with open(skipped_not_found_file, "w") as f_notfound:
|
||||
for line in not_found_wavs:
|
||||
f_notfound.write(f"{line}\n")
|
||||
|
||||
# save skipped audios that are too short or too long into a file.
|
||||
if len(wrong_duration_wavs) > 0:
|
||||
skipped_wrong_duration_file = manifests_root / "skipped_wrong_duration_wavs.list"
|
||||
with open(skipped_wrong_duration_file, "w") as f_wrong_dur:
|
||||
for line in wrong_duration_wavs:
|
||||
f_wrong_dur.write(f"{line}\n")
|
||||
|
||||
# normalize text if requested. New json file, train_manifest_text_normed.json, will be generated.
|
||||
if args.normalize_text:
|
||||
__text_normalization(train_json, args.num_workers)
|
||||
__text_normalization(val_json, args.num_workers)
|
||||
__text_normalization(test_json, args.num_workers)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user