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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:28:58 +08:00
commit ba4be087d5
2316 changed files with 2668701 additions and 0 deletions
@@ -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
View File
@@ -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
1 Mr. mister
2 Mrs. misses
3 Dr. doctor
4 Drs. doctors
5 Co. company
6 Lt. lieutenant
7 Sgt. sergeant
8 St. saint
9 Jr. junior
10 Maj. major
11 Hon. honorable
12 Gov. governor
13 Capt. captain
14 Esq. esquire
15 Gen. general
16 Ltd. limited
17 Rev. reverend
18 Col. colonel
19 Mt. mount
20 Ft. fort
21 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))
@@ -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
View File
@@ -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()