mirror of
https://github.com/storytold/storyteller-ml.git
synced 2026-10-16 00:13:07 +00:00
Introduce code from Rust monorepo. This is a mess
This commit is contained in:
+318
@@ -0,0 +1,318 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Execution :
|
||||
|
||||
python create_tarred_tokenized_text_lm_dataset.py \
|
||||
--text_path=<comma seperated text filepaths> \
|
||||
--data_root=<path to output directory> \
|
||||
--tokenizer_name="bert-base-cased" \
|
||||
--tokenizer_vocab_file=<path to vocab file for tokenizer> \
|
||||
--num_shards=64 \
|
||||
--chunk_size=8192 \
|
||||
--chunk_write_buffer=512 \
|
||||
--lower_case \
|
||||
--log
|
||||
"""
|
||||
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tarfile
|
||||
|
||||
import joblib
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.collections.nlp.modules.common.tokenizer_utils import get_tokenizer
|
||||
|
||||
parser = argparse.ArgumentParser(description='Tarred Tokenized dataset for text language modelling')
|
||||
|
||||
# Data path arguments
|
||||
parser.add_argument('--text_path', required=True, default=None, type=str, help='Text paths, seperated by commas')
|
||||
parser.add_argument('--data_root', required=True, default=None, type=str, help='Output directory')
|
||||
|
||||
# General arguments
|
||||
parser.add_argument(
|
||||
'--chunk_write_buffer',
|
||||
default=128,
|
||||
type=int,
|
||||
help='Number of chunks of `chunk_size` to buffer for parallel tokenization and serial write to disk',
|
||||
)
|
||||
parser.add_argument('--lower_case', action='store_true', help='Whether to lower case the corpus')
|
||||
parser.add_argument('--log', action='store_true', help='Whether to print logs to terminal')
|
||||
|
||||
# Tokenizer arguments
|
||||
parser.add_argument('--tokenizer_name', required=False, default=None, type=str, help='Tokenizer name for resolution')
|
||||
parser.add_argument(
|
||||
'--tokenizer_model', required=False, default=None, type=str, help='Path to tokenizer model for sentencepiece'
|
||||
)
|
||||
parser.add_argument('--tokenizer_vocab_file', required=False, type=str, default=None, help='Path to a vocab file')
|
||||
parser.add_argument(
|
||||
'--tokenizer_special_tokens', default=None, type=str, nargs='+', help='List of special tokens for the tokenizer'
|
||||
)
|
||||
|
||||
# Tarred dataset arguments
|
||||
parser.add_argument('--num_shards', default=1, type=int, help='Number of shards for the tarfile')
|
||||
parser.add_argument('--chunk_size', default=8192, type=int, help='Number of rows of data concatenated into a vector')
|
||||
|
||||
parser.set_defaults(log=False, lower_case=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def __build_dataset_from_text(texts: str, lower_case: bool, chunk_size: int):
|
||||
if ',' in texts:
|
||||
texts = texts.split(',')
|
||||
else:
|
||||
texts = [texts]
|
||||
|
||||
num_lines = 0
|
||||
text_dataset = []
|
||||
|
||||
for text in texts:
|
||||
with open(text, 'r') as in_reader:
|
||||
reader = tqdm(iter(lambda: in_reader.readline(), ''), desc="Read 0 lines", unit=' lines')
|
||||
|
||||
for i, line in enumerate(reader):
|
||||
# Clean text line
|
||||
line = line.replace("\n", "").strip()
|
||||
|
||||
if lower_case:
|
||||
line = line.lower()
|
||||
|
||||
if line:
|
||||
text_dataset.append(line)
|
||||
|
||||
num_lines += 1
|
||||
if num_lines % 100000 == 0:
|
||||
reader.set_description(f"Read {num_lines} lines")
|
||||
|
||||
if num_lines % chunk_size == 0:
|
||||
yield text_dataset, num_lines
|
||||
|
||||
# Empty cache
|
||||
text_dataset = []
|
||||
|
||||
logging.info(f"Finished extracting manifest : {text}")
|
||||
|
||||
logging.info("Finished extracting all manifests ! Number of sentences : {}".format(num_lines))
|
||||
|
||||
if len(text_dataset) != 0:
|
||||
yield text_dataset, num_lines
|
||||
|
||||
|
||||
def __tokenize_str(texts, tokenizer):
|
||||
tokenized_text = []
|
||||
for text in texts:
|
||||
tok_text = tokenizer.text_to_ids(text)
|
||||
tokenized_text.extend(tok_text)
|
||||
return tokenized_text
|
||||
|
||||
|
||||
def __tokenize_text(
|
||||
text_paths, tokenizer, tokenized_cachedir, lower_case: bool = False, chunk_size=8192, write_buffer: int = -1
|
||||
):
|
||||
if write_buffer < 1:
|
||||
write_buffer = max(os.cpu_count() - write_buffer, 1)
|
||||
|
||||
logging.info(f"Using write chunk buffer of size {write_buffer}")
|
||||
|
||||
if not os.path.exists(tokenized_cachedir):
|
||||
os.makedirs(tokenized_cachedir)
|
||||
|
||||
# global parameters
|
||||
global_chunk_idx = 0
|
||||
chunk_paths = []
|
||||
chunk_lens = []
|
||||
|
||||
# buffer parameters
|
||||
data_cache = []
|
||||
chunk_idx = 0
|
||||
|
||||
text_generator = iter(__build_dataset_from_text(text_paths, lower_case=lower_case, chunk_size=chunk_size))
|
||||
global_num_lines = 0
|
||||
last_batch = False
|
||||
|
||||
with joblib.Parallel(n_jobs=-2, verbose=10) as parallel:
|
||||
|
||||
while True:
|
||||
try:
|
||||
data, num_lines = next(text_generator)
|
||||
data_cache.append(data)
|
||||
|
||||
global_num_lines = num_lines
|
||||
except StopIteration:
|
||||
last_batch = True
|
||||
|
||||
# Update counters
|
||||
chunk_idx += 1
|
||||
|
||||
if (chunk_idx == write_buffer) or last_batch:
|
||||
# write the chunks into disk after parallel tokenization
|
||||
tokenized_data_list = parallel(
|
||||
joblib.delayed(__tokenize_str)(chunk, tokenizer) for chunk in data_cache
|
||||
)
|
||||
|
||||
# Sequential write cache
|
||||
for chunk in tokenized_data_list:
|
||||
fp = os.path.join(tokenized_cachedir, f"chunk_{global_chunk_idx}.npy")
|
||||
chunk = np.asarray(chunk, dtype=np.int64)
|
||||
np.save(fp, chunk, allow_pickle=False)
|
||||
|
||||
chunk_paths.append(fp)
|
||||
chunk_lens.append(len(chunk))
|
||||
|
||||
global_chunk_idx += 1
|
||||
|
||||
logging.info(f"Wrote a total of {global_chunk_idx} chunks to file...")
|
||||
|
||||
# reset buffers
|
||||
data_cache.clear()
|
||||
del data_cache
|
||||
|
||||
data_cache = []
|
||||
chunk_idx = 0
|
||||
|
||||
if last_batch:
|
||||
logging.info("Finished tokenizing last chunk")
|
||||
break
|
||||
|
||||
logging.info(
|
||||
f"Chunking {global_num_lines} rows into {global_num_lines // chunk_size} tasks (each chunk contains {chunk_size} elements)"
|
||||
)
|
||||
return chunk_paths, chunk_lens
|
||||
|
||||
|
||||
def __create_chunk(data_root, chunk_path, shard_id, compute_metrics=False):
|
||||
"""Creates a tarball containing the tokenized text chunks.
|
||||
"""
|
||||
tar = tarfile.open(os.path.join(data_root, f'text_{shard_id}.tar'), mode='a')
|
||||
|
||||
# We squash the filename since we do not preserve directory structure of tokenized text in the tarball.
|
||||
base, ext = os.path.splitext(chunk_path)
|
||||
base = base.replace(os.pathsep, '_')
|
||||
# Need the following replacement as long as WebDataset splits on first period
|
||||
base = base.replace('.', '_')
|
||||
squashed_filename = f'{base}{ext}'
|
||||
tar.add(chunk_path, arcname=squashed_filename)
|
||||
|
||||
tar.close()
|
||||
|
||||
if compute_metrics:
|
||||
data = np.load(chunk_path, allow_pickle=False)
|
||||
chunk_len = len(data)
|
||||
return (chunk_len,)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def __write_tarred_tokenized_text_dataset(data_root, num_shards, chunk_paths, chunk_lens):
|
||||
num_chunks = len(chunk_paths)
|
||||
|
||||
if chunk_lens is not None:
|
||||
num_text = sum(chunk_lens)
|
||||
shard_counts = {chunk_id: chunk_len for chunk_id, chunk_len in enumerate(chunk_lens)}
|
||||
compute_metrics = False
|
||||
|
||||
else:
|
||||
num_text = 0
|
||||
shard_counts = {}
|
||||
compute_metrics = True
|
||||
|
||||
for chunk_id, chunk_path in enumerate(tqdm(chunk_paths, desc='Writing chunk ', unit=' chunks')):
|
||||
shard_id = chunk_id % num_shards
|
||||
metrics = __create_chunk(data_root, chunk_path, shard_id, compute_metrics=compute_metrics)
|
||||
|
||||
if metrics is not None:
|
||||
num_text += metrics[0]
|
||||
shard_counts[chunk_id] = metrics[0]
|
||||
|
||||
# write metadata
|
||||
metadata_path = os.path.join(data_root, 'metadata.json')
|
||||
with open(metadata_path, 'w') as f:
|
||||
metadata = {'num_chunks': num_chunks, 'num_text': num_text, 'shard_count': shard_counts}
|
||||
json.dump(metadata, f, indent=4)
|
||||
logging.info("Metadata writen..")
|
||||
|
||||
|
||||
def main():
|
||||
text_path = args.text_path
|
||||
data_root = args.data_root
|
||||
|
||||
if args.log:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
tokenized_cachedir = os.path.join(data_root, '_tokenized_dataset_cachedir')
|
||||
|
||||
if os.path.exists(tokenized_cachedir):
|
||||
logging.warning(
|
||||
f'Tokenized cache directory {tokenized_cachedir} already potentially contains files.'
|
||||
f'In such a case, please be aware that the tarfiles will be **appended** instead of overridden!'
|
||||
)
|
||||
|
||||
if not os.path.exists(data_root):
|
||||
os.makedirs(data_root)
|
||||
|
||||
chunk_paths = None
|
||||
chunk_lens = None
|
||||
|
||||
if os.path.exists(tokenized_cachedir):
|
||||
paths = glob.glob(os.path.join(tokenized_cachedir, "*.npy"))
|
||||
if len(paths) > 0:
|
||||
logging.info("Cached tokenized numpy files found, skipping re-tokenization of dataset")
|
||||
|
||||
chunk_paths = paths
|
||||
chunk_lens = None
|
||||
|
||||
if chunk_paths is None:
|
||||
if args.tokenizer_name is None:
|
||||
raise ValueError("`tokenizer_name` name is required when tokenizing the dataset for the first time.")
|
||||
|
||||
if args.tokenizer_vocab_file is None:
|
||||
raise ValueError("`tokenizer_vocab_file` is required when constructing the tokenized dataset")
|
||||
|
||||
tokenizer = get_tokenizer(
|
||||
tokenizer_name=args.tokenizer_name,
|
||||
tokenizer_model=args.tokenizer_model,
|
||||
vocab_file=args.tokenizer_vocab_file,
|
||||
special_tokens=args.tokenizer_special_tokens,
|
||||
)
|
||||
|
||||
logging.info("Built tokenizer")
|
||||
|
||||
# tokenize text data into sub-words
|
||||
chunk_paths, chunk_lens = __tokenize_text(
|
||||
text_paths=text_path,
|
||||
tokenizer=tokenizer,
|
||||
tokenized_cachedir=tokenized_cachedir,
|
||||
lower_case=args.lower_case,
|
||||
chunk_size=args.chunk_size,
|
||||
write_buffer=args.chunk_write_buffer,
|
||||
)
|
||||
logging.info(f"Tokenized dataset into sub-words and serialized cache at {tokenized_cachedir}")
|
||||
|
||||
# Write tarred dataset
|
||||
__write_tarred_tokenized_text_dataset(
|
||||
data_root, num_shards=args.num_shards, chunk_paths=chunk_paths, chunk_lens=chunk_lens
|
||||
)
|
||||
|
||||
logging.info('Done preparing tokenized dataset!')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,288 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
This script would evaluate a neural language model (Transformer) trained with
|
||||
`examples/nlp/language_modeling/transformer_lm.py' as a rescorer for ASR systems.
|
||||
Given a trained TransformerLMModel `.nemo` file, this script can be used to re-score the beams obtained from a beam
|
||||
search decoder of an ASR model.
|
||||
|
||||
USAGE:
|
||||
1. Obtain `.tsv` file with beams and their corresponding scores. Scores can be from a regular beam search decoder or
|
||||
in fusion with an N-gram LM scores. For a given beam size `beam_size` and a number of examples
|
||||
for evaluation `num_eval_examples`, it should contain (`beam_size` x `num_eval_examples`) lines of
|
||||
form `beam_candidate_text \t score`. This file can be generated by `scripts/asr_language_modeling/ngram_lm/eval_beamsearch_ngram.py`.
|
||||
|
||||
2. Rescore the candidates:
|
||||
python eval_neural_rescorer.py
|
||||
--lm_model=[path to .nemo file of the LM]
|
||||
--beams_file=[path to beams .tsv file]
|
||||
--beam_size=[size of the beams]
|
||||
--eval_manifest=[path to eval manifest .json file]
|
||||
--batch_size=[batch size used for inference on the LM model]
|
||||
--alpha=[the value for the parameter rescorer_alpha]
|
||||
--beta=[the value for the parameter rescorer_beta]
|
||||
|
||||
You may find more info on how to use this script at:
|
||||
https://docs.nvidia.com/deeplearning/nemo/user-guide/docs/en/main/asr/asr_language_modeling.html
|
||||
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import editdistance
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
import tqdm
|
||||
|
||||
from nemo.collections.nlp.models.language_modeling import TransformerLMModel
|
||||
from nemo.utils import logging
|
||||
|
||||
|
||||
class BeamScoresDataset(torch.utils.data.Dataset):
|
||||
"""
|
||||
Dataset to read the score file containing the beams and their score
|
||||
|
||||
Args:
|
||||
data_path: path to the beams file
|
||||
tokenizer: tokenizer of the LM model
|
||||
manifest_path: manifest `.json` file which contains the ground truths transcripts
|
||||
beam_size: the number of beams per sample
|
||||
max_seq_length: the maximum length of sequences
|
||||
"""
|
||||
|
||||
def __init__(self, data_path, tokenizer, manifest_path, beam_size=128, max_seq_length=256):
|
||||
self.data = pd.read_csv(data_path, delimiter="\t", header=None)
|
||||
self.tokenizer = tokenizer
|
||||
self.ground_truths = []
|
||||
with open(manifest_path, 'r') as f_orig:
|
||||
for line in f_orig:
|
||||
item = json.loads(line)
|
||||
self.ground_truths.append(item['text'])
|
||||
self.beam_size = beam_size
|
||||
self.max_seq_length = max_seq_length
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
text = str(self.data[0][idx])
|
||||
tokens = [self.tokenizer.bos_id] + self.tokenizer.text_to_ids(text) + [self.tokenizer.eos_id]
|
||||
input_ids = [self.tokenizer.pad_id] * self.max_seq_length
|
||||
input_ids[: len(tokens)] = tokens
|
||||
input_ids = np.array(input_ids)
|
||||
input_mask = (input_ids != self.tokenizer.pad_id).astype(np.float32)
|
||||
acoustic_score = self.data[1][idx]
|
||||
dist = editdistance.eval(text.split(), self.ground_truths[idx // self.beam_size].split())
|
||||
ref_len = len(self.ground_truths[idx // self.beam_size].split())
|
||||
len_in_chars = len(str(self.data[0][idx]))
|
||||
return input_ids, input_mask, acoustic_score, dist, ref_len, len_in_chars, idx
|
||||
|
||||
|
||||
def linear_search_wer(
|
||||
dists, scores1, scores2, total_len, coef_range=[0, 10], coef_steps=10000, param_name='parameter'
|
||||
):
|
||||
"""
|
||||
performs linear search to find the best coefficient when two set of scores are getting linearly fused.
|
||||
|
||||
Args:
|
||||
dists: Tesnor of the distances between the ground truth and the candidates with shape of [number of samples, beam size]
|
||||
scores1: Tensor of the first set of scores with shape of [number of samples, beam size]
|
||||
scores2: Tensor of the second set of scores with shape of [number of samples, beam size]
|
||||
total_len: The total length of all samples
|
||||
coef_range: the search range for the coefficient
|
||||
coef_steps: the number of steps that the search range would get divided into
|
||||
param_name: the name of the parameter to be used in the figure
|
||||
|
||||
Output:
|
||||
(best coefficient found, best WER achived)
|
||||
"""
|
||||
scale = scores1.mean().abs().item() / scores2.mean().abs().item()
|
||||
left = coef_range[0] * scale
|
||||
right = coef_range[1] * scale
|
||||
coefs = np.linspace(left, right, coef_steps)
|
||||
|
||||
best_wer = 10000
|
||||
best_coef = left
|
||||
wers = []
|
||||
for coef in coefs:
|
||||
scores = scores1 + coef * scores2
|
||||
wer = compute_wer(dists, scores, total_len)
|
||||
wers.append(wer)
|
||||
if wer < best_wer:
|
||||
best_wer = wer
|
||||
best_coef = coef
|
||||
|
||||
plt.plot(coefs, wers)
|
||||
plt.title(f'WER% after rescoring with different values of {param_name}')
|
||||
plt.ylabel('WER%')
|
||||
plt.xlabel(param_name)
|
||||
plt.show()
|
||||
return best_coef, best_wer
|
||||
|
||||
|
||||
def compute_wer(dists, scores, total_len):
|
||||
"""
|
||||
Sorts the candidates based on the scores and calculates the WER with the new top candidates.
|
||||
|
||||
Args:
|
||||
dists: Tensor of the distances between the ground truth and the candidates with shape of [number of samples, beam size]
|
||||
scores: Tensor of the scores for candidates with shape of [number of samples, beam size]
|
||||
total_len: The total length of all samples
|
||||
|
||||
Output:
|
||||
WER with the new scores
|
||||
"""
|
||||
indices = scores.max(dim=1, keepdim=True)[1]
|
||||
wer = dists.gather(dim=1, index=indices).sum() / total_len
|
||||
wer = wer.item()
|
||||
return wer
|
||||
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("--lm_model_file", type=str, required=True, help="path to LM model .nemo file")
|
||||
parser.add_argument("--beams_file", type=str, required=True, help="path to beams .tsv file")
|
||||
parser.add_argument(
|
||||
"--eval_manifest", type=str, required=True, help="path to the evaluation `.json` manifest file"
|
||||
)
|
||||
parser.add_argument("--beam_size", type=int, required=True, help="number of beams per candidate")
|
||||
parser.add_argument("--batch_size", type=int, default=256, help="inference batch size")
|
||||
parser.add_argument("--alpha", type=float, default=None, help="parameter alpha of the fusion")
|
||||
parser.add_argument("--beta", type=float, default=None, help="parameter beta of the fusion")
|
||||
parser.add_argument(
|
||||
"--scores_output_file", default=None, type=str, help="The optional path to store the rescored beams"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--device", default="cuda", type=str, help="The device to load the model onto to calculate the scores"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use_amp", action="store_true", help="Whether to use AMP if available to calculate the scores"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
device = args.device
|
||||
if device.startswith("cuda") and not torch.cuda.is_available():
|
||||
logging.info(f"cuda is not available! switched to cpu.")
|
||||
device = "cpu"
|
||||
|
||||
if args.lm_model_file.endswith(".nemo"):
|
||||
logging.info("Attempting to initialize from .nemo file")
|
||||
model = TransformerLMModel.restore_from(
|
||||
restore_path=args.lm_model_file, map_location=torch.device(device)
|
||||
).eval()
|
||||
else:
|
||||
raise NotImplementedError(f"Only supports .nemo files, but got: {args.model}")
|
||||
|
||||
max_seq_length = model.encoder._embedding.position_embedding.pos_enc.shape[0]
|
||||
dataset = BeamScoresDataset(args.beams_file, model.tokenizer, args.eval_manifest, args.beam_size, max_seq_length)
|
||||
data_loader = torch.utils.data.DataLoader(dataset=dataset, batch_size=args.batch_size)
|
||||
|
||||
if args.use_amp:
|
||||
if torch.cuda.is_available() and hasattr(torch.cuda, 'amp') and hasattr(torch.cuda.amp, 'autocast'):
|
||||
logging.info("AMP is enabled!\n")
|
||||
autocast = torch.cuda.amp.autocast
|
||||
else:
|
||||
|
||||
@contextlib.contextmanager
|
||||
def autocast():
|
||||
yield
|
||||
|
||||
logging.info(f"Rescoring with beam_size: {args.beam_size}")
|
||||
logging.info("Calculating the scores...")
|
||||
with autocast():
|
||||
with torch.no_grad():
|
||||
am_scores, lm_scores, dists, ref_lens, lens_in_chars = [], [], [], [], []
|
||||
for batch in tqdm.tqdm(data_loader):
|
||||
input_ids, input_mask, acoustic_score, dist, ref_len, len_in_chars, idx = batch
|
||||
|
||||
max_len_in_batch = input_mask.sum(dim=0).argmin().item()
|
||||
input_ids, input_mask = input_ids[:, :max_len_in_batch], input_mask[:, :max_len_in_batch]
|
||||
if torch.cuda.is_available():
|
||||
input_ids, input_mask = input_ids.to(device), input_mask.to(device)
|
||||
dist, acoustic_score, len_in_chars = (
|
||||
dist.to(device),
|
||||
acoustic_score.to(device),
|
||||
len_in_chars.to(device),
|
||||
)
|
||||
|
||||
log_probs = model.forward(input_ids[:, :-1], input_mask[:, :-1])
|
||||
target_log_probs = log_probs.gather(2, input_ids[:, 1:].unsqueeze(2)).squeeze(2)
|
||||
neural_lm_score = torch.sum(target_log_probs * input_mask[:, 1:], dim=-1)
|
||||
|
||||
am_scores.append(acoustic_score)
|
||||
lm_scores.append(neural_lm_score)
|
||||
dists.append(dist)
|
||||
ref_lens.append(ref_len)
|
||||
lens_in_chars.append(len_in_chars)
|
||||
|
||||
am_scores = torch.cat(am_scores).view(-1, args.beam_size)
|
||||
lm_scores = torch.cat(lm_scores).view(-1, args.beam_size)
|
||||
dists = torch.cat(dists).view(-1, args.beam_size)
|
||||
ref_lens = torch.cat(ref_lens).view(-1, args.beam_size)
|
||||
lens_in_chars = torch.cat(lens_in_chars).view(-1, args.beam_size).to(am_scores.dtype)
|
||||
|
||||
total_len = ref_lens[:, 0].sum()
|
||||
model_wer = dists[:, 0].sum() / total_len
|
||||
ideal_wer = dists.min(dim=1)[0].sum() / total_len
|
||||
|
||||
if args.alpha is None:
|
||||
logging.info("Linear search for alpha...")
|
||||
coef1, _ = linear_search_wer(
|
||||
dists=dists, scores1=am_scores, scores2=lm_scores, total_len=total_len, param_name='alpha'
|
||||
)
|
||||
coef1 = np.round(coef1, 3)
|
||||
logging.info(f"alpha={coef1} achieved the best WER.")
|
||||
logging.info(f"------------------------------------------------")
|
||||
else:
|
||||
coef1 = args.alpha
|
||||
|
||||
scores = am_scores + coef1 * lm_scores
|
||||
|
||||
if args.beta is None:
|
||||
logging.info("Linear search for beta...")
|
||||
coef2, _ = linear_search_wer(
|
||||
dists=dists, scores1=scores, scores2=lens_in_chars, total_len=total_len, param_name='beta'
|
||||
)
|
||||
coef2 = np.round(coef2, 3)
|
||||
logging.info(f"beta={coef2} achieved the best WER.")
|
||||
logging.info(f"------------------------------------------------")
|
||||
else:
|
||||
coef2 = args.beta
|
||||
|
||||
new_scores = am_scores + coef1 * lm_scores + coef2 * lens_in_chars
|
||||
rescored_wer = compute_wer(dists, new_scores, total_len)
|
||||
|
||||
logging.info(f"Input beams WER: {np.round(model_wer.item() * 100, 2)}%")
|
||||
logging.info(f"------------------------------------------------")
|
||||
logging.info(f" +LM rescoring WER: {np.round(rescored_wer * 100, 2)}%")
|
||||
logging.info(f" with alpha={coef1}, beta={coef2}")
|
||||
logging.info(f"------------------------------------------------")
|
||||
logging.info(f"Best possible WER: {np.round(ideal_wer.item() * 100, 2)}%")
|
||||
logging.info(f"------------------------------------------------")
|
||||
|
||||
new_scores_flatten = new_scores.flatten()
|
||||
if args.scores_output_file is not None:
|
||||
logging.info(f'Saving the candidates with their new scores at `{args.scores_output_file}`...')
|
||||
with open(args.scores_output_file, "w") as fout:
|
||||
for sample_id in range(len(dataset)):
|
||||
fout.write(f"{dataset.data[0][sample_id]}\t{new_scores_flatten[sample_id]}\n")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,345 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# This script would evaluate an N-gram language model trained with KenLM library (https://github.com/kpu/kenlm) in
|
||||
# fusion with beam search decoders on top of a trained ASR model. NeMo's beam search decoders are capable of using the
|
||||
# KenLM's N-gram models to find the best candidates. This script supports both character level and BPE level
|
||||
# encodings and models which is detected automatically from the type of the model.
|
||||
# You may train the LM model with 'scripts/ngram_lm/train_kenlm.py'.
|
||||
#
|
||||
# USAGE: python eval_beamsearch_ngram.py --nemo_model_file <path to the .nemo file of the model> \
|
||||
# --input_manifest <path to the evaluation JSON manifest file \
|
||||
# --kenlm_model_file <path to the binary KenLM model> \
|
||||
# --beam_width <list of the beam widths> \
|
||||
# --beam_alpha <list of the beam alphas> \
|
||||
# --beam_width <list of the beam betas> \
|
||||
# --preds_output_folder <optional folder to store the predictions> \
|
||||
# --decoding_mode beam_search_ngram
|
||||
# ...
|
||||
#
|
||||
# You may find more info on how to use this script at:
|
||||
# https://docs.nvidia.com/deeplearning/nemo/user-guide/docs/en/main/asr/asr_language_modeling.html
|
||||
|
||||
|
||||
# Please check train_kenlm.py to find out why we need TOKEN_OFFSET for BPE-based models
|
||||
TOKEN_OFFSET = 100
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
|
||||
import editdistance
|
||||
import kenlm_utils
|
||||
import numpy as np
|
||||
import torch
|
||||
from sklearn.model_selection import ParameterGrid
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
import nemo
|
||||
import nemo.collections.asr as nemo_asr
|
||||
from nemo.utils import logging
|
||||
|
||||
|
||||
def beam_search_eval(
|
||||
all_probs,
|
||||
target_transcripts,
|
||||
vocab,
|
||||
ids_to_text_func=None,
|
||||
preds_output_file=None,
|
||||
lm_path=None,
|
||||
beam_alpha=1.0,
|
||||
beam_beta=0.0,
|
||||
beam_width=128,
|
||||
beam_batch_size=128,
|
||||
progress_bar=True,
|
||||
):
|
||||
# creating the beam search decoder
|
||||
beam_search_lm = nemo_asr.modules.BeamSearchDecoderWithLM(
|
||||
vocab=vocab,
|
||||
beam_width=beam_width,
|
||||
alpha=beam_alpha,
|
||||
beta=beam_beta,
|
||||
lm_path=lm_path,
|
||||
num_cpus=max(os.cpu_count(), 1),
|
||||
input_tensor=False,
|
||||
)
|
||||
|
||||
wer_dist_first = cer_dist_first = 0
|
||||
wer_dist_best = cer_dist_best = 0
|
||||
words_count = 0
|
||||
chars_count = 0
|
||||
sample_idx = 0
|
||||
if preds_output_file:
|
||||
out_file = open(preds_output_file, 'w')
|
||||
|
||||
if progress_bar:
|
||||
it = tqdm(
|
||||
range(int(np.ceil(len(all_probs) / beam_batch_size))),
|
||||
desc=f"Beam search decoding with width={beam_width}, alpha={beam_alpha}, beta={beam_beta}",
|
||||
ncols=120,
|
||||
)
|
||||
else:
|
||||
it = range(int(np.ceil(len(all_probs) / beam_batch_size)))
|
||||
for batch_idx in it:
|
||||
# disabling type checking
|
||||
with nemo.core.typecheck.disable_checks():
|
||||
probs_batch = all_probs[batch_idx * beam_batch_size : (batch_idx + 1) * beam_batch_size]
|
||||
beams_batch = beam_search_lm.forward(log_probs=probs_batch, log_probs_length=None,)
|
||||
|
||||
for beams_idx, beams in enumerate(beams_batch):
|
||||
target = target_transcripts[sample_idx + beams_idx]
|
||||
target_split_w = target.split()
|
||||
target_split_c = list(target)
|
||||
words_count += len(target_split_w)
|
||||
chars_count += len(target_split_c)
|
||||
wer_dist_min = cer_dist_min = 10000
|
||||
for candidate_idx, candidate in enumerate(beams):
|
||||
if ids_to_text_func is not None:
|
||||
# For BPE encodings, need to shift by TOKEN_OFFSET to retrieve the original sub-word ids
|
||||
pred_text = ids_to_text_func([ord(c) - TOKEN_OFFSET for c in candidate[1]])
|
||||
else:
|
||||
pred_text = candidate[1]
|
||||
pred_split_w = pred_text.split()
|
||||
wer_dist = editdistance.eval(target_split_w, pred_split_w)
|
||||
pred_split_c = list(pred_text)
|
||||
cer_dist = editdistance.eval(target_split_c, pred_split_c)
|
||||
|
||||
wer_dist_min = min(wer_dist_min, wer_dist)
|
||||
cer_dist_min = min(cer_dist_min, cer_dist)
|
||||
|
||||
if candidate_idx == 0:
|
||||
# first candidate
|
||||
wer_dist_first += wer_dist
|
||||
cer_dist_first += cer_dist
|
||||
|
||||
score = candidate[0]
|
||||
if preds_output_file:
|
||||
out_file.write('{}\t{}\n'.format(pred_text, score))
|
||||
wer_dist_best += wer_dist_min
|
||||
cer_dist_best += cer_dist_min
|
||||
sample_idx += len(probs_batch)
|
||||
|
||||
if preds_output_file:
|
||||
out_file.close()
|
||||
logging.info(f"Stored the predictions of beam search decoding at '{preds_output_file}'.")
|
||||
|
||||
if lm_path:
|
||||
logging.info(
|
||||
'WER/CER with beam search decoding and N-gram model = {:.2%}/{:.2%}'.format(
|
||||
wer_dist_first / words_count, cer_dist_first / chars_count
|
||||
)
|
||||
)
|
||||
else:
|
||||
logging.info(
|
||||
'WER/CER with beam search decoding = {:.2%}/{:.2%}'.format(
|
||||
wer_dist_first / words_count, cer_dist_first / chars_count
|
||||
)
|
||||
)
|
||||
logging.info(
|
||||
'Best WER/CER in candidates = {:.2%}/{:.2%}'.format(wer_dist_best / words_count, cer_dist_best / chars_count)
|
||||
)
|
||||
logging.info(f"=================================================================================")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Evaluate an ASR model with beam search decoding and n-gram KenLM language model.'
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nemo_model_file", required=True, type=str, help="The path of the '.nemo' file of the ASR model"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--kenlm_model_file", required=False, default=None, type=str, help="The path of the KenLM binary model file"
|
||||
)
|
||||
parser.add_argument("--input_manifest", required=True, type=str, help="The manifest file of the evaluation set")
|
||||
parser.add_argument(
|
||||
"--preds_output_folder", default=None, type=str, help="The optional folder where the predictions are stored"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--probs_cache_file", default=None, type=str, help="The cache file for storing the outputs of the model"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--acoustic_batch_size", default=16, type=int, help="The batch size to calculate log probabilities"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--device", default="cuda", type=str, help="The device to load the model onto to calculate log probabilities"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use_amp", action="store_true", help="Whether to use AMP if available to calculate log probabilities"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--decoding_mode",
|
||||
choices=["greedy", "beamsearch", "beamsearch_ngram"],
|
||||
default="beamsearch_ngram",
|
||||
type=str,
|
||||
help="The decoding scheme to be used for evaluation.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--beam_width",
|
||||
required=True,
|
||||
type=int,
|
||||
nargs="+",
|
||||
help="The width or list of the widths for the beam search decoding",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--beam_alpha",
|
||||
required=True,
|
||||
type=float,
|
||||
nargs="+",
|
||||
help="The alpha parameter or list of the alphas for the beam search decoding",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--beam_beta",
|
||||
required=True,
|
||||
type=float,
|
||||
nargs="+",
|
||||
help="The beta parameter or list of the betas for the beam search decoding",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--beam_batch_size", default=128, type=int, help="The batch size to be used for beam search decoding"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
asr_model = nemo_asr.models.EncDecCTCModelBPE.restore_from(
|
||||
args.nemo_model_file, map_location=torch.device(args.device)
|
||||
)
|
||||
|
||||
target_transcripts = []
|
||||
with open(args.input_manifest, 'r') as manifest_file:
|
||||
audio_file_paths = []
|
||||
for line in tqdm(manifest_file, desc=f"Reading Manifest {args.input_manifest} ...", ncols=120):
|
||||
data = json.loads(line)
|
||||
target_transcripts.append(data['text'])
|
||||
audio_file_paths.append(data['audio_filepath'])
|
||||
|
||||
if args.probs_cache_file and os.path.exists(args.probs_cache_file):
|
||||
logging.info(f"Found a pickle file of probabilities at '{args.probs_cache_file}'.")
|
||||
logging.info(f"Loading the cached pickle file of probabilities from '{args.probs_cache_file}' ...")
|
||||
with open(args.probs_cache_file, 'rb') as probs_file:
|
||||
all_probs = pickle.load(probs_file)
|
||||
|
||||
if len(all_probs) != len(audio_file_paths):
|
||||
raise ValueError(
|
||||
f"The number of samples in the probabilities file '{args.probs_cache_file}' does not "
|
||||
f"match the manifest file. You may need to delete the probabilities cached file."
|
||||
)
|
||||
else:
|
||||
if args.use_amp:
|
||||
if torch.cuda.is_available() and hasattr(torch.cuda, 'amp') and hasattr(torch.cuda.amp, 'autocast'):
|
||||
logging.info("AMP is enabled!\n")
|
||||
autocast = torch.cuda.amp.autocast
|
||||
else:
|
||||
|
||||
@contextlib.contextmanager
|
||||
def autocast():
|
||||
yield
|
||||
|
||||
with autocast():
|
||||
with torch.no_grad():
|
||||
all_logits = asr_model.transcribe(audio_file_paths, batch_size=args.acoustic_batch_size, logprobs=True)
|
||||
all_probs = [kenlm_utils.softmax(logits) for logits in all_logits]
|
||||
if args.probs_cache_file:
|
||||
logging.info(f"Writing pickle files of probabilities at '{args.probs_cache_file}'...")
|
||||
with open(args.probs_cache_file, 'wb') as f_dump:
|
||||
pickle.dump(all_probs, f_dump)
|
||||
|
||||
wer_dist_greedy = 0
|
||||
cer_dist_greedy = 0
|
||||
words_count = 0
|
||||
chars_count = 0
|
||||
for batch_idx, probs in enumerate(all_probs):
|
||||
preds = np.argmax(probs, axis=1)
|
||||
preds_tensor = torch.tensor(preds, device='cpu').unsqueeze(0)
|
||||
pred_text = asr_model._wer.ctc_decoder_predictions_tensor(preds_tensor)[0]
|
||||
|
||||
pred_split_w = pred_text.split()
|
||||
target_split_w = target_transcripts[batch_idx].split()
|
||||
pred_split_c = list(pred_text)
|
||||
target_split_c = list(target_transcripts[batch_idx])
|
||||
|
||||
wer_dist = editdistance.eval(target_split_w, pred_split_w)
|
||||
cer_dist = editdistance.eval(target_split_c, pred_split_c)
|
||||
|
||||
wer_dist_greedy += wer_dist
|
||||
cer_dist_greedy += cer_dist
|
||||
words_count += len(target_split_w)
|
||||
chars_count += len(target_split_c)
|
||||
|
||||
logging.info('Greedy WER/CER = {:.2%}/{:.2%}'.format(wer_dist_greedy / words_count, cer_dist_greedy / chars_count))
|
||||
|
||||
encoding_level = kenlm_utils.SUPPORTED_MODELS.get(type(asr_model).__name__, None)
|
||||
if not encoding_level:
|
||||
logging.warning(
|
||||
f"Model type '{type(asr_model).__name__}' may not be supported. Would try to train a char-level LM."
|
||||
)
|
||||
encoding_level = 'char'
|
||||
|
||||
vocab = asr_model.decoder.vocabulary
|
||||
ids_to_text_func = None
|
||||
if encoding_level == "subword":
|
||||
vocab = [chr(idx + TOKEN_OFFSET) for idx in range(len(vocab))]
|
||||
ids_to_text_func = asr_model.tokenizer.ids_to_text
|
||||
# delete the model to free the memory
|
||||
del asr_model
|
||||
|
||||
if args.decoding_mode == "beamsearch_ngram":
|
||||
if not os.path.exists(args.kenlm_model_file):
|
||||
raise FileNotFoundError(f"Could not find the KenLM model file '{args.kenlm_model_file}'.")
|
||||
lm_path = args.kenlm_model_file
|
||||
else:
|
||||
lm_path = None
|
||||
|
||||
# 'greedy' decoding_mode would skip the beam search decoding
|
||||
if args.decoding_mode in ["beamsearch_ngram", "beamsearch"]:
|
||||
|
||||
params = {'beam_width': args.beam_width, 'beam_alpha': args.beam_alpha, 'beam_beta': args.beam_beta}
|
||||
hp_grid = ParameterGrid(params)
|
||||
hp_grid = list(hp_grid)
|
||||
|
||||
logging.info(f"==============================Starting the beam search decoding===============================")
|
||||
logging.info(f"Grid search size: {len(hp_grid)}")
|
||||
logging.info(f"It may take some time...")
|
||||
logging.info(f"==============================================================================================")
|
||||
|
||||
if args.preds_output_folder and not os.path.exists(args.preds_output_folder):
|
||||
os.mkdir(args.preds_output_folder)
|
||||
for hp in hp_grid:
|
||||
if args.preds_output_folder:
|
||||
preds_output_file = os.path.join(
|
||||
args.preds_output_folder,
|
||||
f"preds_out_width{hp['beam_width']}_alpha{hp['beam_alpha']}_beta{hp['beam_beta']}.tsv",
|
||||
)
|
||||
else:
|
||||
preds_output_file = None
|
||||
|
||||
beam_search_eval(
|
||||
all_probs=all_probs,
|
||||
target_transcripts=target_transcripts,
|
||||
vocab=vocab,
|
||||
ids_to_text_func=ids_to_text_func,
|
||||
preds_output_file=preds_output_file,
|
||||
lm_path=lm_path,
|
||||
beam_width=hp["beam_width"],
|
||||
beam_alpha=hp["beam_alpha"],
|
||||
beam_beta=hp["beam_beta"],
|
||||
beam_batch_size=args.beam_batch_size,
|
||||
progress_bar=True,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,6 @@
|
||||
git clone https://github.com/NVIDIA/OpenSeq2Seq -b ctc-decoders
|
||||
mv OpenSeq2Seq/decoders .
|
||||
rm -rf OpenSeq2Seq
|
||||
cd decoders
|
||||
./setup.sh
|
||||
cd ..
|
||||
@@ -0,0 +1,112 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Utility methods to be used for training N-gram LM with KenLM in train_kenlm.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
from joblib import Parallel, delayed
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
# List of the supported models to be used with N-gram LM and beam search decoding
|
||||
SUPPORTED_MODELS = {
|
||||
'EncDecCTCModelBPE': 'subword',
|
||||
'EncDecCTCModel': 'char',
|
||||
}
|
||||
|
||||
|
||||
def softmax(x):
|
||||
e = np.exp(x - np.max(x))
|
||||
return e / e.sum(axis=-1).reshape([x.shape[0], 1])
|
||||
|
||||
|
||||
def read_train_file(path, lowercase: bool = False):
|
||||
lines_read = 0
|
||||
text_dataset = []
|
||||
|
||||
with open(path, 'r') as f:
|
||||
reader = tqdm(iter(lambda: f.readline(), ''), desc="Read 0 lines", unit=' lines')
|
||||
for i, line in enumerate(reader):
|
||||
if path.endswith('.json'):
|
||||
line = json.loads(line)['text']
|
||||
|
||||
line = line.replace("\n", "").strip()
|
||||
if lowercase:
|
||||
line = line.lower()
|
||||
|
||||
if line:
|
||||
text_dataset.append(line)
|
||||
|
||||
lines_read += 1
|
||||
if lines_read % 100000 == 0:
|
||||
reader.set_description(f"Read {lines_read} lines")
|
||||
|
||||
return text_dataset
|
||||
|
||||
|
||||
def tokenize_str(texts, tokenizer, offset):
|
||||
tokenized_text = []
|
||||
for text in texts:
|
||||
tok_text = tokenizer.text_to_ids(text)
|
||||
tok_text = [chr(token + offset) for token in tok_text]
|
||||
tokenized_text.append(tok_text)
|
||||
return tokenized_text
|
||||
|
||||
|
||||
def tokenize_text(data, tokenizer, path, chunk_size=8192, buffer_size=32, token_offset=100):
|
||||
dataset_len = len(data)
|
||||
print(
|
||||
f"Chunking {dataset_len} rows into {dataset_len / float(chunk_size):0.4f} tasks (each chunk contains {chunk_size} elements)"
|
||||
)
|
||||
|
||||
current_step = 0
|
||||
if os.path.exists(path):
|
||||
print(f"Deleting previous file : {path}")
|
||||
os.remove(path)
|
||||
|
||||
with Parallel(n_jobs=-2, verbose=10) as parallel:
|
||||
while True:
|
||||
start = current_step * chunk_size
|
||||
end = min((current_step + buffer_size) * chunk_size, dataset_len)
|
||||
|
||||
tokenized_data = parallel(
|
||||
delayed(tokenize_str)(data[start : start + chunk_size], tokenizer, token_offset)
|
||||
for start in range(start, end, chunk_size)
|
||||
)
|
||||
|
||||
# Write dataset
|
||||
write_dataset(tokenized_data, path)
|
||||
current_step += len(tokenized_data)
|
||||
print(f"Finished writing {len(tokenized_data)} chunks to {path}. Current chunk index = {current_step}")
|
||||
del tokenized_data
|
||||
if end >= dataset_len:
|
||||
break
|
||||
|
||||
|
||||
def write_dataset(chunks, path):
|
||||
basedir = os.path.dirname(path)
|
||||
|
||||
if not os.path.exists(basedir):
|
||||
os.makedirs(basedir, exist_ok=True)
|
||||
|
||||
with open(path, 'a+', encoding='utf-8') as f:
|
||||
for chunk_idx in tqdm(range(len(chunks)), desc='Chunk ', total=len(chunks), unit=' chunks'):
|
||||
for text in chunks[chunk_idx]:
|
||||
line = ' '.join(text)
|
||||
f.write(f"{line}\n")
|
||||
@@ -0,0 +1,154 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
|
||||
# This script would train an N-gram language model with KenLM library (https://github.com/kpu/kenlm) which can be used
|
||||
# with the beam search decoders on top of the ASR models. This script supports both character level and BPE level
|
||||
# encodings and models which is detected automatically from the type of the model.
|
||||
# After the N-gram model is trained, and stored in the binary format, you may use
|
||||
# 'scripts/ngram_lm/eval_beamsearch_ngram.py' to evaluate it on an ASR model.
|
||||
#
|
||||
# You need to install the KenLM library and also the beam search decoders to use this feature. Please refer
|
||||
# to 'scripts/ngram_lm/install_beamsearch_decoders.sh' on how to install them.
|
||||
#
|
||||
# USAGE: python train_kenlm.py --nemo_model_file <path to the .nemo file of the model> \
|
||||
# --train_file <path to the training text or JSON manifest file \
|
||||
# --kenlm_bin_path <path to the bin folder of KenLM library> \
|
||||
# --kenlm_model_file <path to store the binary KenLM model> \
|
||||
# --ngram_length <order of N-gram model>
|
||||
#
|
||||
# After training is done, the binary LM model is stored at the path specified by '--kenlm_model_file'.
|
||||
# You may find more info on how to use this script at:
|
||||
# https://docs.nvidia.com/deeplearning/nemo/user-guide/docs/en/main/asr/asr_language_modeling.html
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import kenlm_utils
|
||||
import torch
|
||||
|
||||
import nemo.collections.asr as nemo_asr
|
||||
from nemo.utils import logging
|
||||
|
||||
"""
|
||||
NeMo's beam search decoders only support char-level encodings. In order to make it work with BPE-level encodings, we
|
||||
use a trick to encode the sub-word tokens of the training data as unicode characters and train a char-level KenLM.
|
||||
TOKEN_OFFSET is the offset in the unicode table to be used to encode the BPE sub-words. This encoding scheme reduces
|
||||
the required memory significantly, and the LM and its binary blob format require less storage space.
|
||||
"""
|
||||
TOKEN_OFFSET = 100
|
||||
|
||||
CHUNK_SIZE = 8192
|
||||
CHUNK_BUFFER_SIZE = 512
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Train an N-gram language model with KenLM to be used with beam search decoder of ASR models.'
|
||||
)
|
||||
parser.add_argument(
|
||||
"--train_file",
|
||||
required=True,
|
||||
type=str,
|
||||
help="Path to the training file, it can be a text file or JSON manifest",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nemo_model_file", required=True, type=str, help="The path of the '.nemo' file of the ASR model"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--kenlm_model_file", required=True, type=str, help="The path to store the KenLM binary model file"
|
||||
)
|
||||
parser.add_argument("--ngram_length", required=True, type=int, help="The order of N-gram LM")
|
||||
parser.add_argument("--kenlm_bin_path", required=True, type=str, help="The path to the bin folder of KenLM")
|
||||
parser.add_argument(
|
||||
"--do_lowercase", action='store_true', help="Whether to apply lower case conversion on the training text"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
""" TOKENIZER SETUP """
|
||||
logging.info(f"Loading nemo model '{args.nemo_model_file}' ...")
|
||||
model = nemo_asr.models.ASRModel.restore_from(args.nemo_model_file, map_location=torch.device('cpu'))
|
||||
|
||||
encoding_level = kenlm_utils.SUPPORTED_MODELS.get(type(model).__name__, None)
|
||||
if not encoding_level:
|
||||
logging.warning(
|
||||
f"Model type '{type(model).__name__}' may not be supported. Would try to train a char-level LM."
|
||||
)
|
||||
encoding_level = 'char'
|
||||
|
||||
""" DATASET SETUP """
|
||||
logging.info(f"Encoding the train file '{args.train_file}' ...")
|
||||
dataset = kenlm_utils.read_train_file(args.train_file, lowercase=args.do_lowercase)
|
||||
encoded_train_file = f"{args.kenlm_model_file}.tmp.txt"
|
||||
if encoding_level == "subword":
|
||||
kenlm_utils.tokenize_text(
|
||||
dataset,
|
||||
model.tokenizer,
|
||||
path=encoded_train_file,
|
||||
chunk_size=CHUNK_SIZE,
|
||||
buffer_size=CHUNK_BUFFER_SIZE,
|
||||
token_offset=TOKEN_OFFSET,
|
||||
)
|
||||
# --discount_fallback is needed for training KenLM for BPE-based models
|
||||
discount_arg = "--discount_fallback"
|
||||
else:
|
||||
with open(encoded_train_file, 'w', encoding='utf-8') as f:
|
||||
for line in dataset:
|
||||
f.write(f"{line}\n")
|
||||
|
||||
discount_arg = ""
|
||||
|
||||
del model
|
||||
|
||||
arpa_file = f"{args.kenlm_model_file}.tmp.arpa"
|
||||
""" LMPLZ ARGUMENT SETUP """
|
||||
kenlm_args = [
|
||||
os.path.join(args.kenlm_bin_path, 'lmplz'),
|
||||
"-o",
|
||||
f"{args.ngram_length}",
|
||||
"--text",
|
||||
encoded_train_file,
|
||||
"--arpa",
|
||||
arpa_file,
|
||||
discount_arg,
|
||||
]
|
||||
|
||||
ret = subprocess.run(kenlm_args, capture_output=False, text=True, stdout=sys.stdout, stderr=sys.stderr)
|
||||
if ret.returncode != 0:
|
||||
raise RuntimeError("Training KenLM was not successful!")
|
||||
""" BINARY BUILD """
|
||||
logging.info(f"Running binary_build command \n\n{' '.join(kenlm_args)}\n\n")
|
||||
kenlm_args = [
|
||||
os.path.join(args.kenlm_bin_path, "build_binary"),
|
||||
"trie",
|
||||
arpa_file,
|
||||
args.kenlm_model_file,
|
||||
]
|
||||
ret = subprocess.run(kenlm_args, capture_output=False, text=True, stdout=sys.stdout, stderr=sys.stderr)
|
||||
|
||||
if ret.returncode != 0:
|
||||
raise RuntimeError("Training KenLM was not successful!")
|
||||
|
||||
os.remove(encoded_train_file)
|
||||
logging.info(f"Deleted the temporary encoded training file '{encoded_train_file}'.")
|
||||
os.remove(arpa_file)
|
||||
logging.info(f"Deleted the arpa file '{arpa_file}'.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,171 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Copyright 2017 Johns Hopkins University (Shinji Watanabe)
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
# Changes to script
|
||||
Change the script to import the NeMo model class you would like to load a checkpoint for,
|
||||
then update the model constructor to use this model class. This can be found by the line:
|
||||
|
||||
<<< Change model class here ! >>>
|
||||
|
||||
By default, this script imports and creates the `EncDecCTCModelBPE` class but it can be
|
||||
changed to any NeMo Model.
|
||||
|
||||
# Run the script
|
||||
|
||||
## Saving a .nemo model file (loaded with ModelPT.restore_from(...))
|
||||
HYDRA_FULL_ERROR=1 python average_model_checkpoints.py \
|
||||
--config-path="<path to config directory>" \
|
||||
--config-name="<config name>" \
|
||||
name=<name of the averaged checkpoint> \
|
||||
+checkpoint_dir=<OPTIONAL: directory of checkpoint> \
|
||||
+checkpoint_paths=\"[/path/to/ptl_1.ckpt,/path/to/ptl_2.ckpt,/path/to/ptl_3.ckpt,...]\"
|
||||
|
||||
|
||||
## Saving an averaged pytorch checkpoint (loaded with torch.load(...))
|
||||
HYDRA_FULL_ERROR=1 python average_model_checkpoints.py \
|
||||
--config-path="<path to config directory>" \
|
||||
--config-name="<config name>" \
|
||||
name=<name of the averaged checkpoint> \
|
||||
+checkpoint_dir=<OPTIONAL: directory of checkpoint> \
|
||||
+checkpoint_paths=\"[/path/to/ptl_1.ckpt,/path/to/ptl_2.ckpt,/path/to/ptl_3.ckpt,...]\" \
|
||||
+save_ckpt_only=true
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytorch_lightning as pl
|
||||
import torch
|
||||
from omegaconf import OmegaConf, open_dict
|
||||
|
||||
# Change this import to the model you would like to average
|
||||
from nemo.collections.asr.models import EncDecCTCModelBPE
|
||||
from nemo.core.config import hydra_runner
|
||||
from nemo.utils import logging
|
||||
|
||||
|
||||
def process_config(cfg: OmegaConf):
|
||||
if 'name' not in cfg or cfg.name is None:
|
||||
raise ValueError("`cfg.name` must be provided to save a model checkpoint")
|
||||
|
||||
if 'checkpoint_paths' not in cfg or cfg.checkpoint_paths is None:
|
||||
raise ValueError(
|
||||
"`cfg.checkpoint_paths` must be provided as a list of one or more str paths to "
|
||||
"pytorch lightning checkpoints"
|
||||
)
|
||||
|
||||
save_ckpt_only = False
|
||||
|
||||
with open_dict(cfg):
|
||||
name_prefix = cfg.name
|
||||
checkpoint_paths = cfg.pop('checkpoint_paths')
|
||||
|
||||
if 'checkpoint_dir' in cfg:
|
||||
checkpoint_dir = cfg.pop('checkpoint_dir')
|
||||
else:
|
||||
checkpoint_dir = None
|
||||
|
||||
if 'save_ckpt_only' in cfg:
|
||||
save_ckpt_only = cfg.pop('save_ckpt_only')
|
||||
|
||||
if type(checkpoint_paths) not in (list, tuple):
|
||||
checkpoint_paths = str(checkpoint_paths).replace("[", "").replace("]", "")
|
||||
checkpoint_paths = checkpoint_paths.split(",")
|
||||
checkpoint_paths = [ckpt_path.strip() for ckpt_path in checkpoint_paths]
|
||||
|
||||
if checkpoint_dir is not None:
|
||||
checkpoint_paths = [os.path.join(checkpoint_dir, path) for path in checkpoint_paths]
|
||||
|
||||
return name_prefix, checkpoint_paths, save_ckpt_only
|
||||
|
||||
|
||||
@hydra_runner(config_path=None, config_name=None)
|
||||
def main(cfg):
|
||||
name_prefix, checkpoint_paths, save_ckpt_only = process_config(cfg)
|
||||
|
||||
if not save_ckpt_only:
|
||||
trainer = pl.Trainer(**cfg.trainer)
|
||||
|
||||
# <<< Change model class here ! >>>
|
||||
# Model architecture which will contain the averaged checkpoints
|
||||
# Change the model constructor to the one you would like (if needed)
|
||||
model = EncDecCTCModelBPE(cfg=cfg.model, trainer=trainer)
|
||||
|
||||
""" < Checkpoint Averaging Logic > """
|
||||
# load state dicts
|
||||
n = len(checkpoint_paths)
|
||||
avg_state = None
|
||||
|
||||
logging.info(f"Averaging {n} checkpoints ...")
|
||||
|
||||
for ix, path in enumerate(checkpoint_paths):
|
||||
checkpoint = torch.load(path, map_location='cpu')
|
||||
|
||||
if 'state_dict' in checkpoint:
|
||||
checkpoint = checkpoint['state_dict']
|
||||
|
||||
if ix == 0:
|
||||
# Initial state
|
||||
avg_state = checkpoint
|
||||
|
||||
logging.info(f"Initialized average state dict with checkpoint : {path}")
|
||||
else:
|
||||
# Accumulated state
|
||||
for k in avg_state:
|
||||
avg_state[k] = avg_state[k] + checkpoint[k]
|
||||
|
||||
logging.info(f"Updated average state dict with state from checkpoint : {path}")
|
||||
|
||||
for k in avg_state:
|
||||
if str(avg_state[k].dtype).startswith("torch.int"):
|
||||
# For int type, not averaged, but only accumulated.
|
||||
# e.g. BatchNorm.num_batches_tracked
|
||||
pass
|
||||
else:
|
||||
avg_state[k] = avg_state[k] / n
|
||||
|
||||
# Save model
|
||||
if save_ckpt_only:
|
||||
ckpt_name = name_prefix + '-averaged.ckpt'
|
||||
torch.save(avg_state, ckpt_name)
|
||||
|
||||
logging.info(f"Averaged pytorch checkpoint saved as : {ckpt_name}")
|
||||
else:
|
||||
# Set model state
|
||||
logging.info("Loading averaged state dict in provided model")
|
||||
model.load_state_dict(avg_state, strict=True)
|
||||
|
||||
ckpt_name = name_prefix + '-averaged.nemo'
|
||||
model.save_to(ckpt_name)
|
||||
|
||||
logging.info(f"Averaged model saved as : {ckpt_name}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,178 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# Copyright (c) 2020, SeanNaren. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# To convert mp3 files to wav using sox, you must have installed sox with mp3 support
|
||||
# For example sudo apt-get install libsox-fmt-mp3
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import sox
|
||||
from sox import Transformer
|
||||
from tqdm import tqdm
|
||||
|
||||
parser = argparse.ArgumentParser(description='Downloads and processes Mozilla Common Voice dataset.')
|
||||
parser.add_argument("--data_root", default='CommonVoice_dataset/', type=str, help="Directory to store the dataset.")
|
||||
parser.add_argument('--manifest_dir', default='./', type=str, help='Output directory for manifests')
|
||||
parser.add_argument("--num_workers", default=multiprocessing.cpu_count(), type=int, help="Workers to process dataset.")
|
||||
parser.add_argument('--sample_rate', default=16000, type=int, help='Sample rate')
|
||||
parser.add_argument('--n_channels', default=1, type=int, help='Number of channels for output wav files')
|
||||
parser.add_argument(
|
||||
'--files_to_process',
|
||||
nargs='+',
|
||||
default=['test.tsv', 'dev.tsv', 'train.tsv'],
|
||||
type=str,
|
||||
help='list of *.csv file names to process',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
default='cv-corpus-5.1-2020-06-22',
|
||||
type=str,
|
||||
help='Version of the dataset (obtainable via https://commonvoice.mozilla.org/en/datasets',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--language',
|
||||
default='en',
|
||||
type=str,
|
||||
help='Which language to download.(default english,'
|
||||
'check https://commonvoice.mozilla.org/en/datasets for more language codes',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
COMMON_VOICE_URL = (
|
||||
f"https://voice-prod-bundler-ee1969a6ce8178826482b88e843c335139bd3fb4.s3.amazonaws.com/"
|
||||
"{}/{}.tar.gz".format(args.version, args.language)
|
||||
)
|
||||
|
||||
|
||||
def create_manifest(data: List[tuple], output_name: str, manifest_path: str):
|
||||
output_file = Path(manifest_path) / output_name
|
||||
output_file.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
with output_file.open(mode='w') as f:
|
||||
for wav_path, duration, text in tqdm(data, total=len(data)):
|
||||
f.write(
|
||||
json.dumps({'audio_filepath': os.path.abspath(wav_path), "duration": duration, 'text': text}) + '\n'
|
||||
)
|
||||
|
||||
|
||||
def process_files(csv_file, data_root, num_workers):
|
||||
""" Read *.csv file description, convert mp3 to wav, process text.
|
||||
Save results to data_root.
|
||||
|
||||
Args:
|
||||
csv_file: str, path to *.csv file with data description, usually start from 'cv-'
|
||||
data_root: str, path to dir to save results; wav/ dir will be created
|
||||
"""
|
||||
wav_dir = os.path.join(data_root, 'wav/')
|
||||
os.makedirs(wav_dir, exist_ok=True)
|
||||
audio_clips_path = os.path.dirname(csv_file) + '/clips/'
|
||||
|
||||
def process(x):
|
||||
file_path, text = x
|
||||
file_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||
text = text.lower().strip()
|
||||
audio_path = os.path.join(audio_clips_path, file_path)
|
||||
output_wav_path = os.path.join(wav_dir, file_name + '.wav')
|
||||
|
||||
tfm = Transformer()
|
||||
tfm.rate(samplerate=args.sample_rate)
|
||||
tfm.channels(n_channels=args.n_channels)
|
||||
tfm.build(input_filepath=audio_path, output_filepath=output_wav_path)
|
||||
duration = sox.file_info.duration(output_wav_path)
|
||||
return output_wav_path, duration, text
|
||||
|
||||
logging.info('Converting mp3 to wav for {}.'.format(csv_file))
|
||||
with open(csv_file) as csvfile:
|
||||
reader = csv.DictReader(csvfile, delimiter='\t')
|
||||
next(reader, None) # skip the headers
|
||||
data = [(row['path'], row['sentence']) for row in reader]
|
||||
with ThreadPool(num_workers) as pool:
|
||||
data = list(tqdm(pool.imap(process, data), total=len(data)))
|
||||
return data
|
||||
|
||||
|
||||
def main():
|
||||
data_root = args.data_root
|
||||
os.makedirs(data_root, exist_ok=True)
|
||||
|
||||
target_unpacked_dir = os.path.join(data_root, "CV_unpacked")
|
||||
|
||||
if os.path.exists(target_unpacked_dir):
|
||||
logging.info('Find existing folder {}'.format(target_unpacked_dir))
|
||||
else:
|
||||
logging.info("Could not find Common Voice, Downloading corpus...")
|
||||
|
||||
commands = [
|
||||
'wget',
|
||||
'--user-agent',
|
||||
'"Mozilla/5.0 (Windows NT 10.0; WOW64) '
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"',
|
||||
'-P',
|
||||
data_root,
|
||||
f'{COMMON_VOICE_URL}',
|
||||
]
|
||||
commands = " ".join(commands)
|
||||
subprocess.run(commands, shell=True, stderr=sys.stderr, stdout=sys.stdout, capture_output=False)
|
||||
filename = f"{args.language}.tar.gz"
|
||||
target_file = os.path.join(data_root, os.path.basename(filename))
|
||||
|
||||
os.makedirs(target_unpacked_dir, exist_ok=True)
|
||||
logging.info("Unpacking corpus to {} ...".format(target_unpacked_dir))
|
||||
tar = tarfile.open(target_file)
|
||||
tar.extractall(target_unpacked_dir)
|
||||
tar.close()
|
||||
|
||||
folder_path = os.path.join(target_unpacked_dir, args.version + f'/{args.language}/')
|
||||
|
||||
for csv_file in args.files_to_process:
|
||||
data = process_files(
|
||||
csv_file=os.path.join(folder_path, csv_file),
|
||||
data_root=os.path.join(data_root, os.path.splitext(csv_file)[0]),
|
||||
num_workers=args.num_workers,
|
||||
)
|
||||
logging.info('Creating manifests...')
|
||||
create_manifest(
|
||||
data=data,
|
||||
output_name=f'commonvoice_{os.path.splitext(csv_file)[0]}_manifest.json',
|
||||
manifest_path=args.manifest_dir,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,185 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# USAGE: python get_aishell_data.py --data_root=<where to put data>
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging as _logging
|
||||
import os
|
||||
import tarfile
|
||||
import urllib.request
|
||||
from glob import glob
|
||||
|
||||
import librosa as l
|
||||
from sklearn.model_selection import StratifiedShuffleSplit
|
||||
from tqdm import tqdm
|
||||
|
||||
parser = argparse.ArgumentParser(description='HI-MIA Data download')
|
||||
parser.add_argument("--data_root", required=True, default=None, type=str)
|
||||
parser.add_argument("--log_level", default=20, type=int)
|
||||
args = parser.parse_args()
|
||||
logging = _logging.getLogger(__name__)
|
||||
logging.addHandler(_logging.StreamHandler())
|
||||
logging.setLevel(args.log_level)
|
||||
|
||||
URL = {
|
||||
'dev': "http://www.openslr.org/resources/85/dev.tar.gz",
|
||||
'test': "http://www.openslr.org/resources/85/test.tar.gz",
|
||||
'train': "http://www.openslr.org/resources/85/train.tar.gz",
|
||||
}
|
||||
|
||||
|
||||
def __maybe_download_file(destination: str, source: str):
|
||||
"""
|
||||
Downloads source to destination if it doesn't exist.
|
||||
If exists, skips download
|
||||
Args:
|
||||
destination: local filepath
|
||||
source: url of resource
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
source = URL[source]
|
||||
if not os.path.exists(destination) and not os.path.exists(os.path.splitext(destination)[0]):
|
||||
logging.info("{0} does not exist. Downloading ...".format(destination))
|
||||
urllib.request.urlretrieve(source, filename=destination + '.tmp')
|
||||
os.rename(destination + '.tmp', destination)
|
||||
logging.info("Downloaded {0}.".format(destination))
|
||||
elif os.path.exists(destination):
|
||||
logging.info("Destination {0} exists. Skipping.".format(destination))
|
||||
elif os.path.exists(os.path.splitext(destination)[0]):
|
||||
logging.warning(
|
||||
"Assuming extracted folder %s contains the extracted files from %s. Will not download.",
|
||||
os.path.basename(destination),
|
||||
destination,
|
||||
)
|
||||
return destination
|
||||
|
||||
|
||||
def __extract_all_files(filepath: str, data_root: str, data_dir: str):
|
||||
if not os.path.exists(data_dir):
|
||||
extract_file(filepath, data_root)
|
||||
audio_dir = os.path.join(data_dir, 'wav')
|
||||
for subfolder, _, filelist in os.walk(audio_dir):
|
||||
for ftar in filelist:
|
||||
extract_file(os.path.join(subfolder, ftar), subfolder)
|
||||
else:
|
||||
logging.info('Skipping extracting. Data already there %s' % data_dir)
|
||||
|
||||
|
||||
def extract_file(filepath: str, data_dir: str):
|
||||
try:
|
||||
tar = tarfile.open(filepath)
|
||||
tar.extractall(data_dir)
|
||||
tar.close()
|
||||
except Exception:
|
||||
logging.info('Not extracting. Maybe already there?')
|
||||
|
||||
|
||||
def __remove_tarred_files(filepath: str, data_dir: str):
|
||||
if os.path.exists(data_dir) and os.path.isfile(filepath):
|
||||
logging.info('Deleting %s' % filepath)
|
||||
os.remove(filepath)
|
||||
|
||||
|
||||
def write_file(name, lines, idx):
|
||||
with open(name, 'w') as fout:
|
||||
for i in idx:
|
||||
dic = lines[i]
|
||||
json.dump(dic, fout)
|
||||
fout.write('\n')
|
||||
logging.info("wrote %s", name)
|
||||
|
||||
|
||||
def __process_data(data_folder: str, data_set: str):
|
||||
"""
|
||||
To generate manifest
|
||||
Args:
|
||||
data_folder: source with wav files
|
||||
Returns:
|
||||
|
||||
"""
|
||||
fullpath = os.path.abspath(data_folder)
|
||||
scp = glob(fullpath + '/**/*.wav', recursive=True)
|
||||
out = os.path.join(fullpath, data_set + '_all.json')
|
||||
utt2spk = os.path.join(fullpath, 'utt2spk')
|
||||
utt2spk_file = open(utt2spk, 'w')
|
||||
id = -2 # speaker id
|
||||
|
||||
if os.path.exists(out):
|
||||
logging.warning(
|
||||
"%s already exists and is assumed to be processed. If not, please delete %s and rerun this script",
|
||||
out,
|
||||
out,
|
||||
)
|
||||
return
|
||||
|
||||
speakers = []
|
||||
lines = []
|
||||
with open(out, 'w') as outfile:
|
||||
for line in tqdm(scp):
|
||||
line = line.strip()
|
||||
y, sr = l.load(line, sr=None)
|
||||
if sr != 16000:
|
||||
y, sr = l.load(line, sr=16000)
|
||||
l.output.write_wav(line, y, sr)
|
||||
dur = l.get_duration(y=y, sr=sr)
|
||||
if data_set == 'test':
|
||||
speaker = line.split('/')[-1].split('.')[0].split('_')[0]
|
||||
else:
|
||||
speaker = line.split('/')[id]
|
||||
speaker = list(speaker)
|
||||
speaker = ''.join(speaker)
|
||||
speakers.append(speaker)
|
||||
meta = {"audio_filepath": line, "duration": float(dur), "label": speaker}
|
||||
lines.append(meta)
|
||||
json.dump(meta, outfile)
|
||||
outfile.write("\n")
|
||||
utt2spk_file.write(line.split('/')[-1] + "\t" + speaker + "\n")
|
||||
|
||||
utt2spk_file.close()
|
||||
|
||||
if data_set != 'test':
|
||||
sss = StratifiedShuffleSplit(n_splits=1, test_size=0.1, random_state=42)
|
||||
for train_idx, test_idx in sss.split(speakers, speakers):
|
||||
print(len(train_idx))
|
||||
|
||||
out = os.path.join(fullpath, 'train.json')
|
||||
write_file(out, lines, train_idx)
|
||||
out = os.path.join(fullpath, 'dev.json')
|
||||
write_file(out, lines, test_idx)
|
||||
|
||||
|
||||
def main():
|
||||
data_root = args.data_root
|
||||
for data_set in URL.keys():
|
||||
|
||||
# data_set = 'data_aishell'
|
||||
logging.info("\n\nWorking on: {0}".format(data_set))
|
||||
file_path = os.path.join(data_root, data_set + ".tgz")
|
||||
logging.info("Getting {0}".format(data_set))
|
||||
__maybe_download_file(file_path, data_set)
|
||||
logging.info("Extracting {0}".format(data_set))
|
||||
data_folder = os.path.join(data_root, data_set)
|
||||
__extract_all_files(file_path, data_root, data_folder)
|
||||
__remove_tarred_files(file_path, data_folder)
|
||||
logging.info("Processing {0}".format(data_set))
|
||||
__process_data(data_folder, data_set)
|
||||
logging.info('Done!')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,171 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# USAGE: python get_librispeech_data.py --data_root=<where to put data>
|
||||
# --data_set=<datasets_to_download> --num_workers=<number of parallel workers>
|
||||
# where <datasets_to_download> can be: dev_clean, dev_other, test_clean,
|
||||
# test_other, train_clean_100, train_clean_360, train_other_500 or ALL
|
||||
# You can also put more than one data_set comma-separated:
|
||||
# --data_set=dev_clean,train_clean_100
|
||||
import argparse
|
||||
import fnmatch
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import subprocess
|
||||
import tarfile
|
||||
import urllib.request
|
||||
|
||||
from sox import Transformer
|
||||
from tqdm import tqdm
|
||||
|
||||
parser = argparse.ArgumentParser(description='LibriSpeech Data download')
|
||||
parser.add_argument("--data_root", required=True, default=None, type=str)
|
||||
parser.add_argument("--data_sets", default="dev_clean", type=str)
|
||||
parser.add_argument("--num_workers", default=4, type=int)
|
||||
args = parser.parse_args()
|
||||
|
||||
URLS = {
|
||||
'TRAIN_CLEAN_100': ("http://www.openslr.org/resources/12/train-clean-100.tar.gz"),
|
||||
'TRAIN_CLEAN_360': ("http://www.openslr.org/resources/12/train-clean-360.tar.gz"),
|
||||
'TRAIN_OTHER_500': ("http://www.openslr.org/resources/12/train-other-500.tar.gz"),
|
||||
'DEV_CLEAN': "http://www.openslr.org/resources/12/dev-clean.tar.gz",
|
||||
'DEV_OTHER': "http://www.openslr.org/resources/12/dev-other.tar.gz",
|
||||
'TEST_CLEAN': "http://www.openslr.org/resources/12/test-clean.tar.gz",
|
||||
'TEST_OTHER': "http://www.openslr.org/resources/12/test-other.tar.gz",
|
||||
}
|
||||
|
||||
|
||||
def __maybe_download_file(destination: str, source: str):
|
||||
"""
|
||||
Downloads source to destination if it doesn't exist.
|
||||
If exists, skips download
|
||||
Args:
|
||||
destination: local filepath
|
||||
source: url of resource
|
||||
Returns:
|
||||
"""
|
||||
source = URLS[source]
|
||||
if not os.path.exists(destination):
|
||||
logging.info("{0} does not exist. Downloading ...".format(destination))
|
||||
urllib.request.urlretrieve(source, filename=destination + '.tmp')
|
||||
os.rename(destination + '.tmp', destination)
|
||||
logging.info("Downloaded {0}.".format(destination))
|
||||
else:
|
||||
logging.info("Destination {0} exists. Skipping.".format(destination))
|
||||
return destination
|
||||
|
||||
|
||||
def __extract_file(filepath: str, data_dir: str):
|
||||
try:
|
||||
tar = tarfile.open(filepath)
|
||||
tar.extractall(data_dir)
|
||||
tar.close()
|
||||
except Exception:
|
||||
logging.info('Not extracting. Maybe already there?')
|
||||
|
||||
|
||||
def __process_transcript(file_path: str, dst_folder: str):
|
||||
"""
|
||||
Converts flac files to wav from a given transcript, capturing the metadata.
|
||||
Args:
|
||||
file_path: path to a source transcript with flac sources
|
||||
dst_folder: path where wav files will be stored
|
||||
Returns:
|
||||
a list of metadata entries for processed files.
|
||||
"""
|
||||
entries = []
|
||||
root = os.path.dirname(file_path)
|
||||
with open(file_path, encoding="utf-8") as fin:
|
||||
for line in fin:
|
||||
id, text = line[: line.index(" ")], line[line.index(" ") + 1 :]
|
||||
transcript_text = text.lower().strip()
|
||||
|
||||
# Convert FLAC file to WAV
|
||||
flac_file = os.path.join(root, id + ".flac")
|
||||
wav_file = os.path.join(dst_folder, id + ".wav")
|
||||
if not os.path.exists(wav_file):
|
||||
Transformer().build(flac_file, wav_file)
|
||||
# check duration
|
||||
duration = subprocess.check_output("soxi -D {0}".format(wav_file), shell=True)
|
||||
|
||||
entry = {}
|
||||
entry['audio_filepath'] = os.path.abspath(wav_file)
|
||||
entry['duration'] = float(duration)
|
||||
entry['text'] = transcript_text
|
||||
entries.append(entry)
|
||||
return entries
|
||||
|
||||
|
||||
def __process_data(data_folder: str, dst_folder: str, manifest_file: str, num_workers: int):
|
||||
"""
|
||||
Converts flac to wav and build manifests's json
|
||||
Args:
|
||||
data_folder: source with flac files
|
||||
dst_folder: where wav files will be stored
|
||||
manifest_file: where to store manifest
|
||||
num_workers: number of parallel workers processing files
|
||||
Returns:
|
||||
"""
|
||||
|
||||
if not os.path.exists(dst_folder):
|
||||
os.makedirs(dst_folder)
|
||||
|
||||
files = []
|
||||
entries = []
|
||||
|
||||
for root, dirnames, filenames in os.walk(data_folder):
|
||||
for filename in fnmatch.filter(filenames, '*.trans.txt'):
|
||||
files.append(os.path.join(root, filename))
|
||||
|
||||
with multiprocessing.Pool(num_workers) as p:
|
||||
processing_func = functools.partial(__process_transcript, dst_folder=dst_folder)
|
||||
results = p.imap(processing_func, files)
|
||||
for result in tqdm(results, total=len(files)):
|
||||
entries.extend(result)
|
||||
|
||||
with open(manifest_file, 'w') as fout:
|
||||
for m in entries:
|
||||
fout.write(json.dumps(m) + '\n')
|
||||
|
||||
|
||||
def main():
|
||||
data_root = args.data_root
|
||||
data_sets = args.data_sets
|
||||
num_workers = args.num_workers
|
||||
|
||||
if data_sets == "ALL":
|
||||
data_sets = "dev_clean,dev_other,train_clean_100,train_clean_360,train_other_500,test_clean,test_other"
|
||||
|
||||
for data_set in data_sets.split(','):
|
||||
logging.info("\n\nWorking on: {0}".format(data_set))
|
||||
filepath = os.path.join(data_root, data_set + ".tar.gz")
|
||||
logging.info("Getting {0}".format(data_set))
|
||||
__maybe_download_file(filepath, data_set.upper())
|
||||
logging.info("Extracting {0}".format(data_set))
|
||||
__extract_file(filepath, data_root)
|
||||
logging.info("Processing {0}".format(data_set))
|
||||
__process_data(
|
||||
os.path.join(os.path.join(data_root, "LibriSpeech"), data_set.replace("_", "-"),),
|
||||
os.path.join(os.path.join(data_root, "LibriSpeech"), data_set.replace("_", "-"),) + "-processed",
|
||||
os.path.join(data_root, data_set + ".json"),
|
||||
num_workers=num_workers,
|
||||
)
|
||||
logging.info('Done!')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,140 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# USAGE: python get_openslr_rir_data.py --data_root=<where to put data>
|
||||
# Data is downloaded from OpenSLR's "Room Impulse Response and Noise Database"
|
||||
# RIRs in multichannel files are separated into single channel files and
|
||||
# a json file that can be used as in input to NeMo is created
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import urllib.request
|
||||
from shutil import copy, move
|
||||
from zipfile import ZipFile
|
||||
|
||||
parser = argparse.ArgumentParser(description='OpenSLR RIR Data download and process')
|
||||
parser.add_argument("--data_root", required=True, default=None, type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
URLS = {
|
||||
'SLR28': ("http://www.openslr.org/resources/28/rirs_noises.zip"),
|
||||
}
|
||||
|
||||
|
||||
def __maybe_download_file(destination: str, source: str):
|
||||
"""
|
||||
Downloads source to destination if it doesn't exist.
|
||||
If exists, skips download
|
||||
Args:
|
||||
destination: local filepath
|
||||
source: url of resource
|
||||
Returns:
|
||||
"""
|
||||
source = URLS[source]
|
||||
if not os.path.exists(destination):
|
||||
logging.info("{0} does not exist. Downloading ...".format(destination))
|
||||
urllib.request.urlretrieve(source, filename=destination + '.tmp')
|
||||
os.rename(destination + '.tmp', destination)
|
||||
logging.info("Downloaded {0}.".format(destination))
|
||||
else:
|
||||
logging.info("Destination {0} exists. Skipping.".format(destination))
|
||||
return destination
|
||||
|
||||
|
||||
def __extract_file(filepath: str, data_dir: str):
|
||||
try:
|
||||
with ZipFile(filepath, 'r') as zipObj:
|
||||
zipObj.extractall(data_dir)
|
||||
except Exception:
|
||||
logging.info('Not extracting. Maybe already there?')
|
||||
|
||||
|
||||
def __process_data(data_folder: str, dst_folder: str, manifest_file: str):
|
||||
"""
|
||||
Converts flac to wav and build manifests's json
|
||||
Args:
|
||||
data_folder: source with flac files
|
||||
dst_folder: where wav files will be stored
|
||||
manifest_file: where to store manifest
|
||||
Returns:
|
||||
"""
|
||||
if not os.path.exists(dst_folder):
|
||||
os.makedirs(dst_folder)
|
||||
|
||||
real_rir_list = os.path.join(data_folder, "RIRS_NOISES", "real_rirs_isotropic_noises", "rir_list")
|
||||
rirfiles = []
|
||||
with open(real_rir_list, "r") as rir_f:
|
||||
for line in rir_f:
|
||||
rirfiles.append(os.path.join(data_folder, line.rstrip().split(" ")[4]))
|
||||
|
||||
real_rir_folder = os.path.join(dst_folder, "real_rirs")
|
||||
if not os.path.exists(real_rir_folder):
|
||||
os.makedirs(real_rir_folder)
|
||||
# split multi-channel rir files to single channel
|
||||
for rir_f in rirfiles:
|
||||
n_chans = int(subprocess.check_output("soxi -c {0}".format(rir_f), shell=True))
|
||||
if n_chans == 1:
|
||||
copy(rir_f, real_rir_folder)
|
||||
else:
|
||||
for chan in range(1, n_chans + 1):
|
||||
chan_file_name = os.path.join(
|
||||
real_rir_folder, os.path.splitext(os.path.basename(rir_f))[0] + "-" + str(chan) + ".wav"
|
||||
)
|
||||
_ = subprocess.check_output(f"sox {rir_f} {chan_file_name} remix {chan}", shell=True)
|
||||
|
||||
# move simulated rirs to processed
|
||||
if not os.path.exists(os.path.join(dst_folder, "simulated_rirs")):
|
||||
move(os.path.join(data_folder, "RIRS_NOISES", "simulated_rirs"), dst_folder)
|
||||
|
||||
os.chdir(dst_folder)
|
||||
all_rirs = glob.glob("**/*.wav", recursive=True)
|
||||
with open(manifest_file, 'w') as man_f:
|
||||
entry = {}
|
||||
for rir in all_rirs:
|
||||
rir_file = os.path.join(dst_folder, rir)
|
||||
duration = subprocess.check_output("soxi -D {0}".format(rir_file), shell=True)
|
||||
entry['audio_filepath'] = rir_file
|
||||
entry['duration'] = float(duration)
|
||||
entry['offset'] = 0
|
||||
entry['text'] = '_'
|
||||
man_f.write(json.dumps(entry) + "\n")
|
||||
|
||||
print('Done!')
|
||||
|
||||
|
||||
def main():
|
||||
data_root = os.path.abspath(args.data_root)
|
||||
data_set = "slr28"
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
logging.info("\n\nWorking on: {0}".format(data_set))
|
||||
filepath = os.path.join(data_root, data_set + ".zip")
|
||||
logging.info("Getting {0}".format(data_set))
|
||||
__maybe_download_file(filepath, data_set.upper())
|
||||
logging.info("Extracting {0}".format(data_set))
|
||||
__extract_file(filepath, data_root)
|
||||
logging.info("Processing {0}".format(data_set))
|
||||
__process_data(
|
||||
data_root,
|
||||
os.path.join(os.path.join(data_root, "processed")),
|
||||
os.path.join(os.path.join(data_root, "processed", "rir.json")),
|
||||
)
|
||||
logging.info('Done!')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,14 @@
|
||||
These scripts are used for extracting features from LJSpeech.
|
||||
|
||||
You will need to run the following scripts (note that these will call the other
|
||||
scripts as necessary):
|
||||
|
||||
- `create_manifests_and_textfiles.py`: Creates the manifest and text files that
|
||||
are used by the Montreal Forced Aligner (MFA) library.
|
||||
- `extract_ljspeech_phonemes_and_durs.sh`: Extracts phonemes and alignments via
|
||||
MFA and calculates phoneme durations.
|
||||
Needs to be run with Conda active since it creates a Conda environment.
|
||||
Also note that you may need to install OpenBlas manually (e.g. sudo apt-get
|
||||
install libopenblas-dev), sometimes MFA can't find it if installed via Conda.
|
||||
(This script calls `create_token2idx_dict.py` and `calculate_durs.py`.)
|
||||
- `extract_ljspeech_energy_pitch.py`
|
||||
@@ -0,0 +1,126 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Calculates durations for LJSpeech based on MFA TextGrid alignments.
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
from math import ceil
|
||||
|
||||
import numpy as np
|
||||
import tgt
|
||||
import torch
|
||||
from tqdm import tqdm
|
||||
|
||||
parser = argparse.ArgumentParser(description="Calculates phoneme durations for LJSpeech from TextGrids.")
|
||||
parser.add_argument('--ljspeech_dir', required=True, default=None, type=str)
|
||||
parser.add_argument(
|
||||
'--mappings',
|
||||
required=False,
|
||||
default=None,
|
||||
type=str,
|
||||
help='JSON file of mappings created with create_token2idx_dict.py',
|
||||
)
|
||||
parser.add_argument('--sr', required=False, default=22050, type=int)
|
||||
parser.add_argument('--hop_length', required=False, default=256, type=int)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def calculate_durations(textgrid, phone2idx):
|
||||
tokens = []
|
||||
durs = []
|
||||
|
||||
frames_per_second = args.sr / args.hop_length
|
||||
tg = tgt.read_textgrid(textgrid, include_empty_intervals=True)
|
||||
data_tier = tg.get_tier_by_name("phones")
|
||||
|
||||
# Get total frames
|
||||
total_frames = ceil((data_tier.end_time - data_tier.start_time) * frames_per_second)
|
||||
|
||||
# Find start and end frames of each token
|
||||
se_in_frames = np.array([(frames_per_second * d.start_time, frames_per_second * d.end_time) for d in data_tier])
|
||||
se_in_frames = np.round(se_in_frames)
|
||||
durs = (se_in_frames[:, 1] - se_in_frames[:, 0]).astype(int)
|
||||
blank_set = ('sil', 'sp', 'spn', '', '<unk>')
|
||||
blank_token = " "
|
||||
|
||||
# merge repeated blank tokens
|
||||
tokens, durations = [], []
|
||||
for i in range(len(data_tier)):
|
||||
x = data_tier[i].text
|
||||
if x == 'spn':
|
||||
return None, None, None
|
||||
x = blank_token if x in blank_set else x
|
||||
|
||||
if len(tokens) and tokens[-1] == blank_token and x == blank_token:
|
||||
durations[-1] += durs[i]
|
||||
else:
|
||||
tokens.append(x)
|
||||
durations.append(durs[i])
|
||||
|
||||
tokens_enc = [phone2idx[token] for token in tokens]
|
||||
tokens_enc, durations = torch.LongTensor(tokens_enc), torch.LongTensor(durations)
|
||||
|
||||
# Add rounding error to final token
|
||||
durations[-1] += total_frames - durations.sum()
|
||||
|
||||
return tokens, tokens_enc, durations
|
||||
|
||||
|
||||
def main():
|
||||
textgrid_list = glob.glob(os.path.join(args.ljspeech_dir, 'alignments/wavs/*.TextGrid'))
|
||||
|
||||
# Create target_dir if necessary
|
||||
target_dir = os.path.join(args.ljspeech_dir, 'phoneme_durations/')
|
||||
print(f"Calculating phoneme durations, files will be in: {target_dir}")
|
||||
|
||||
if not os.path.exists(target_dir):
|
||||
print(f"Creating {target_dir}")
|
||||
os.mkdir(target_dir)
|
||||
|
||||
# Read phoneme to idx mappings
|
||||
phone2idx = None
|
||||
if args.mappings:
|
||||
with open(args.mappings, 'r') as f:
|
||||
mappings = json.load(f)
|
||||
phone2idx = mappings['phone2idx']
|
||||
|
||||
oov_samples = []
|
||||
|
||||
# Iterate through all TextGrid files
|
||||
for textgrid in tqdm(textgrid_list):
|
||||
basename = os.path.splitext(os.path.basename(textgrid))[0][5:] # Chop off 'wavs_' prefix
|
||||
|
||||
phones_mfa, tokens_mfa, durs = calculate_durations(textgrid, phone2idx)
|
||||
|
||||
if phones_mfa is None:
|
||||
oov_samples.append(basename)
|
||||
continue
|
||||
|
||||
# Save to file
|
||||
target_path = os.path.join(target_dir, f'{basename}.pt')
|
||||
torch.save({'text_encoded': tokens_mfa, 'token_duration': durs}, target_path)
|
||||
|
||||
print(f"Getting rid of {len(oov_samples)} samples with OOV words.")
|
||||
oov_target = os.path.join(args.ljspeech_dir, 'wavs_to_ignore.pkl')
|
||||
with open(oov_target, 'wb') as f:
|
||||
pickle.dump(oov_samples, f)
|
||||
print(f"List of OOV samples written to: {oov_target}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,71 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Creates the manifest and .txt transcript files for an LJSpeech split.
|
||||
The manifest will be used for training, and the .txt files are for the MFA library to find.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
|
||||
import sox
|
||||
import wget
|
||||
|
||||
from nemo.collections.common.parts.preprocessing import parsers
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--ljspeech_base', required=True, default=None, type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
filelist_base = 'https://raw.githubusercontent.com/NVIDIA/tacotron2/master/filelists/'
|
||||
filelists = ['train', 'val', 'test']
|
||||
|
||||
# NeMo parser for text normalization
|
||||
text_parser = parsers.make_parser(name='en')
|
||||
|
||||
for split in filelists:
|
||||
# Download file list if necessary
|
||||
filelist_path = os.path.join(args.ljspeech_base, f"ljs_audio_text_{split}_filelist.txt")
|
||||
if not os.path.exists(filelist_path):
|
||||
wget.download(f"{filelist_base}/ljs_audio_text_{split}_filelist.txt", out=args.ljspeech_base)
|
||||
|
||||
manifest_target = os.path.join(args.ljspeech_base, f"ljspeech_{split}.json")
|
||||
with open(manifest_target, 'w') as f_out:
|
||||
with open(filelist_path, 'r') as filelist:
|
||||
print(f"\nCreating {manifest_target}...")
|
||||
for line in filelist:
|
||||
basename = line[6:16]
|
||||
text = text_parser._normalize(line[21:].strip())
|
||||
|
||||
# Make sure corresponding wavfile exists and write .txt transcript
|
||||
wav_path = os.path.join(args.ljspeech_base, 'wavs/', basename + '.wav')
|
||||
assert os.path.exists(wav_path)
|
||||
txt_path = os.path.join(args.ljspeech_base, 'wavs/', basename + '.txt')
|
||||
with open(txt_path, 'w') as f_txt:
|
||||
f_txt.write(text)
|
||||
|
||||
# Write manifest entry
|
||||
entry = {
|
||||
'audio_filepath': wav_path,
|
||||
'duration': sox.file_info.duration(wav_path),
|
||||
'text': text,
|
||||
}
|
||||
f_out.write(json.dumps(entry) + '\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,68 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Creates a dictionary from token to index based on dictionary .txt given.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--dictionary', required=True, default=None, type=str)
|
||||
parser.add_argument('--dict_out', required=True, default=None, type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(args.dictionary):
|
||||
raise FileNotFoundError(f"Could not find dictionary file {args.dictionary}")
|
||||
|
||||
phonemes = set()
|
||||
word2phones = {}
|
||||
with open(args.dictionary, 'r') as f:
|
||||
for line in f:
|
||||
line = line.split()
|
||||
word = line[0]
|
||||
tokens = line[1:]
|
||||
|
||||
word2phones[word] = tokens
|
||||
phonemes.update(tokens)
|
||||
|
||||
# Small list of additional punctuation
|
||||
word2phones[','] = [' ']
|
||||
word2phones[';'] = [' ']
|
||||
word2phones['.'] = [' ']
|
||||
word2phones['!'] = [' ']
|
||||
word2phones['?'] = [' ']
|
||||
word2phones['"'] = [' ']
|
||||
word2phones['-'] = [' ']
|
||||
|
||||
phone2idx = {k: i for i, k in enumerate(phonemes)}
|
||||
phone2idx[' '] = len(phone2idx)
|
||||
phone2idx['sil'] = phone2idx[' '] # Silence
|
||||
phone2idx['sp'] = phone2idx[' '] # Space
|
||||
phone2idx['spn'] = phone2idx[' '] # OOV/unk
|
||||
|
||||
dicts = {
|
||||
'phone2idx': phone2idx,
|
||||
'word2phones': word2phones,
|
||||
}
|
||||
with open(args.dict_out, 'w') as f:
|
||||
json.dump(dicts, f)
|
||||
|
||||
print(f"Total number of phone indices: {len(phone2idx)}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,88 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Extracts energy and pitch data from LJSpeech wav files and writes them to a directory as LJxxx-xxxx.npy files.
|
||||
Assuming that the wavs are located at `<LJSpeech_base_path>/wavs`, this script will write the
|
||||
energy files to `<LJSpeech_base_path>/energies/` and the pitch files to `<LJSpeech_base_path>/pitches`,
|
||||
creating the relevant directories if necessary.
|
||||
|
||||
USAGE: python extract_ljspeech_energy_pitch.py --ljspeech_dir=<LJSpeech_base_path>
|
||||
"""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import librosa
|
||||
import numpy as np
|
||||
|
||||
tqdm = None
|
||||
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extracts energies (L2-norm of STFT frame amplitudes) and pitches (F0) from LJSpeech data."
|
||||
)
|
||||
parser.add_argument("--ljspeech_dir", required=True, default=None, type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
wavfile_dir = Path(args.ljspeech_dir) / "wavs"
|
||||
wavfile_list = list(wavfile_dir.glob('*.wav'))
|
||||
|
||||
target_dir = Path(args.ljspeech_dir)
|
||||
# Create target dir <LJSpeech_base_dir>/energies and <LJSpeech_base_dir>/pitches if necessary
|
||||
if not Path(target_dir / "energies").exists():
|
||||
print(f"Creating target directory: {target_dir/'energies'}")
|
||||
Path(target_dir / "energies").mkdir()
|
||||
if not Path(target_dir / "pitches").exists():
|
||||
print(f"Creating target directory: {target_dir/'pitches'}")
|
||||
Path(target_dir / "pitches").mkdir()
|
||||
|
||||
if tqdm is not None:
|
||||
wavfile_list = tqdm(wavfile_list)
|
||||
for count, file_ in enumerate(wavfile_list):
|
||||
basename = Path(file_).stem
|
||||
pitch_path = target_dir / "pitches" / f"{basename}.npy"
|
||||
energy_path = target_dir / "energies" / f"{basename}.npy"
|
||||
if pitch_path.exists() and energy_path.exists():
|
||||
continue
|
||||
audio, sr = librosa.load(file_, sr=22050)
|
||||
|
||||
# Calculate f0
|
||||
# Please note that fmin and fmax are good approximates for the speaker in LJSpeech and may not generalize to
|
||||
# other speakers
|
||||
f0, _, _ = librosa.pyin(audio, fmin=80, fmax=800, frame_length=1024, sr=sr, fill_na=0.0)
|
||||
|
||||
# Save to new file
|
||||
np.save(pitch_path, f0)
|
||||
|
||||
# Calculate energy
|
||||
stft_amplitude = np.abs(librosa.stft(audio, n_fft=1024, hop_length=256, win_length=1024))
|
||||
energy = np.linalg.norm(stft_amplitude, axis=0) # axis=0 since librosa.stft -> (freq bins, frames)
|
||||
|
||||
# Save to new file
|
||||
np.save(energy_path, energy)
|
||||
|
||||
assert energy.shape == f0.shape
|
||||
if tqdm is None and count % 1000 == 0:
|
||||
print(f"Finished processing {count} wav files...")
|
||||
|
||||
print(f"Finished energy extraction for a total of {len(wavfile_list)} wav files.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Extracts the phonemes and alignments for the LJSpeech dataset via the
|
||||
# Montreal Forced Aligner (MFA) library, and computes durations per phoneme.
|
||||
# Assumes you have downloaded and expanded the LJSpeech dataset, and that they
|
||||
# are located at the directory specified.
|
||||
#
|
||||
# This script will create:
|
||||
# - <LJSPEECH_BASE>/mappings.json: Contains word->phone and phone->idx mappings
|
||||
# - <LJSPEECH_BASE>/wavs_to_ignore.pkl: A pickled list of wavs to ignore
|
||||
# because of OOV words in their transcripts
|
||||
# - <LJSPEECH_BASE>/phoneme_durations/LJ*.npz: Numpy files for each wavfile
|
||||
# with fields 'tokens' (token indices) and 'durs' (durations per token)
|
||||
#
|
||||
# It will also create intermediate files:
|
||||
# - <LJSPEECH_BASE>/alignments/wavs/LJ*.TextGrid: MFA output
|
||||
#
|
||||
# Note: This script will create a conda environment to set up MFA, so please
|
||||
# make sure that conda is active before running this script.
|
||||
#
|
||||
# Note: You may need to install OpenBlas yourself if the MFA script can't find
|
||||
# it. (e.g. sudo apt-get install libopenblas-dev)
|
||||
#
|
||||
# Example Usage:
|
||||
# ./extract_ljspeech_phonemes_and_durs.sh /data/LJSpeech-1.1
|
||||
|
||||
ENV_NAME='aligner'
|
||||
|
||||
SAMPLE_RATE=22050
|
||||
WINDOW_STRIDE=256
|
||||
|
||||
CMUDICT_URL='https://raw.githubusercontent.com/cmusphinx/cmudict/master/cmudict.dict'
|
||||
SPLITS_BASE_URL='https://raw.githubusercontent.com/NVIDIA/tacotron2/master/filelists/'
|
||||
|
||||
# Usage info
|
||||
show_help() {
|
||||
cat << EOF
|
||||
Usage: $(basename "$0") [-h] \
|
||||
[--skip_env_setup] \
|
||||
[--g2p_dict=<G2P_DICT_PATH>] \
|
||||
<LJSPEECH_BASE>
|
||||
Extracts phonemes and their respective durations for the LJSpeech dataset using
|
||||
the Montreal Forced Aligner (MFA).
|
||||
|
||||
-h Help message
|
||||
--skip_env_setup (Optional) Skips setting up the MFA conda environment
|
||||
"aligner". Use only if you already have this set up.
|
||||
--g2p_dict (Optional) Path to the grapheme to phoneme dictionary
|
||||
text file, if already generated. If set, skips the G2P
|
||||
step.
|
||||
EOF
|
||||
}
|
||||
|
||||
SKIP_ENV_SETUP=false
|
||||
G2P_DICT=''
|
||||
|
||||
while :; do
|
||||
case $1 in
|
||||
-h|-\?|--help)
|
||||
show_help
|
||||
exit
|
||||
;;
|
||||
--g2p_dict=?*)
|
||||
G2P_DICT=${1#*=}
|
||||
;;
|
||||
--skip_env_setup)
|
||||
SKIP_ENV_SETUP=true
|
||||
;;
|
||||
*)
|
||||
break
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [[ -z $1 ]]; then
|
||||
echo "Must specify a LJSpeech base directory."
|
||||
exit 1
|
||||
fi
|
||||
LJSPEECH_BASE=$1
|
||||
|
||||
# Check for conda
|
||||
read -r -d '' CONDA_MESSAGE << EOM
|
||||
This script requires either Anaconda or Miniconda to be active, as it installs the Montreal Forced Aligner via a conda environment.
|
||||
See their documentation (https://montreal-forced-aligner.readthedocs.io/en/latest/installation.html) for more details.
|
||||
EOM
|
||||
if [ -z "$(which conda)" ]; then
|
||||
echo $CONDA_MESSAGE
|
||||
exit
|
||||
fi
|
||||
|
||||
CONDA_PREFIX=$(conda info --base)
|
||||
source $CONDA_PREFIX/etc/profile.d/conda.sh
|
||||
|
||||
# Set up env for MFA (env name "aligner") and install
|
||||
if $SKIP_ENV_SETUP; then
|
||||
echo "Skipping environment setup. Assuming env name "aligner" exists."
|
||||
else
|
||||
echo "Setting up conda environment for MFA (env name \"aligner\")..."
|
||||
conda create -n $ENV_NAME -c conda-forge openblas python=3.8 openfst pynini ngram baumwelch
|
||||
conda activate $ENV_NAME
|
||||
pip install montreal-forced-aligner tgt torch
|
||||
mfa thirdparty download
|
||||
echo "Conda environment \"$ENV_NAME\" set up."
|
||||
fi
|
||||
|
||||
if ! conda activate $ENV_NAME; then
|
||||
echo "Could not activate environment, see Conda output above."
|
||||
exit
|
||||
fi
|
||||
|
||||
# Download CMU word-to-phoneme dict and clean out comments so they're not mistaken for tokens
|
||||
if [ -z $G2P_DICT ]; then
|
||||
if [ ! -f $LJSPEECH_BASE/cmudict.dict ]; then
|
||||
echo "Downloading CMU dict."
|
||||
wget -P $LJSPEECH_BASE $CMUDICT_URL
|
||||
fi
|
||||
if [ ! -f $LJSPEECH_BASE/uncommented_cmudict.dict ]; then
|
||||
echo "Creating uncommented version of CMUdict."
|
||||
sed 's/\ \#.*//' $LJSPEECH_BASE/cmudict.dict > $LJSPEECH_BASE/uncommented_cmudict.dict
|
||||
fi
|
||||
G2P_DICT=$LJSPEECH_BASE/uncommented_cmudict.dict
|
||||
fi
|
||||
|
||||
# Run alignment
|
||||
echo "Starting MFA with dictionary at: $G2P_DICT"
|
||||
|
||||
read -r -d '' MFA_ERROR_MSG << EOM
|
||||
Could not run MFA. If it could not find OpenBlas, you may need to install it manually.
|
||||
(e.g. sudo apt-get install libopenblas-dev)
|
||||
EOM
|
||||
|
||||
if ! mfa download acoustic english; then
|
||||
echo $MFA_ERROR_MSG
|
||||
fi
|
||||
mfa align --clean $LJSPEECH_BASE $G2P_DICT english $LJSPEECH_BASE/alignments
|
||||
|
||||
# Create JSON mappings from word to phonemes and phonemes to indices
|
||||
echo "Creating word->phone and phone->idx mappings at $LJSPEECH_BASE/mappings.json..."
|
||||
python create_token2idx_dict.py \
|
||||
--dictionary=$G2P_DICT \
|
||||
--dict_out=$LJSPEECH_BASE/mappings.json
|
||||
|
||||
# Calculate phoneme durations
|
||||
echo "Calculating phoneme durations..."
|
||||
python calculate_durs.py \
|
||||
--ljspeech_dir=$LJSPEECH_BASE \
|
||||
--mappings=$LJSPEECH_BASE/mappings.json \
|
||||
--sr=$SAMPLE_RATE \
|
||||
--hop_length=$WINDOW_STRIDE
|
||||
echo "Phoneme durations and tokens written."
|
||||
@@ -0,0 +1,88 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import tarfile
|
||||
|
||||
import librosa
|
||||
import wget
|
||||
|
||||
parser = argparse.ArgumentParser(description="AN4 dataset download and processing")
|
||||
parser.add_argument("--data_root", required=True, default=None, type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def build_manifest(data_root, transcripts_path, manifest_path, wav_path):
|
||||
with open(transcripts_path, 'r') as fin:
|
||||
with open(manifest_path, 'w') as fout:
|
||||
for line in fin:
|
||||
# Lines look like this:
|
||||
# <s> transcript </s> (fileID)
|
||||
transcript = line[: line.find('(') - 1].lower()
|
||||
transcript = transcript.replace('<s>', '').replace('</s>', '')
|
||||
transcript = transcript.strip()
|
||||
|
||||
file_id = line[line.find('(') + 1 : -2] # e.g. "cen4-fash-b"
|
||||
audio_path = os.path.join(
|
||||
data_root, wav_path, file_id[file_id.find('-') + 1 : file_id.rfind('-')], file_id + '.wav',
|
||||
)
|
||||
|
||||
duration = librosa.core.get_duration(filename=audio_path)
|
||||
|
||||
# Write the metadata to the manifest
|
||||
metadata = {
|
||||
"audio_filepath": audio_path,
|
||||
"duration": duration,
|
||||
"text": transcript,
|
||||
}
|
||||
json.dump(metadata, fout)
|
||||
fout.write('\n')
|
||||
|
||||
|
||||
def main():
|
||||
data_root = os.path.abspath(args.data_root)
|
||||
|
||||
# Convert from .sph to .wav
|
||||
logging.info("Converting audio files to .wav...")
|
||||
sph_list = glob.glob(os.path.join(data_root, 'an4/**/*.sph'), recursive=True)
|
||||
for sph_path in sph_list:
|
||||
wav_path = sph_path[:-4] + '.wav'
|
||||
cmd = ['sox', sph_path, wav_path]
|
||||
subprocess.run(cmd)
|
||||
logging.info("Finished conversion.")
|
||||
|
||||
# Build manifests
|
||||
logging.info("Building training manifest...")
|
||||
train_transcripts = os.path.join(data_root, 'an4/etc/an4_train.transcription')
|
||||
train_manifest = os.path.join(data_root, 'an4/train_manifest.json')
|
||||
train_wavs = os.path.join(data_root, 'an4/wav/an4_clstk')
|
||||
build_manifest(data_root, train_transcripts, train_manifest, train_wavs)
|
||||
logging.info("Training manifests created.")
|
||||
|
||||
logging.info("Building test manifest...")
|
||||
test_transcripts = os.path.join(data_root, 'an4/etc/an4_test.transcription')
|
||||
test_manifest = os.path.join(data_root, 'an4/test_manifest.json')
|
||||
test_wavs = os.path.join(data_root, 'an4/wav/an4test_clstk')
|
||||
build_manifest(data_root, test_transcripts, test_manifest, test_wavs)
|
||||
logging.info("Test manifest created.")
|
||||
|
||||
logging.info("Done with AN4 processing!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,70 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from nemo.collections.tts.data.datalayers import preprocess_linear_specs_dataset
|
||||
|
||||
sys.path.insert(0, '../')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Create dataset fitted for training and validating deep griffin iteration from wavefiles'
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--valid_filelist",
|
||||
help="Filelist for validation set, with all validation audio files listed",
|
||||
required=True,
|
||||
default=None,
|
||||
type=str,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--train_filelist",
|
||||
help="Filelist for train set, with all train audio files listed",
|
||||
required=True,
|
||||
default=None,
|
||||
type=str,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--n_fft",
|
||||
help="Value for the n_fft parameter, and the filter length for the STFT",
|
||||
default=512,
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument("--hop_length", help="STFT parameter", default=256, type=int)
|
||||
parser.add_argument(
|
||||
"-d", "--destination", help="Destination to save the preprocessed data set to", default="/tmp", type=str
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--num_snr",
|
||||
help="Number of distinctive noisy samples to generate for each clear sample at the file list",
|
||||
default=1,
|
||||
type=int,
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
preprocess_linear_specs_dataset(
|
||||
args.valid_filelist, args.train_filelist, args.n_fft, args.hop_length, args.num_snr, args.destination
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,256 @@
|
||||
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# This script is heavily derived from the Patter HUB5 processing script written
|
||||
# by Ryan Leary
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from math import ceil, floor
|
||||
from operator import attrgetter
|
||||
|
||||
import numpy as np
|
||||
import scipy.io.wavfile as wavfile
|
||||
from tqdm import tqdm
|
||||
|
||||
parser = argparse.ArgumentParser(description="Prepare HUB5 data for training/eval")
|
||||
parser.add_argument(
|
||||
"--data_root", default=None, type=str, required=True, help="The path to the root LDC HUB5 dataset directory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dest_root",
|
||||
default=None,
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to the destination root directory for processed files.",
|
||||
)
|
||||
|
||||
# Optional arguments
|
||||
parser.add_argument(
|
||||
"--min_slice_duration", default=10.0, type=float, help="Minimum audio slice duration after processing.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
StmUtterance = namedtuple(
|
||||
'StmUtterance', ['filename', 'channel', 'speaker_id', 'begin', 'end', 'label', 'transcript',],
|
||||
)
|
||||
STM_LINE_FMT = re.compile(r"^(\w+)\s+(\w+)\s+(\w+)\s+([0-9.]+)\s+([0-9.]+)\s+(<.*>)?\s+(.+)$")
|
||||
|
||||
# Transcription errors and their fixes
|
||||
TRANSCRIPT_BUGS = {"en_4622-B-12079-12187": "KIND OF WEIRD BUT"}
|
||||
|
||||
|
||||
def get_utt_id(segment):
|
||||
"""
|
||||
Gives utterance IDs in a form like: en_4156-a-36558-37113
|
||||
"""
|
||||
return "{}-{}-{}-{}".format(segment.filename, segment.channel, int(segment.begin * 100), int(segment.end * 100),)
|
||||
|
||||
|
||||
def convert_utterances(sph_path, wav_path):
|
||||
"""
|
||||
Converts a sphere audio file to wav.
|
||||
"""
|
||||
cmd = ["sph2pipe", "-f", "wav", "-p", sph_path, wav_path]
|
||||
subprocess.run(cmd)
|
||||
|
||||
|
||||
def create_wavs(data_root, dest_root):
|
||||
"""
|
||||
Converts the English sph files to wav using sph2pipe.
|
||||
"""
|
||||
sph_root = os.path.join(data_root, "hub5e_00", "english")
|
||||
sph_list = glob.glob(os.path.join(sph_root, "*.sph"))
|
||||
|
||||
# Iterate over each sphere file and conver to wav
|
||||
for sph_path in tqdm(sph_list, desc="Converting to wav", unit="file"):
|
||||
sph_name, _ = os.path.splitext(os.path.basename(sph_path))
|
||||
wav_path = os.path.join(dest_root, 'full_audio_wav', sph_name + ".wav")
|
||||
cmd = ["sph2pipe", "-f", "wav", "-p", sph_path, wav_path]
|
||||
subprocess.run(cmd)
|
||||
|
||||
|
||||
def process_transcripts(dataset_root):
|
||||
"""
|
||||
Reads in transcripts for each audio segment and processes them.
|
||||
"""
|
||||
stm_path = os.path.join(dataset_root, "2000_hub5_eng_eval_tr", "reference", "hub5e00.english.000405.stm",)
|
||||
results = []
|
||||
chars = set()
|
||||
|
||||
with open(stm_path, "r") as fh:
|
||||
for line in fh:
|
||||
# lines with ';;' are comments
|
||||
if line.startswith(";;"):
|
||||
continue
|
||||
|
||||
if "IGNORE_TIME_SEGMENT_" in line:
|
||||
continue
|
||||
line = line.replace("<B_ASIDE>", "").replace("<E_ASIDE>", "")
|
||||
line = line.replace("(%HESITATION)", "UH")
|
||||
line = line.replace("-", "")
|
||||
line = line.replace("(%UH)", "UH")
|
||||
line = line.replace("(%AH)", "UH")
|
||||
line = line.replace("(", "").replace(")", "")
|
||||
|
||||
line = line.lower()
|
||||
|
||||
m = STM_LINE_FMT.search(line.strip())
|
||||
utt = StmUtterance(*m.groups())
|
||||
|
||||
# Convert begin/end times to float
|
||||
utt = utt._replace(begin=float(utt.begin))
|
||||
utt = utt._replace(end=float(utt.end))
|
||||
|
||||
# Check for utterance in dict of transcript mistakes
|
||||
transcript_update = TRANSCRIPT_BUGS.get(get_utt_id(utt))
|
||||
if transcript_update is not None:
|
||||
utt = utt._replace(transcript=transcript_update)
|
||||
|
||||
results.append(utt)
|
||||
chars.update(list(utt.transcript))
|
||||
return results, chars
|
||||
|
||||
|
||||
def write_one_segment(dest_root, speaker_id, count, audio, sr, duration, transcript):
|
||||
"""
|
||||
Writes out one segment of audio, and writes its corresponding transcript
|
||||
in the manifest.
|
||||
|
||||
Args:
|
||||
dest_root: the path to the output directory root
|
||||
speaker_id: ID of the speaker, used in file naming
|
||||
count: number of segments from this speaker so far
|
||||
audio: the segment's audio data
|
||||
sr: sample rate of the audio
|
||||
duration: duration of the audio
|
||||
transcript: the corresponding transcript
|
||||
"""
|
||||
audio_path = os.path.join(dest_root, "audio", f"{speaker_id}_{count:03}.wav")
|
||||
|
||||
manifest_path = os.path.join(dest_root, "manifest_hub5.json")
|
||||
|
||||
# Write audio
|
||||
wavfile.write(audio_path, sr, audio)
|
||||
|
||||
# Write transcript
|
||||
transcript = {
|
||||
"audio_filepath": audio_path,
|
||||
"duration": duration,
|
||||
"text": transcript,
|
||||
}
|
||||
with open(manifest_path, 'a') as f:
|
||||
json.dump(transcript, f)
|
||||
f.write('\n')
|
||||
|
||||
|
||||
def segment_audio(info_list, dest_root, min_slice_duration):
|
||||
"""
|
||||
Combines audio into >= min_slice_duration segments of the same speaker,
|
||||
and writes the combined transcripts into a manifest.
|
||||
|
||||
Args:
|
||||
info_list: list of StmUtterance objects with transcript information.
|
||||
dest_root: path to output destination
|
||||
min_slice_duration: min number of seconds per output audio slice
|
||||
"""
|
||||
info_list = sorted(info_list, key=attrgetter('speaker_id', 'begin'))
|
||||
|
||||
prev_id = None # For checking audio concatenation
|
||||
id_count = 0
|
||||
|
||||
sample_rate, audio_data = None, None
|
||||
transcript_buffer = ''
|
||||
audio_buffer = []
|
||||
buffer_duration = 0.0
|
||||
|
||||
# Iterate through utterances to build segments
|
||||
for info in info_list:
|
||||
if info.speaker_id != prev_id:
|
||||
# Scrap the remainder in the buffers and start next segment
|
||||
prev_id = info.speaker_id
|
||||
id_count = 0
|
||||
|
||||
sample_rate, audio_data = wavfile.read(os.path.join(dest_root, 'full_audio_wav', info.filename + '.wav'))
|
||||
transcript_buffer = ''
|
||||
audio_buffer = []
|
||||
buffer_duration = 0.0
|
||||
|
||||
# Append utterance info to buffers
|
||||
transcript_buffer += info.transcript
|
||||
channel = 0 if info.channel.lower() == 'a' else 1
|
||||
audio_buffer.append(
|
||||
audio_data[floor(info.begin * sample_rate) : ceil(info.end * sample_rate), channel,]
|
||||
)
|
||||
buffer_duration += info.end - info.begin
|
||||
|
||||
if buffer_duration < min_slice_duration:
|
||||
transcript_buffer += ' '
|
||||
else:
|
||||
# Write out segment and transcript
|
||||
id_count += 1
|
||||
write_one_segment(
|
||||
dest_root,
|
||||
info.speaker_id,
|
||||
id_count,
|
||||
np.concatenate(audio_buffer, axis=0),
|
||||
sample_rate,
|
||||
buffer_duration,
|
||||
transcript_buffer,
|
||||
)
|
||||
|
||||
transcript_buffer = ''
|
||||
audio_buffer = []
|
||||
buffer_duration = 0.0
|
||||
|
||||
|
||||
def main():
|
||||
data_root = args.data_root
|
||||
dest_root = args.dest_root
|
||||
|
||||
min_slice_duration = args.min_slice_duration
|
||||
|
||||
if not os.path.exists(os.path.join(dest_root, 'full_audio_wav')):
|
||||
os.makedirs(os.path.join(dest_root, 'full_audio_wav'))
|
||||
if not os.path.exists(os.path.join(dest_root, 'audio')):
|
||||
os.makedirs(os.path.join(dest_root, 'audio'))
|
||||
|
||||
# Create/wipe manifest contents
|
||||
open(os.path.join(dest_root, "manifest_hub5.json"), 'w').close()
|
||||
|
||||
# Convert full audio files from .sph to .wav
|
||||
create_wavs(data_root, dest_root)
|
||||
|
||||
# Get each audio transcript from transcript file
|
||||
info_list, chars = process_transcripts(data_root)
|
||||
|
||||
print("Writing out vocab file", file=sys.stderr)
|
||||
with open(os.path.join(dest_root, "vocab.txt"), 'w') as fh:
|
||||
for x in sorted(list(chars)):
|
||||
fh.write(x + "\n")
|
||||
|
||||
# Segment the audio data
|
||||
print("Segmenting audio and writing manifest")
|
||||
segment_audio(info_list, dest_root, min_slice_duration)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,402 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Usage:
|
||||
|
||||
python process_speech_commands_data.py \
|
||||
--data_root=<absolute path to where the data should be stored> \
|
||||
--data_version=<either 1 or 2, indicating version of the dataset> \
|
||||
--class_split=<either "all" or "sub", indicates whether all 30/35 classes should be used, or the 10+2 split should be used> \
|
||||
--rebalance \
|
||||
--log
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import tarfile
|
||||
import urllib.request
|
||||
|
||||
import librosa
|
||||
import numpy as np
|
||||
|
||||
URL_v1 = "http://download.tensorflow.org/data/speech_commands_v0.01.tar.gz"
|
||||
URL_v2 = "http://download.tensorflow.org/data/speech_commands_v0.02.tar.gz"
|
||||
|
||||
|
||||
def __maybe_download_file(destination: str, source: str):
|
||||
"""
|
||||
Downloads source to destination if it doesn't exist.
|
||||
If exists, skips download
|
||||
Args:
|
||||
destination: local filepath
|
||||
source: url of resource
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if not os.path.exists(destination):
|
||||
logging.info(f"{destination} does not exist. Downloading ...")
|
||||
urllib.request.urlretrieve(source, filename=destination + '.tmp')
|
||||
os.rename(destination + '.tmp', destination)
|
||||
logging.info(f"Downloaded {destination}.")
|
||||
else:
|
||||
logging.info(f"Destination {destination} exists. Skipping.")
|
||||
return destination
|
||||
|
||||
|
||||
def __extract_all_files(filepath: str, data_root: str, data_dir: str):
|
||||
if not os.path.exists(data_dir):
|
||||
extract_file(filepath, data_dir)
|
||||
else:
|
||||
logging.info(f'Skipping extracting. Data already there {data_dir}')
|
||||
|
||||
|
||||
def extract_file(filepath: str, data_dir: str):
|
||||
try:
|
||||
tar = tarfile.open(filepath)
|
||||
tar.extractall(data_dir)
|
||||
tar.close()
|
||||
except Exception:
|
||||
logging.info('Not extracting. Maybe already there?')
|
||||
|
||||
|
||||
def __process_data(
|
||||
data_folder: str, dst_folder: str, rebalance: bool = False, class_split: str = "all", skip_duration: bool = False
|
||||
):
|
||||
"""
|
||||
To generate manifest
|
||||
|
||||
Args:
|
||||
data_folder: source with wav files and validation / test lists
|
||||
dst_folder: where manifest files will be stored
|
||||
rebalance: rebalance the classes to have same number of samples.
|
||||
class_split: whether to use all classes as distinct labels, or to use
|
||||
10 classes subset and rest of the classes as noise or background.
|
||||
skip_duration: Bool whether to skip duration computation. Use this only for
|
||||
colab notebooks where knowing duration is not necessary for demonstration.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
|
||||
if not os.path.exists(dst_folder):
|
||||
os.makedirs(dst_folder)
|
||||
|
||||
# Used for 10 classes + silence + unknown class setup
|
||||
class_subset = ["yes", "no", "up", "down", "left", "right", "on", "off", "stop", "go"]
|
||||
|
||||
pattern = re.compile(r"(.+\/)?(\w+)\/([^_]+)_.+wav")
|
||||
all_files = glob.glob(os.path.join(data_folder, '*/*wav'))
|
||||
|
||||
with open(os.path.join(data_folder, 'validation_list.txt'), 'r') as fin:
|
||||
validation_files = fin.readlines()
|
||||
|
||||
valset = set()
|
||||
for entry in validation_files:
|
||||
r = re.match(pattern, entry)
|
||||
if r:
|
||||
valset.add(r.group(3))
|
||||
|
||||
with open(os.path.join(data_folder, 'testing_list.txt'), 'r') as fin:
|
||||
testing_files = fin.readlines()
|
||||
|
||||
testset = set()
|
||||
for entry in testing_files:
|
||||
r = re.match(pattern, entry)
|
||||
if r:
|
||||
testset.add(r.group(3))
|
||||
|
||||
logging.info("Validation and test set lists extracted")
|
||||
|
||||
label_count = {}
|
||||
label_filepaths = {}
|
||||
unknown_val_filepaths = []
|
||||
unknown_test_filepaths = []
|
||||
|
||||
train, val, test = [], [], []
|
||||
for entry in all_files:
|
||||
r = re.match(pattern, entry)
|
||||
if r:
|
||||
label, uid = r.group(2), r.group(3)
|
||||
|
||||
if label == '_background_noise_' or label == 'silence':
|
||||
continue
|
||||
|
||||
if class_split == "sub":
|
||||
if label not in class_subset:
|
||||
label = "unknown" # replace label
|
||||
|
||||
if uid in valset:
|
||||
unknown_val_filepaths.append((label, entry))
|
||||
elif uid in testset:
|
||||
unknown_test_filepaths.append((label, entry))
|
||||
|
||||
sample = (label, entry)
|
||||
|
||||
if uid not in valset and uid not in testset:
|
||||
if label in label_count:
|
||||
label_count[label] += 1
|
||||
else:
|
||||
label_count[label] = 1
|
||||
|
||||
if label in label_filepaths:
|
||||
label_filepaths[label] += [sample]
|
||||
else:
|
||||
label_filepaths[label] = [sample]
|
||||
|
||||
if label == 'unknown':
|
||||
continue
|
||||
|
||||
if uid in valset:
|
||||
val.append(sample)
|
||||
elif uid in testset:
|
||||
test.append(sample)
|
||||
else:
|
||||
train.append(sample)
|
||||
|
||||
logging.info("Prepared filepaths for dataset")
|
||||
|
||||
# Add silence and unknown class label samples
|
||||
if class_split == "sub":
|
||||
logging.info("Perforiming 10+2 class subsplit")
|
||||
|
||||
silence_path = os.path.join(data_folder, "silence")
|
||||
if not os.path.exists(silence_path):
|
||||
os.mkdir(silence_path)
|
||||
|
||||
silence_stride = 1000 # 0.0625 second stride
|
||||
sampling_rate = 16000
|
||||
folder = os.path.join(data_folder, "_background_noise_")
|
||||
|
||||
silence_files = []
|
||||
rng = np.random.RandomState(0)
|
||||
|
||||
for file in os.listdir(folder):
|
||||
if ".wav" in file:
|
||||
load_path = os.path.join(folder, file)
|
||||
y, sr = librosa.load(load_path, sr=sampling_rate)
|
||||
|
||||
for i in range(0, len(y) - sampling_rate, silence_stride):
|
||||
file_path = "silence/{}_{}.wav".format(file[:-4], i)
|
||||
y_slice = y[i : i + sampling_rate]
|
||||
magnitude = rng.uniform(0.0, 1.0)
|
||||
y_slice *= magnitude
|
||||
out_file_path = os.path.join(data_folder, file_path)
|
||||
librosa.output.write_wav(out_file_path, y_slice, sr)
|
||||
|
||||
silence_files.append(('silence', out_file_path))
|
||||
|
||||
rng = np.random.RandomState(0)
|
||||
rng.shuffle(silence_files)
|
||||
logging.info(f"Constructed silence set of {len(silence_files)}")
|
||||
|
||||
# Create the splits
|
||||
rng = np.random.RandomState(0)
|
||||
silence_split = 0.1
|
||||
unknown_split = 0.1
|
||||
|
||||
# train split
|
||||
num_total_samples = sum([label_count[cls] for cls in class_subset])
|
||||
num_silence_samples = int(np.ceil(silence_split * num_total_samples))
|
||||
|
||||
# initialize sample
|
||||
label_count['silence'] = 0
|
||||
|
||||
for silence_id in range(num_silence_samples):
|
||||
label_count['silence'] += 1
|
||||
|
||||
if 'silence' in label_filepaths:
|
||||
label_filepaths['silence'] += [silence_files[silence_id]]
|
||||
else:
|
||||
label_filepaths['silence'] = [silence_files[silence_id]]
|
||||
|
||||
train.extend(label_filepaths['silence'])
|
||||
|
||||
# Update train unknown set
|
||||
unknown_train_samples = label_filepaths['unknown']
|
||||
|
||||
rng.shuffle(unknown_train_samples)
|
||||
unknown_size = int(np.ceil(unknown_split * num_total_samples))
|
||||
|
||||
label_count['unknown'] = unknown_size
|
||||
label_filepaths['unknown'] = unknown_train_samples[:unknown_size]
|
||||
|
||||
train.extend(label_filepaths['unknown'])
|
||||
|
||||
logging.info("Train set prepared")
|
||||
|
||||
# val set silence
|
||||
num_val_samples = len(val)
|
||||
num_silence_samples = int(np.ceil(silence_split * num_val_samples))
|
||||
|
||||
val_idx = label_count['silence'] + 1
|
||||
for silence_id in range(num_silence_samples):
|
||||
val.append(silence_files[val_idx + silence_id])
|
||||
|
||||
# Update val unknown set
|
||||
rng.shuffle(unknown_val_filepaths)
|
||||
unknown_size = int(np.ceil(unknown_split * num_val_samples))
|
||||
|
||||
val.extend(unknown_val_filepaths[:unknown_size])
|
||||
|
||||
logging.info("Validation set prepared")
|
||||
|
||||
# test set silence
|
||||
num_test_samples = len(test)
|
||||
num_silence_samples = int(np.ceil(silence_split * num_test_samples))
|
||||
|
||||
test_idx = val_idx + num_silence_samples + 1
|
||||
for silence_id in range(num_silence_samples):
|
||||
test.append(silence_files[test_idx + silence_id])
|
||||
|
||||
# Update test unknown set
|
||||
rng.shuffle(unknown_test_filepaths)
|
||||
unknown_size = int(np.ceil(unknown_split * num_test_samples))
|
||||
|
||||
test.extend(unknown_test_filepaths[:unknown_size])
|
||||
|
||||
logging.info("Test set prepared")
|
||||
|
||||
max_command = None
|
||||
max_count = -1
|
||||
for command, count in label_count.items():
|
||||
if command == 'unknown':
|
||||
continue
|
||||
|
||||
if count > max_count:
|
||||
max_count = count
|
||||
max_command = command
|
||||
|
||||
if rebalance:
|
||||
logging.info(f"Command with maximum number of samples = {max_command} with {max_count} samples")
|
||||
logging.info(f"Rebalancing dataset by duplicating classes with less than {max_count} samples...")
|
||||
|
||||
for command, samples in label_filepaths.items():
|
||||
filepaths = [sample[1] for sample in samples]
|
||||
|
||||
rng = np.random.RandomState(0)
|
||||
filepaths = np.asarray(filepaths)
|
||||
num_samples = len(filepaths)
|
||||
|
||||
if num_samples < max_count:
|
||||
difference = max_count - num_samples
|
||||
duplication_ids = rng.choice(num_samples, difference, replace=True)
|
||||
|
||||
filepaths = np.append(filepaths, filepaths[duplication_ids], axis=0)
|
||||
|
||||
logging.info(f"Extended class label {command} from {num_samples} samples to {len(filepaths)} samples")
|
||||
|
||||
label_filepaths[command] = [(command, filepath) for filepath in filepaths]
|
||||
|
||||
del train
|
||||
train = []
|
||||
for label, samples in label_filepaths.items():
|
||||
train.extend(samples)
|
||||
|
||||
manifests = [
|
||||
('train_manifest.json', train),
|
||||
('validation_manifest.json', val),
|
||||
('test_manifest.json', test),
|
||||
]
|
||||
|
||||
for manifest_filename, dataset in manifests:
|
||||
with open(os.path.join(dst_folder, manifest_filename), 'w') as fout:
|
||||
num_files = len(dataset)
|
||||
pct_file = num_files // 100
|
||||
file_count = 0
|
||||
|
||||
logging.info(f"Preparing manifest : {manifest_filename} with #{num_files} files")
|
||||
|
||||
for label, audio_path in dataset:
|
||||
if not skip_duration:
|
||||
duration = librosa.core.get_duration(filename=audio_path)
|
||||
else:
|
||||
duration = 0.0
|
||||
|
||||
# Write the metadata to the manifest
|
||||
metadata = {
|
||||
"audio_filepath": audio_path,
|
||||
"duration": duration,
|
||||
"command": label,
|
||||
}
|
||||
json.dump(metadata, fout)
|
||||
fout.write('\n')
|
||||
fout.flush()
|
||||
|
||||
file_count += 1
|
||||
if file_count % pct_file == 0:
|
||||
if not skip_duration:
|
||||
logging.info(f"Finished serializing {file_count} / {num_files} into {manifest_filename}")
|
||||
|
||||
logging.info(f"Finished construction of manifest : {manifest_filename}")
|
||||
|
||||
if skip_duration:
|
||||
logging.info(
|
||||
f"\n<<NOTE>> Duration computation was skipped for demonstration purposes on Colaboratory.\n"
|
||||
f"In order to replicate paper results and properly perform data augmentation, \n"
|
||||
f"please recompute the manifest file without the `--skip_duration` flag !\n"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Google Speech Command Data download')
|
||||
parser.add_argument("--data_root", required=True, default=None, type=str)
|
||||
parser.add_argument('--data_version', required=True, default=1, type=int, choices=[1, 2])
|
||||
parser.add_argument('--class_split', required=False, default='all', type=str, choices=['all', 'sub'])
|
||||
parser.add_argument('--rebalance', required=False, action='store_true')
|
||||
parser.add_argument('--skip_duration', required=False, action='store_true')
|
||||
parser.add_argument('--log', required=False, action='store_true')
|
||||
parser.set_defaults(log=False, rebalance=False, skip_duration=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.log:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
data_root = args.data_root
|
||||
data_set = "google_speech_recognition_v{0}".format(args.data_version)
|
||||
data_folder = os.path.join(data_root, data_set)
|
||||
|
||||
logging.info(f"Working on: {data_set}")
|
||||
|
||||
if args.data_version == 1:
|
||||
URL = URL_v1
|
||||
else:
|
||||
URL = URL_v2
|
||||
|
||||
# Download and extract
|
||||
if not os.path.exists(data_folder):
|
||||
file_path = os.path.join(data_root, data_set + ".tar.bz2")
|
||||
logging.info(f"Getting {data_set}")
|
||||
__maybe_download_file(file_path, URL)
|
||||
logging.info(f"Extracting {data_set}")
|
||||
__extract_all_files(file_path, data_root, data_folder)
|
||||
|
||||
logging.info(f"Processing {data_set}")
|
||||
__process_data(
|
||||
data_folder,
|
||||
data_folder,
|
||||
rebalance=args.rebalance,
|
||||
class_split=args.class_split,
|
||||
skip_duration=args.skip_duration,
|
||||
)
|
||||
logging.info('Done!')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,486 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Usage:
|
||||
|
||||
python process_vad_data.py \
|
||||
--out_dir=<output path to where the generated manifest should be stored> \
|
||||
--speech_data_root=<path where the speech data are stored> \
|
||||
--background_data_root=<path where the background data are stored> \
|
||||
--rebalance_method=<'under' or 'over' or 'fixed'> \
|
||||
--log
|
||||
(Optional --demo (for demonstration in tutorial). If you want to use your own background noise data, make sure to delete --demo)
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tarfile
|
||||
import urllib.request
|
||||
|
||||
import librosa
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
from sklearn.model_selection import train_test_split
|
||||
|
||||
sr = 16000
|
||||
|
||||
# google speech command v2
|
||||
URL = "http://download.tensorflow.org/data/speech_commands_v0.02.tar.gz"
|
||||
|
||||
|
||||
def __maybe_download_file(destination: str, source: str):
|
||||
"""
|
||||
Downloads source to destination if it doesn't exist.
|
||||
If exists, skips download
|
||||
Args:
|
||||
destination: local filepath
|
||||
source: url of resource
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if not os.path.exists(destination):
|
||||
logging.info(f"{destination} does not exist. Downloading ...")
|
||||
urllib.request.urlretrieve(source, filename=destination + '.tmp')
|
||||
os.rename(destination + '.tmp', destination)
|
||||
logging.info(f"Downloaded {destination}.")
|
||||
else:
|
||||
logging.info(f"Destination {destination} exists. Skipping.")
|
||||
return destination
|
||||
|
||||
|
||||
def extract_file(filepath: str, data_dir: str):
|
||||
try:
|
||||
tar = tarfile.open(filepath)
|
||||
tar.extractall(data_dir)
|
||||
tar.close()
|
||||
except Exception:
|
||||
logging.info('Not extracting. Maybe already there?')
|
||||
|
||||
|
||||
def __extract_all_files(filepath: str, data_root: str, data_dir: str):
|
||||
if not os.path.exists(data_dir):
|
||||
extract_file(filepath, data_dir)
|
||||
else:
|
||||
logging.info(f'Skipping extracting. Data already there {data_dir}')
|
||||
|
||||
|
||||
def split_train_val_test(data_dir, file_type, test_size=0.1, val_size=0.1, demo=False):
|
||||
X = []
|
||||
if file_type == "speech":
|
||||
for o in os.listdir(data_dir):
|
||||
if os.path.isdir(os.path.join(data_dir, o)) and o.split("/")[-1] != "_background_noise_":
|
||||
X.extend(glob.glob(os.path.join(data_dir, o) + '/*.wav'))
|
||||
|
||||
if demo:
|
||||
logging.info(
|
||||
f"For Demonstration, we use {int(len(X)/100)}/{len(X)} speech data. Make sure to remove --demo flag when you actually train your model!"
|
||||
)
|
||||
X = np.random.choice(X, int(len(X) / 100), replace=False)
|
||||
|
||||
else:
|
||||
for o in os.listdir(data_dir):
|
||||
if os.path.isdir(os.path.join(data_dir, o)):
|
||||
X.extend(glob.glob(os.path.join(data_dir, o) + '/*.wav'))
|
||||
else: # for using "_background_noise_" from google speech commands as background data
|
||||
if o.endswith(".wav"):
|
||||
X.append(os.path.join(data_dir, o))
|
||||
|
||||
X_train, X_test = train_test_split(X, test_size=test_size, random_state=1)
|
||||
val_size_tmp = val_size / (1 - test_size)
|
||||
X_train, X_val = train_test_split(X_train, test_size=val_size_tmp, random_state=1)
|
||||
|
||||
with open(os.path.join(data_dir, file_type + "_training_list.txt"), "w") as outfile:
|
||||
outfile.write("\n".join(X_train))
|
||||
with open(os.path.join(data_dir, file_type + "_testing_list.txt"), "w") as outfile:
|
||||
outfile.write("\n".join(X_test))
|
||||
with open(os.path.join(data_dir, file_type + "_validation_list.txt"), "w") as outfile:
|
||||
outfile.write("\n".join(X_val))
|
||||
|
||||
logging.info(f'Overall: {len(X)}, Train: {len(X_train)}, Validatoin: {len(X_val)}, Test: {len(X_test)}')
|
||||
logging.info(f"Finish spliting train, val and test for {file_type}. Write to files!")
|
||||
|
||||
|
||||
def process_google_speech_train(data_dir):
|
||||
X = []
|
||||
for o in os.listdir(data_dir):
|
||||
if os.path.isdir(os.path.join(data_dir, o)) and o.split("/")[-1] != "_background_noise_":
|
||||
X.extend(glob.glob(os.path.join(data_dir, o) + '/*.wav'))
|
||||
|
||||
short_files = [i.split(data_dir)[1] for i in files]
|
||||
|
||||
with open(os.path.join(data_dir, 'testing_list.txt'), 'r') as allfile:
|
||||
testing_list = allfile.read().splitlines()
|
||||
|
||||
with open(os.path.join(data_dir, 'validation_list.txt'), 'r') as allfile:
|
||||
validation_list = allfile.read().splitlines()
|
||||
|
||||
exist_set = set(testing_list).copy()
|
||||
exist_set.update(set(validation_list))
|
||||
|
||||
training_list = [i for i in short_files if i not in exist_set]
|
||||
|
||||
with open(os.path.join(data_dir, "training_list.txt"), "w") as outfile:
|
||||
outfile.write("\n".join(training_list))
|
||||
|
||||
logging.info(
|
||||
f'Overall: {len(files)}, Train: {len(training_list)}, Validatoin: {len(validation_list)}, Test: {len(testing_list)}'
|
||||
)
|
||||
|
||||
|
||||
def write_manifest(
|
||||
out_dir,
|
||||
files,
|
||||
prefix,
|
||||
manifest_name,
|
||||
start=0.0,
|
||||
end=None,
|
||||
duration_stride=1.0,
|
||||
duration_max=None,
|
||||
duration_limit=100.0,
|
||||
filter_long=False,
|
||||
):
|
||||
"""
|
||||
Given a list of files, segment each file and write them to manifest with restrictions.
|
||||
Args:
|
||||
out_dir: directory of generated manifest
|
||||
files: list of files to be processed
|
||||
prefix: label of samples
|
||||
manifest_name: name of generated manifest
|
||||
start: beginning of audio of generating segment
|
||||
end: end of audio of generating segment
|
||||
duration_stride: stride for segmenting audio samples
|
||||
duration_max: duration for each segment
|
||||
duration_limit: duration threshold for filtering out long audio samples
|
||||
filter_long: boolean to determine whether to filter out long audio samples
|
||||
Returns:
|
||||
"""
|
||||
seg_num = 0
|
||||
skip_num = 0
|
||||
if duration_max is None:
|
||||
duration_max = 1e9
|
||||
|
||||
if not os.path.exists(out_dir):
|
||||
logging.info(f'Outdir {out_dir} does not exist. Creat directory.')
|
||||
os.mkdir(out_dir)
|
||||
|
||||
output_path = os.path.join(out_dir, manifest_name + '.json')
|
||||
with open(output_path, 'w') as fout:
|
||||
for file in files:
|
||||
label = prefix
|
||||
|
||||
try:
|
||||
x, _sr = librosa.load(file, sr=sr)
|
||||
duration = librosa.get_duration(x, sr=sr)
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if filter_long and duration > duration_limit:
|
||||
skip_num += 1
|
||||
continue
|
||||
|
||||
offsets = []
|
||||
durations = []
|
||||
|
||||
if duration > duration_max:
|
||||
current_offset = start
|
||||
|
||||
while current_offset < duration:
|
||||
if end is not None and current_offset > end:
|
||||
break
|
||||
|
||||
difference = duration - current_offset
|
||||
|
||||
if difference < duration_max:
|
||||
break
|
||||
|
||||
offsets.append(current_offset)
|
||||
durations.append(duration_max)
|
||||
|
||||
current_offset += duration_stride
|
||||
|
||||
else:
|
||||
# Duration is not long enough! Skip
|
||||
skip_num += 1
|
||||
|
||||
for duration, offset in zip(durations, offsets):
|
||||
metadata = {
|
||||
'audio_filepath': file,
|
||||
'duration': duration,
|
||||
'label': label,
|
||||
'text': '_', # for compatibility with ASRAudioText
|
||||
'offset': offset,
|
||||
}
|
||||
json.dump(metadata, fout)
|
||||
fout.write('\n')
|
||||
fout.flush()
|
||||
seg_num += 1
|
||||
return skip_num, seg_num, output_path
|
||||
|
||||
|
||||
def load_list_write_manifest(
|
||||
data_dir,
|
||||
out_dir,
|
||||
filename,
|
||||
prefix,
|
||||
start,
|
||||
end,
|
||||
duration_stride=1.0,
|
||||
duration_max=1.0,
|
||||
duration_limit=100.0,
|
||||
filter_long=True,
|
||||
):
|
||||
|
||||
filename = prefix + '_' + filename
|
||||
file_path = os.path.join(data_dir, filename)
|
||||
|
||||
with open(file_path, 'r') as allfile:
|
||||
files = allfile.read().splitlines()
|
||||
|
||||
manifest_name = filename.split('_list.txt')[0] + '_manifest'
|
||||
skip_num, seg_num, output_path = write_manifest(
|
||||
out_dir,
|
||||
files,
|
||||
prefix,
|
||||
manifest_name,
|
||||
start,
|
||||
end,
|
||||
duration_stride,
|
||||
duration_max,
|
||||
duration_limit,
|
||||
filter_long=True,
|
||||
)
|
||||
return skip_num, seg_num, output_path
|
||||
|
||||
|
||||
def rebalance_json(data_dir, data_json, num, prefix):
|
||||
data = []
|
||||
seg = 0
|
||||
for line in open(data_json, 'r'):
|
||||
data.append(json.loads(line))
|
||||
filename = data_json.split('/')[-1]
|
||||
fout_path = os.path.join(data_dir, prefix + "_" + filename)
|
||||
|
||||
if len(data) >= num:
|
||||
selected_sample = np.random.choice(data, num, replace=False)
|
||||
else:
|
||||
selected_sample = np.random.choice(data, num, replace=True)
|
||||
|
||||
with open(fout_path, 'a') as fout:
|
||||
for i in selected_sample:
|
||||
seg += 1
|
||||
json.dump(i, fout)
|
||||
fout.write('\n')
|
||||
fout.flush()
|
||||
|
||||
logging.info(f'Get {seg}/{num} to {fout_path} from {data_json}')
|
||||
return fout_path
|
||||
|
||||
|
||||
def generate_variety_noise(data_dir, filename, prefix):
|
||||
|
||||
curr_dir = data_dir.split("_background_noise_")[0]
|
||||
silence_path = os.path.join(curr_dir, "_background_noise_more")
|
||||
|
||||
if not os.path.exists(silence_path):
|
||||
os.mkdir(silence_path)
|
||||
|
||||
silence_stride = 1000 # stride = 1/16 seconds
|
||||
sampling_rate = 16000
|
||||
|
||||
silence_files = []
|
||||
rng = np.random.RandomState(0)
|
||||
|
||||
filename = prefix + '_' + filename
|
||||
file_path = os.path.join(data_dir, filename)
|
||||
|
||||
with open(file_path, 'r') as allfile:
|
||||
files = allfile.read().splitlines()
|
||||
|
||||
for file in files:
|
||||
y, sr = librosa.load(file, sr=sampling_rate)
|
||||
|
||||
for i in range(
|
||||
0, len(y) - sampling_rate, silence_stride * 100
|
||||
): # stride * 100 to generate less samples for demo
|
||||
file_name = "{}_{}.wav".format(file.split("/")[-1], i)
|
||||
y_slice = y[i : i + sampling_rate]
|
||||
magnitude = rng.uniform(0.0, 1.0)
|
||||
y_slice *= magnitude
|
||||
out_file_path = os.path.join(silence_path, file_name)
|
||||
sf.write(out_file_path, y_slice, sr)
|
||||
|
||||
silence_files.append(out_file_path)
|
||||
|
||||
new_list_file = os.path.join(silence_path, filename)
|
||||
with open(new_list_file, "w") as outfile:
|
||||
outfile.write("\n".join(silence_files))
|
||||
|
||||
logging.info(f"Generate {len(out_file_path)} background files for {file_path}. => {new_list_file} !")
|
||||
return len(silence_files)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Speech and backgound data download and preprocess')
|
||||
parser.add_argument("--out_dir", required=False, default='./manifest/', type=str)
|
||||
parser.add_argument("--speech_data_root", required=True, default=None, type=str)
|
||||
parser.add_argument("--background_data_root", required=True, default=None, type=str)
|
||||
parser.add_argument('--test_size', required=False, default=0.1, type=float)
|
||||
parser.add_argument('--val_size', required=False, default=0.1, type=float)
|
||||
parser.add_argument('--seg_len', required=False, default=0.63, type=float)
|
||||
parser.add_argument('--log', required=False, action='store_true')
|
||||
parser.add_argument('--rebalance_method', required=False, default=None, type=str)
|
||||
parser.add_argument('--demo', required=False, action='store_true')
|
||||
parser.set_defaults(log=False, generate=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.rebalance_method:
|
||||
rebalance = False
|
||||
else:
|
||||
if args.rebalance_method != 'over' and args.rebalance_method != 'under' and args.rebalance_method != 'fixed':
|
||||
raise NameError("Please select a valid sampling method: over/under/fixed.")
|
||||
else:
|
||||
rebalance = True
|
||||
|
||||
if args.log:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# Download speech data
|
||||
speech_data_root = args.speech_data_root
|
||||
data_set = "google_speech_recognition_v2"
|
||||
speech_data_folder = os.path.join(speech_data_root, data_set)
|
||||
|
||||
background_data_folder = args.background_data_root
|
||||
logging.info(f"Working on: {data_set}")
|
||||
|
||||
# Download and extract speech data
|
||||
if not os.path.exists(speech_data_folder):
|
||||
file_path = os.path.join(speech_data_root, data_set + ".tar.bz2")
|
||||
logging.info(f"Getting {data_set}")
|
||||
__maybe_download_file(file_path, URL)
|
||||
logging.info(f"Extracting {data_set}")
|
||||
__extract_all_files(file_path, speech_data_root, speech_data_folder)
|
||||
|
||||
logging.info(f"Split speech data!")
|
||||
# dataset provide testing.txt and validation.txt feel free to split data using that with process_google_speech_train
|
||||
split_train_val_test(speech_data_folder, "speech", args.test_size, args.val_size, args.demo)
|
||||
|
||||
logging.info(f"Split background data!")
|
||||
split_train_val_test(background_data_folder, "background", args.test_size, args.val_size)
|
||||
|
||||
out_dir = args.out_dir
|
||||
|
||||
# Process Speech manifest
|
||||
logging.info(f"=== Write speech data to manifest!")
|
||||
skip_num_val, speech_seg_num_val, speech_val = load_list_write_manifest(
|
||||
speech_data_folder, out_dir, 'validation_list.txt', 'speech', 0.2, 0.8, args.seg_len, args.seg_len
|
||||
)
|
||||
skip_num_test, speech_seg_num_test, speech_test = load_list_write_manifest(
|
||||
speech_data_folder, out_dir, 'testing_list.txt', 'speech', 0.2, 0.8, 0.01, args.seg_len
|
||||
)
|
||||
skip_num_train, speech_seg_num_train, speech_train = load_list_write_manifest(
|
||||
speech_data_folder, out_dir, 'training_list.txt', 'speech', 0.2, 0.8, args.seg_len, args.seg_len
|
||||
)
|
||||
|
||||
logging.info(f'Val: Skip {skip_num_val} samples. Get {speech_seg_num_val} segments! => {speech_val} ')
|
||||
logging.info(f'Test: Skip {skip_num_test} samples. Get {speech_seg_num_test} segments! => {speech_test}')
|
||||
logging.info(f'Train: Skip {skip_num_train} samples. Get {speech_seg_num_train} segments!=> {speech_train}')
|
||||
|
||||
# Process background manifest
|
||||
# if we select to generate more background noise data
|
||||
if args.demo:
|
||||
logging.info("Start generating more background noise data")
|
||||
generate_variety_noise(background_data_folder, 'validation_list.txt', 'background')
|
||||
generate_variety_noise(background_data_folder, 'training_list.txt', 'background')
|
||||
generate_variety_noise(background_data_folder, 'testing_list.txt', 'background')
|
||||
background_data_folder = os.path.join(
|
||||
background_data_folder.split("_background_noise_")[0], "_background_noise_more"
|
||||
)
|
||||
|
||||
logging.info(f"=== Write background data to manifest!")
|
||||
skip_num_val, background_seg_num_val, background_val = load_list_write_manifest(
|
||||
background_data_folder, out_dir, 'validation_list.txt', 'background', 0, None, 0.15, args.seg_len
|
||||
)
|
||||
skip_num_test, background_seg_num_test, background_test = load_list_write_manifest(
|
||||
background_data_folder, out_dir, 'testing_list.txt', 'background', 0, None, 0.01, args.seg_len
|
||||
)
|
||||
skip_num_train, background_seg_num_train, background_train = load_list_write_manifest(
|
||||
background_data_folder, out_dir, 'training_list.txt', 'background', 0, None, 0.15, args.seg_len
|
||||
)
|
||||
|
||||
logging.info(f'Val: Skip {skip_num_val} samples. Get {background_seg_num_val} segments! => {background_val}')
|
||||
logging.info(f'Test: Skip {skip_num_test} samples. Get {background_seg_num_test} segments! => {background_test}')
|
||||
logging.info(
|
||||
f'Train: Skip {skip_num_train} samples. Get {background_seg_num_train} segments! => {background_train}'
|
||||
)
|
||||
min_val, max_val = min(speech_seg_num_val, background_seg_num_val), max(speech_seg_num_val, background_seg_num_val)
|
||||
min_test, max_test = (
|
||||
min(speech_seg_num_test, background_seg_num_test),
|
||||
max(speech_seg_num_test, background_seg_num_test),
|
||||
)
|
||||
min_train, max_train = (
|
||||
min(speech_seg_num_train, background_seg_num_train),
|
||||
max(speech_seg_num_train, background_seg_num_train),
|
||||
)
|
||||
|
||||
logging.info('Finish generating manifest!')
|
||||
|
||||
if rebalance:
|
||||
# Random Oversampling: Randomly duplicate examples in the minority class.
|
||||
# Random Undersampling: Randomly delete examples in the majority class.
|
||||
if args.rebalance_method == 'under':
|
||||
logging.info(f"Rebalancing number of samples in classes using {args.rebalance_method} sampling.")
|
||||
logging.info(f'Val: {min_val} Test: {min_test} Train: {min_train}!')
|
||||
|
||||
rebalance_json(out_dir, background_val, min_val, 'balanced')
|
||||
rebalance_json(out_dir, background_test, min_test, 'balanced')
|
||||
rebalance_json(out_dir, background_train, min_train, 'balanced')
|
||||
|
||||
rebalance_json(out_dir, speech_val, min_val, 'balanced')
|
||||
rebalance_json(out_dir, speech_test, min_test, 'balanced')
|
||||
rebalance_json(out_dir, speech_train, min_train, 'balanced')
|
||||
|
||||
if args.rebalance_method == 'over':
|
||||
logging.info(f"Rebalancing number of samples in classes using {args.rebalance_method} sampling.")
|
||||
logging.info(f'Val: {max_val} Test: {max_test} Train: {max_train}!')
|
||||
|
||||
rebalance_json(out_dir, background_val, max_val, 'balanced')
|
||||
rebalance_json(out_dir, background_test, max_test, 'balanced')
|
||||
rebalance_json(out_dir, background_train, max_train, 'balanced')
|
||||
|
||||
rebalance_json(out_dir, speech_val, max_val, 'balanced')
|
||||
rebalance_json(out_dir, speech_test, max_test, 'balanced')
|
||||
rebalance_json(out_dir, speech_train, max_train, 'balanced')
|
||||
|
||||
if args.rebalance_method == 'fixed':
|
||||
fixed_test, fixed_val, fixed_train = 200, 100, 500
|
||||
logging.info(f"Rebalancing number of samples in classes using {args.rebalance_method} sampling.")
|
||||
logging.info(f'Val: {fixed_val} Test: {fixed_test} Train: {fixed_train}!')
|
||||
|
||||
rebalance_json(out_dir, background_val, fixed_val, 'balanced')
|
||||
rebalance_json(out_dir, background_test, fixed_test, 'balanced')
|
||||
rebalance_json(out_dir, background_train, fixed_train, 'balanced')
|
||||
|
||||
rebalance_json(out_dir, speech_val, fixed_val, 'balanced')
|
||||
rebalance_json(out_dir, speech_test, fixed_test, 'balanced')
|
||||
rebalance_json(out_dir, speech_train, fixed_train, 'balanced')
|
||||
else:
|
||||
logging.info("Don't rebalance number of samples in classes.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import argparse
|
||||
import os
|
||||
import tarfile
|
||||
from shutil import copyfile
|
||||
|
||||
from nemo.collections.asr.models import EncDecClassificationModel, EncDecCTCModel, EncDecSpeakerLabelModel
|
||||
from nemo.utils import logging
|
||||
|
||||
|
||||
def get_parser():
|
||||
parser = argparse.ArgumentParser(description="Convert .nemo file to onnx file (encoder and decoder fused)")
|
||||
parser.add_argument(
|
||||
"--nemo_file", default=None, type=str, required=True, help="Path to .nemo file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--onnx_file", default=None, type=str, required=True, help="Path to the onnx output",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enemo_file", default=None, type=str, required=True, help="Path to the enemo output, consiting of onnx file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model_type",
|
||||
default="asr",
|
||||
type=str,
|
||||
choices=["asr", "speech_label", "speaker"],
|
||||
help="Type of decoder used by the model.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(
|
||||
nemo_file, enemo_file, onnx_file, model_type="asr",
|
||||
):
|
||||
if model_type == "asr":
|
||||
logging.info("Preparing ASR model")
|
||||
model = EncDecCTCModel.restore_from(nemo_file)
|
||||
elif model_type == "speech_label":
|
||||
logging.info("Preparing Speech Label Classification model")
|
||||
model = EncDecClassificationModel.restore_from(nemo_file)
|
||||
elif model_type == "speaker":
|
||||
logging.info("Preparing Speaker Recognition model")
|
||||
model = EncDecSpeakerLabelModel.restore_from(nemo_file)
|
||||
else:
|
||||
raise NameError("Available model names are asr, speech_label and speaker")
|
||||
|
||||
logging.info("Writing onnx file")
|
||||
model.export(onnx_file, onnx_opset_version=12)
|
||||
logging.info("succesfully ported onnx file")
|
||||
|
||||
with tarfile.open(nemo_file, "r") as archive:
|
||||
archive.extract("./model_config.yaml")
|
||||
with tarfile.open(enemo_file, "w") as enemo_archive:
|
||||
enemo_archive.add("./model_config.yaml")
|
||||
copyfile(onnx_file, "model_graph.onnx")
|
||||
enemo_archive.add("model_graph.onnx")
|
||||
os.remove("model_graph.onnx") # cleanup extra file
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = get_parser().parse_args()
|
||||
main(
|
||||
args.nemo_file, args.enemo_file, args.onnx_file, model_type=args.model_type,
|
||||
)
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import argparse
|
||||
|
||||
from nemo.collections.asr.models import EncDecClassificationModel, EncDecCTCModel, EncDecSpeakerLabelModel
|
||||
from nemo.utils import logging
|
||||
|
||||
|
||||
def get_parser():
|
||||
parser = argparse.ArgumentParser(description="Convert .nemo file to encoder decoder onnx files")
|
||||
parser.add_argument(
|
||||
"--nemo_file", default=None, type=str, required=True, help="Path to .nemo file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--onnx_encoder", default=None, type=str, required=True, help="Path to the onnx encoder output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--onnx_decoder", default=None, type=str, required=True, help="Path to the onnx decoder output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model_type",
|
||||
default='asr',
|
||||
type=str,
|
||||
choices=['asr', 'speech_label', 'speaker'],
|
||||
help="Type of decoder used by the model.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(
|
||||
nemo_file, onnx_encoder, onnx_decoder, model_type='asr',
|
||||
):
|
||||
if model_type == 'asr':
|
||||
logging.info("Preparing encoder decoder for ASR model")
|
||||
model = EncDecCTCModel.restore_from(nemo_file)
|
||||
elif model_type == 'speech_label':
|
||||
logging.info("Preparing encoder decoder for Speech Label Classification model")
|
||||
model = EncDecClassificationModel.restore_from(nemo_file)
|
||||
elif model_type == 'speaker':
|
||||
logging.info("Preparing encoder decoder for Speaker Recognition model")
|
||||
model = EncDecSpeakerLabelModel.restore_from(nemo_file)
|
||||
else:
|
||||
raise NameError("Available model names are asr, speech_label and speaker ")
|
||||
|
||||
logging.info("Writing onnx encoder and decoder onnx files")
|
||||
model.encoder.export(onnx_encoder)
|
||||
model.decoder.export(onnx_decoder, onnx_opset_version=10)
|
||||
logging.info("succesfully ported onnx files")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = get_parser().parse_args()
|
||||
main(
|
||||
args.nemo_file, args.onnx_encoder, args.onnx_decoder, model_type=args.model_type,
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import argparse
|
||||
|
||||
from nemo.collections.asr.models import EncDecClassificationModel, EncDecCTCModel, EncDecSpeakerLabelModel
|
||||
from nemo.utils import logging
|
||||
|
||||
|
||||
def get_parser():
|
||||
parser = argparse.ArgumentParser(description="Convert .nemo file to onnx file (encoder and decoder fused)")
|
||||
parser.add_argument(
|
||||
"--nemo_file", default=None, type=str, required=True, help="Path to .nemo file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--onnx_file", default=None, type=str, required=True, help="Path to the onnx output",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model_type",
|
||||
default='asr',
|
||||
type=str,
|
||||
choices=['asr', 'speech_label', 'speaker'],
|
||||
help="Type of decoder used by the model.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(
|
||||
nemo_file, onnx_file, model_type='asr',
|
||||
):
|
||||
if model_type == 'asr':
|
||||
logging.info("Preparing ASR model")
|
||||
model = EncDecCTCModel.restore_from(nemo_file)
|
||||
elif model_type == 'speech_label':
|
||||
logging.info("Preparing Speech Label Classification model")
|
||||
model = EncDecClassificationModel.restore_from(nemo_file)
|
||||
elif model_type == 'speaker':
|
||||
logging.info("Preparing Speaker Recognition model")
|
||||
model = EncDecSpeakerLabelModel.restore_from(nemo_file)
|
||||
else:
|
||||
raise NameError("Available model names are asr, speech_label and speaker")
|
||||
|
||||
logging.info("Writing onnx file")
|
||||
model.export(onnx_file, onnx_opset_version=12)
|
||||
logging.info("succesfully ported onnx file")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = get_parser().parse_args()
|
||||
main(
|
||||
args.nemo_file, args.onnx_file, model_type=args.model_type,
|
||||
)
|
||||
@@ -0,0 +1,115 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#!/bin/bash
|
||||
|
||||
# This is bash script actually run the downloading and resampling script.
|
||||
# See instructions in freesound_download.py
|
||||
|
||||
# Change this arguments if you want
|
||||
page_size=100 # Number of sounds per page
|
||||
max_samples=200 # Maximum number of sound samples
|
||||
min_filesize=0 # Minimum filesize allowed (in MB)
|
||||
max_filesize=100 # Maximum filesize allowed (in MB)
|
||||
|
||||
if [[ $# -ne 3 ]]; then
|
||||
echo "Require number of all files | data directory | resample data directory as arguments to the script"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
|
||||
NUM_ALL_FILES=$1
|
||||
DATADIR=$2
|
||||
RESAMPLE_DATADIR=$3
|
||||
|
||||
|
||||
if [ ! -d "$DATADIR" ]; then
|
||||
echo "Creating dir $DATADIR"
|
||||
mkdir -p "$DATADIR"
|
||||
fi
|
||||
|
||||
if [ ! -d "$RESAMPLE_DATADIR" ]; then
|
||||
echo "Creating dir $RESAMPLE_DATADIR"
|
||||
mkdir -p "$RESAMPLE_DATADIR"
|
||||
fi
|
||||
|
||||
# we just need background categories for constructing dataset, feel free to include other (speech) categories for testing and training your VAD model
|
||||
# background
|
||||
categories=(
|
||||
"Air brake"
|
||||
"Static"
|
||||
"Acoustic environment"
|
||||
"Distortion"
|
||||
"Tape hiss"
|
||||
"Hubbub"
|
||||
"Vibration"
|
||||
"Cacophony"
|
||||
"Throbbing"
|
||||
"Reverberation"
|
||||
"Inside, public space"
|
||||
"Inside, small room"
|
||||
"Echo"
|
||||
"Outside, rural"
|
||||
"Outside, natural"
|
||||
"Outside, urban"
|
||||
"Outside, manmade"
|
||||
"Car"
|
||||
"Bus"
|
||||
"Traffic noise"
|
||||
"Roadway noise"
|
||||
"Truck"
|
||||
"Emergency vehicle"
|
||||
"Motorcycle"
|
||||
"Aircraft engine"
|
||||
"Aircraft"
|
||||
"Helicopter"
|
||||
"Bicycle"
|
||||
"Skateboard"
|
||||
"Subway, metro, underground"
|
||||
"Railroad car"
|
||||
"Train wagon"
|
||||
"Train"
|
||||
"Sailboat"
|
||||
"Rowboat"
|
||||
"Ship"
|
||||
)
|
||||
|
||||
|
||||
WAV_FILECOUNT="$(find $DATADIR -name '*.wav' -type f | wc -l)"
|
||||
FLAC_FILECOUNT="$(find $DATADIR -name '*.flac' -type f | wc -l)"
|
||||
FILECOUNT="$((WAV_FILECOUNT + FLAC_FILECOUNT))"
|
||||
echo "File count: " $FILECOUNT
|
||||
|
||||
|
||||
while((FILECOUNT <= NUM_ALL_FILES))
|
||||
do
|
||||
for category in "${categories[@]}"
|
||||
do
|
||||
python freesound_download.py --data_dir "${DATADIR}" --category "${category}" --page_size "${page_size}" --max_samples "${max_samples}" --min_filesize "${min_filesize}" --max_filesize "${max_filesize}"
|
||||
ret=$?
|
||||
if [ $ret -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
WAV_FILECOUNT="$(find $DATADIR -name '*.wav' -type f | wc -l)"
|
||||
FLAC_FILECOUNT="$(find $DATADIR -name '*.flac' -type f | wc -l)"
|
||||
FILECOUNT="$((WAV_FILECOUNT + FLAC_FILECOUNT))"
|
||||
echo "Current file count is: " $FILECOUNT
|
||||
done
|
||||
|
||||
# RESAMPLE
|
||||
echo "Got enough data. Start resample!"
|
||||
python freesound_resample.py --data_dir="${DATADIR}" --resampled_dir="${RESAMPLE_DATADIR}"
|
||||
|
||||
echo "Done resample data!"
|
||||
@@ -0,0 +1,585 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import argparse
|
||||
import os
|
||||
import pickle
|
||||
import time
|
||||
|
||||
try:
|
||||
import librosa
|
||||
import requests
|
||||
import requests_oauthlib
|
||||
from joblib import Parallel, delayed
|
||||
from oauthlib.oauth2 import TokenExpiredError
|
||||
except (ModuleNotFoundError, ImportError) as e:
|
||||
raise e
|
||||
|
||||
try:
|
||||
import freesound
|
||||
except ModuleNotFoundError as e:
|
||||
raise ModuleNotFoundError(
|
||||
"freesound is not installed. Execute `pip install --no-cache-dir git+https://github.com/MTG/freesound-python.git` in terminal"
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
Instructions
|
||||
1. We will need some requirements including freesound, requests, requests_oauthlib, joblib, librosa and sox. If they are not installed, please run `pip install -r freesound_requirements.txt`
|
||||
2. Create an API key for freesound.org at https://freesound.org/help/developers/
|
||||
3. Create a python file called `freesound_private_apikey.py` and add lined `api_key = <your Freesound api key>` and `client_id = <your Freesound client id>`
|
||||
4. Authorize by run `python freesound_download.py --authorize` and visit website, and paste response code
|
||||
5. Feel free to change any arguments in download_resample_freesound.sh such as max_samples and max_filesize
|
||||
6. Run `bash download_resample_freesound.sh <numbers of files you want> <download data directory> <resampled data directory>`
|
||||
"""
|
||||
|
||||
# Import the API Key
|
||||
try:
|
||||
from freesound_private_apikey import api_key, client_id
|
||||
|
||||
print("API Key found !")
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Create a python file called `freesound_private_apikey.py` and add lined `api_key = <your Freesound api key>` and `client_id = <your Freesound client id>`"
|
||||
)
|
||||
|
||||
auth_url = 'https://freesound.org/apiv2/oauth2/authorize/'
|
||||
redirect_url = 'https://freesound.org/home/app_permissions/permission_granted/'
|
||||
token_url = 'https://freesound.org/apiv2/oauth2/access_token/'
|
||||
scope = ["read", "write"]
|
||||
|
||||
BACKGROUND_CLASSES = [
|
||||
"Air brake",
|
||||
"Static",
|
||||
"Acoustic environment",
|
||||
"Distortion",
|
||||
"Tape hiss",
|
||||
"Hubbub",
|
||||
"Vibration",
|
||||
"Cacophony",
|
||||
"Throbbing",
|
||||
"Reverberation",
|
||||
"Inside, public space",
|
||||
"Inside, small room",
|
||||
"Echo",
|
||||
"Outside, rural",
|
||||
"Outside, natural",
|
||||
"Outside, urban",
|
||||
"Outside, manmade",
|
||||
"Car",
|
||||
"Bus",
|
||||
"Traffic noise",
|
||||
"Roadway noise",
|
||||
"Truck",
|
||||
"Emergency vehicle",
|
||||
"Motorcycle",
|
||||
"Aircraft engine",
|
||||
"Aircraft",
|
||||
"Helicopter",
|
||||
"Bicycle",
|
||||
"Skateboard",
|
||||
"Subway, metro, underground",
|
||||
"Railroad car",
|
||||
"Train wagon",
|
||||
"Train",
|
||||
"Sailboat",
|
||||
"Rowboat",
|
||||
"Ship",
|
||||
]
|
||||
|
||||
SPEECH_CLASSES = [
|
||||
"Male speech",
|
||||
"Female speech",
|
||||
"Speech synthesizer",
|
||||
"Babbling",
|
||||
"Conversation",
|
||||
"Child speech",
|
||||
"Narration",
|
||||
"Laughter",
|
||||
"Yawn",
|
||||
"Whispering",
|
||||
"Whimper",
|
||||
"Baby cry",
|
||||
"Sigh",
|
||||
"Groan",
|
||||
"Humming",
|
||||
"Male singing",
|
||||
"Female singing",
|
||||
"Child singing",
|
||||
"Children shouting",
|
||||
]
|
||||
|
||||
|
||||
def initialize_oauth():
|
||||
# If token already exists, then just load it
|
||||
if os.path.exists('_token.pkl'):
|
||||
token = unpickle_object('_token')
|
||||
oauth = requests_oauthlib.OAuth2Session(client_id, redirect_uri=redirect_url, scope=scope, token=token)
|
||||
|
||||
else:
|
||||
# Construct a new token after OAuth2 flow
|
||||
# Initialize a OAuth2 session
|
||||
oauth = requests_oauthlib.OAuth2Session(client_id, redirect_uri=redirect_url, scope=scope)
|
||||
|
||||
authorization_url, state = oauth.authorization_url(auth_url)
|
||||
print(f"Visit below website and paste access token below : \n\n{authorization_url}\n")
|
||||
|
||||
authorization_response = input("Paste authorization response code here :\n")
|
||||
|
||||
token = oauth.fetch_token(
|
||||
token_url,
|
||||
authorization_response=authorization_response,
|
||||
code=authorization_response,
|
||||
client_secret=api_key,
|
||||
)
|
||||
|
||||
# Save the token generated
|
||||
pickle_object(token, '_token')
|
||||
|
||||
return oauth, token
|
||||
|
||||
|
||||
def instantiate_session():
|
||||
# Reconstruct session in process, and force singular execution thread to reduce session
|
||||
# connections to server
|
||||
token = unpickle_object('_token')
|
||||
session = requests_oauthlib.OAuth2Session(client_id, redirect_uri=redirect_url, scope=scope, token=token)
|
||||
adapter = requests.adapters.HTTPAdapter(pool_connections=1, pool_maxsize=1)
|
||||
session.mount('http://', adapter)
|
||||
return session
|
||||
|
||||
|
||||
def refresh_token(session):
|
||||
print("Refreshing tokens...")
|
||||
# Token expired, perform token refresh
|
||||
extras = {'client_id': client_id, 'client_secret': api_key}
|
||||
token = session.refresh_token(token_url, **extras)
|
||||
print("Token refresh performed...")
|
||||
# Save the refreshed token
|
||||
pickle_object(token, '_token')
|
||||
return session
|
||||
|
||||
|
||||
def pickle_object(token, name):
|
||||
with open(name + '.pkl', 'wb') as f:
|
||||
pickle.dump(token, f)
|
||||
|
||||
|
||||
def unpickle_object(name):
|
||||
fp = name + '.pkl'
|
||||
if os.path.exists(fp):
|
||||
with open(fp, 'rb') as f:
|
||||
token = pickle.load(f)
|
||||
|
||||
return token
|
||||
else:
|
||||
raise FileNotFoundError('Token not found!')
|
||||
|
||||
|
||||
def is_resource_limited(e: freesound.FreesoundException):
|
||||
"""
|
||||
Test if the reason for a freesound exception was either rate limit
|
||||
or daily limit.
|
||||
|
||||
If it was for either reason, sleep for an appropriate delay and return
|
||||
to try again.
|
||||
|
||||
Args:
|
||||
e: Freesound Exception object
|
||||
|
||||
Returns:
|
||||
A boolean which describes whether the error was due to some
|
||||
api limit issue, or if it was some other reason.
|
||||
|
||||
If false is returned, then the user should carefully check the cause
|
||||
and log it.
|
||||
"""
|
||||
detail = e.detail['detail']
|
||||
|
||||
if '2000' in detail:
|
||||
# This is the request limit, hold off for 1 hour and try again
|
||||
print(f"Hit daily limit, sleeping for 20 minutes.")
|
||||
time.sleep(60 * 20)
|
||||
return True
|
||||
|
||||
elif '60' in detail:
|
||||
# This is the request limit per minute, hold off for 1 minute and try again
|
||||
print(f"Hit rate limit, sleeping for 1 minute.")
|
||||
time.sleep(60)
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def prepare_client(client: freesound.FreesoundClient, token) -> freesound.FreesoundClient:
|
||||
# Initialize the client with token auth
|
||||
client.set_token(token['access_token'], auth_type='oauth')
|
||||
print("Client ready !")
|
||||
return client
|
||||
|
||||
|
||||
def get_text_query_with_resource_limit_checks(client, query: str, filters: list, fields: str, page_size: int):
|
||||
"""
|
||||
Performs a text query, checks for rate / api limits, and retries.
|
||||
|
||||
Args:
|
||||
client: FreesoundAPI client
|
||||
query: query string (either exact or inexact)
|
||||
filters: list of string filters
|
||||
fields: String of values to recover
|
||||
page_size: samples per page returned
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
pages = None
|
||||
attempts = 20
|
||||
|
||||
while pages is None:
|
||||
try:
|
||||
pages = client.text_search(query=query, filter=" ".join(filters), fields=fields, page_size=str(page_size),)
|
||||
|
||||
except freesound.FreesoundException as e:
|
||||
# Most probably a rate limit or a request limit
|
||||
# Check if that was the case, and wait appropriate ammount of time
|
||||
# for retry
|
||||
was_resource_limited = is_resource_limited(e)
|
||||
|
||||
# If result of test False, it means that failure was due to some other reason.
|
||||
# Log it, then break loop
|
||||
if not was_resource_limited:
|
||||
print(e.with_traceback(None))
|
||||
break
|
||||
|
||||
attempts -= 1
|
||||
|
||||
# Attempt to refresh tokens if it fails multiple times
|
||||
if attempts % 5 == 0 and attempts > 0:
|
||||
session = instantiate_session()
|
||||
refresh_token(session)
|
||||
session.close()
|
||||
token = unpickle_object('_token')
|
||||
client = prepare_client(client, token)
|
||||
|
||||
if attempts <= 0:
|
||||
print(f"Failed to query pages for '{query}' after 10 attempts, skipping query")
|
||||
break
|
||||
|
||||
if pages is None:
|
||||
print(f"Query attempts remaining = {attempts}")
|
||||
|
||||
return client, pages
|
||||
|
||||
|
||||
def get_resource_with_auto_refresh(session, download_url):
|
||||
"""
|
||||
Attempts download of audio with a token refresh if necessary.
|
||||
"""
|
||||
try:
|
||||
result = session.get(download_url)
|
||||
|
||||
except TokenExpiredError as e:
|
||||
session = refresh_token(session)
|
||||
result = session.get(download_url)
|
||||
|
||||
except Exception as e:
|
||||
result = None
|
||||
|
||||
print(f"Skipping file {download_url} due to exception below\n\n")
|
||||
print(e)
|
||||
|
||||
return result.content
|
||||
|
||||
|
||||
def download_song(basepath, id, name, download_url):
|
||||
# Cleanup name
|
||||
name = name.replace(":", "-")
|
||||
name = name.replace(",", "-")
|
||||
name = name.replace("/", "-")
|
||||
name = name.replace("\\", "-")
|
||||
name = name.replace(".", "-")
|
||||
name = name.replace(" ", "")
|
||||
|
||||
# Correct last `.` for filetype
|
||||
name = name[:-4] + '.wav'
|
||||
|
||||
# Add file id to filename
|
||||
name = f"id_{id} " + name
|
||||
|
||||
fp = os.path.join(basepath, name)
|
||||
|
||||
# Check if file, if exists already, can be loaded by librosa
|
||||
# If it cannot be loaded, possibly corrupted file.
|
||||
# Delete and then re-download
|
||||
if os.path.exists(fp):
|
||||
try:
|
||||
_ = librosa.load(fp)
|
||||
except Exception:
|
||||
# File is currupted, delete and re-download.
|
||||
os.remove(fp)
|
||||
|
||||
print(f"Pre-existing file {fp} was corrupt and was deleted, will be re-downloaded.")
|
||||
|
||||
if not os.path.exists(fp):
|
||||
print("Downloading file :", name)
|
||||
|
||||
session = instantiate_session()
|
||||
|
||||
data = None
|
||||
attempts = 10
|
||||
|
||||
try:
|
||||
while data is None:
|
||||
|
||||
try:
|
||||
# Get the sound data
|
||||
data = get_resource_with_auto_refresh(session, download_url)
|
||||
|
||||
except freesound.FreesoundException as e:
|
||||
# Most probably a rate limit or a request limit
|
||||
# Check if that was the case, and wait appropriate amount of time
|
||||
# for retry
|
||||
was_resource_limited = is_resource_limited(e)
|
||||
|
||||
# If result of test False, it means that failure was due to some other reason.
|
||||
# Log it, then break loop
|
||||
if not was_resource_limited:
|
||||
print(e)
|
||||
break
|
||||
|
||||
attempts -= 1
|
||||
|
||||
if attempts <= 0:
|
||||
print(f"Failed to download file {fp} after 10 attempts, skipping file")
|
||||
break
|
||||
|
||||
if data is None:
|
||||
print(f"Download attempts remaining = {attempts}")
|
||||
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# Write the data to file
|
||||
if data is not None:
|
||||
print("Downloaded file :", name)
|
||||
|
||||
with open(fp, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
# If file size is less than 89, then this probably is a text format and not an actual audio file.
|
||||
if os.path.getsize(fp) > 89:
|
||||
print(f"File written : {fp}")
|
||||
|
||||
else:
|
||||
os.remove(fp)
|
||||
print(f"File corrupted and has been deleted: {fp}")
|
||||
|
||||
else:
|
||||
print(f"File [{fp}] corrupted or faced some issue when downloading, skipped.")
|
||||
|
||||
# Sleep to avoid hitting rate limits
|
||||
time.sleep(5)
|
||||
|
||||
else:
|
||||
print(f"File [{fp}] already exists in dataset, skipping re-download.")
|
||||
|
||||
|
||||
def get_songs_by_category(
|
||||
client: freesound.FreesoundClient,
|
||||
category: str,
|
||||
data_dir: str,
|
||||
max_num_samples=100,
|
||||
page_size=100,
|
||||
min_filesize_in_mb=0,
|
||||
max_filesize_in_mb=10,
|
||||
n_jobs=None,
|
||||
):
|
||||
"""
|
||||
Download songs of a category with restrictions
|
||||
|
||||
Args:
|
||||
client: FreesoundAPI client
|
||||
category: category to be downloaded
|
||||
data_dir: directory of downloaded songs
|
||||
max_num_samples: maximum number of samples of this category
|
||||
page_size: samples per page returned
|
||||
min_filesize_in_mb: minimum filesize of the song in MB
|
||||
max_filesize_in_mb: maximum filesize of the song in MB
|
||||
n_jobs: number of jobs for parallel processing
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# quote string to force exact match
|
||||
query = f'"{category}"'
|
||||
print(f"Query : {query}")
|
||||
|
||||
page_size = min(page_size, 150)
|
||||
max_filesize = int(max_filesize_in_mb * (2 ** 20))
|
||||
|
||||
if min_filesize_in_mb == 0:
|
||||
min_filesize_in_mb = 1
|
||||
else:
|
||||
min_filesize_in_mb = int(min_filesize_in_mb * (2 ** 20))
|
||||
|
||||
if max_num_samples < 0:
|
||||
max_num_samples = int(1e6)
|
||||
|
||||
filters = [
|
||||
'type:(wav OR flac)',
|
||||
'license:("Attribution" OR "Creative Commons 0")',
|
||||
f'filesize:[{min_filesize_in_mb} TO {max_filesize}]',
|
||||
]
|
||||
|
||||
fields = "id,name,download,license"
|
||||
|
||||
client, pages = get_text_query_with_resource_limit_checks(
|
||||
client, query=query, filters=filters, fields=fields, page_size=page_size
|
||||
)
|
||||
|
||||
if pages is None:
|
||||
print(f"Number of attempts exceeded limit, skipping query {query}")
|
||||
return
|
||||
|
||||
num_pages = pages.count
|
||||
|
||||
# Check if returned empty result; if so, fallback to inexact category search
|
||||
if num_pages == 0:
|
||||
print(f"Found 0 samples of results for query '{query}'")
|
||||
print(f"Trying less restricted query : {category}")
|
||||
|
||||
client, pages = get_text_query_with_resource_limit_checks(
|
||||
client, query=category, filters=filters, fields=fields, page_size=page_size
|
||||
)
|
||||
|
||||
if pages is None:
|
||||
print(f"Number of attempts exceeded limit, skipping query {query}")
|
||||
return
|
||||
|
||||
num_pages = pages.count
|
||||
|
||||
print(f"Found {num_pages} samples of results for query '{query}'")
|
||||
|
||||
category = category.replace(' ', '_')
|
||||
basepath = os.path.join(data_dir, category)
|
||||
|
||||
if not os.path.exists(basepath):
|
||||
os.makedirs(basepath)
|
||||
|
||||
sounds = []
|
||||
sample_count = 0
|
||||
|
||||
# Retrieve sound license information
|
||||
with open(os.path.join(basepath, 'licenses.txt'), 'w') as f:
|
||||
f.write("ID,LICENSE\n")
|
||||
f.flush()
|
||||
|
||||
while True:
|
||||
for sound in pages:
|
||||
if sample_count >= max_num_samples:
|
||||
print(
|
||||
f"Collected {sample_count} samples, which is >= max number of samples requested "
|
||||
f"{max_num_samples}. Stopping for this category : {category}"
|
||||
)
|
||||
break
|
||||
|
||||
sounds.append(sound)
|
||||
sample_count += 1
|
||||
|
||||
f.write(f"{sound.id},{sound.license}\n")
|
||||
f.flush()
|
||||
|
||||
if sample_count >= max_num_samples:
|
||||
break
|
||||
|
||||
try:
|
||||
pages = pages.next_page()
|
||||
except ValueError:
|
||||
break
|
||||
|
||||
if n_jobs is None:
|
||||
n_jobs = max(1, len(sounds))
|
||||
|
||||
# Parallel download all songs
|
||||
with Parallel(n_jobs=n_jobs, verbose=10) as parallel:
|
||||
_ = parallel(delayed(download_song)(basepath, sound.id, sound.name, sound.download) for sound in sounds)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = argparse.ArgumentParser(description="Freesound download script")
|
||||
|
||||
parser.add_argument(
|
||||
'--authorize', action='store_true', dest='auth', help='Flag to only perform OAuth2 authorization step'
|
||||
)
|
||||
|
||||
parser.add_argument('-c', '--category', default='', type=str, help='Category required to download')
|
||||
|
||||
parser.add_argument('-d', '--data_dir', default='', type=str, help='Destination folder to store data')
|
||||
|
||||
parser.add_argument('--page_size', default=100, type=int, help='Number of sounds per page')
|
||||
|
||||
parser.add_argument('--max_samples', default=100, type=int, help='Maximum number of sound samples')
|
||||
|
||||
parser.add_argument('--min_filesize', default=0, type=int, help='Maximum filesize allowed (in MB)')
|
||||
|
||||
parser.add_argument('--max_filesize', default=20, type=int, help='Maximum filesize allowed (in MB)')
|
||||
|
||||
parser.set_defaults(auth=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.auth:
|
||||
""" Initialize oauth token to be used by all """
|
||||
oauth, token = initialize_oauth()
|
||||
oauth.close()
|
||||
|
||||
print("Authentication suceeded ! Token stored in `_token.pkl`")
|
||||
exit(0)
|
||||
|
||||
if not os.path.exists('_token.pkl'):
|
||||
raise FileNotFoundError(
|
||||
"Please authorize the application first using " "`python freesound_download.py --authorize`"
|
||||
)
|
||||
if args.data_dir == '':
|
||||
raise ValueError("Data dir must be passed as an argument using `--data_dir`")
|
||||
|
||||
data_dir = args.data_dir
|
||||
|
||||
page_size = args.page_size
|
||||
max_num_samples = args.max_samples
|
||||
min_filesize_in_mb = args.min_filesize
|
||||
max_filesize_in_mb = args.max_filesize
|
||||
|
||||
# Initialize and authenticate client
|
||||
token = unpickle_object('_token')
|
||||
freesound_client = freesound.FreesoundClient()
|
||||
client = prepare_client(freesound_client, token)
|
||||
|
||||
category = args.category
|
||||
|
||||
if category == '':
|
||||
raise ValueError("Cannot pass empty string as it will select all of FreeSound data !")
|
||||
|
||||
print(f"Downloading category : {category}")
|
||||
get_songs_by_category(
|
||||
client,
|
||||
category,
|
||||
data_dir=data_dir,
|
||||
max_num_samples=max_num_samples,
|
||||
page_size=page_size,
|
||||
min_filesize_in_mb=min_filesize_in_mb,
|
||||
max_filesize_in_mb=max_filesize_in_mb,
|
||||
n_jobs=30,
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
git+git://github.com/MTG/freesound-python.git
|
||||
requests
|
||||
requests_oauthlib
|
||||
joblib
|
||||
librosa
|
||||
sox
|
||||
@@ -0,0 +1,119 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import time
|
||||
|
||||
import librosa
|
||||
import sox
|
||||
from joblib import Parallel, delayed
|
||||
|
||||
|
||||
def resample_file(resampled_dir, filepath, ext, sample_rate):
|
||||
"""
|
||||
Resample an audio file to 16kHZ and transform to monochannel
|
||||
Remove incompatible files.
|
||||
|
||||
Args:
|
||||
resampled_dir: Directory of transformed files.
|
||||
filepath: Filepath of Audio
|
||||
ext: File type e.g. "wav", "flac"
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
head, filename = os.path.split(filepath)
|
||||
_, clsname = os.path.split(head)
|
||||
|
||||
filename, _ = os.path.splitext(filename)
|
||||
|
||||
new_dir = os.path.join(resampled_dir, clsname)
|
||||
if not os.path.exists(new_dir):
|
||||
os.makedirs(new_dir)
|
||||
|
||||
new_path = os.path.join(new_dir, filename + f'.{ext}')
|
||||
|
||||
# check if the resampled data exists.
|
||||
if os.path.exists(new_path):
|
||||
print(f"Resampled file {filepath} exists. Skip it.")
|
||||
return None
|
||||
|
||||
transform = sox.Transformer()
|
||||
transform.set_output_format(file_type='wav')
|
||||
transform.convert(samplerate=sample_rate, n_channels=1)
|
||||
|
||||
try:
|
||||
transform.build(filepath, new_path)
|
||||
print(f"Finished converting file {filepath}.")
|
||||
|
||||
return None
|
||||
|
||||
except sox.core.SoxError as e:
|
||||
|
||||
try:
|
||||
# Check if the file is readable
|
||||
librosa.load(filepath)
|
||||
|
||||
# if it is, force input format and try again
|
||||
transform.set_input_format(file_type=ext)
|
||||
transform.build(filepath, new_path)
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
return filepath
|
||||
|
||||
|
||||
def main():
|
||||
start = time.time()
|
||||
parser = argparse.ArgumentParser(description='Freesound data resample')
|
||||
parser.add_argument("--data_dir", required=True, default=None, type=str)
|
||||
parser.add_argument('--resampled_dir', required=True, default=None, type=str)
|
||||
parser.add_argument('--sample_rate', default=16000, type=int)
|
||||
args = parser.parse_args()
|
||||
|
||||
data_dir = args.data_dir
|
||||
resampled_dir = args.resampled_dir
|
||||
sample_rate = args.sample_rate
|
||||
|
||||
wav_files = sorted(glob.glob(os.path.join(data_dir, '*/*.wav')))
|
||||
flac_files = sorted(glob.glob(os.path.join(data_dir, '*/*.flac')))
|
||||
|
||||
with Parallel(n_jobs=-1, verbose=10) as parallel:
|
||||
wav_files_failed = parallel(
|
||||
delayed(resample_file)(resampled_dir, filepath, ext='wav', sample_rate=sample_rate)
|
||||
for filepath in wav_files
|
||||
)
|
||||
|
||||
flac_files_failed = parallel(
|
||||
delayed(resample_file)(resampled_dir, filepath, ext='flac', sample_rate=sample_rate)
|
||||
for filepath in flac_files
|
||||
)
|
||||
|
||||
with open('dataset_conversion_logs.txt', 'w') as f:
|
||||
for file in wav_files_failed:
|
||||
if file is not None:
|
||||
f.write(f"{file}\n")
|
||||
|
||||
for file in flac_files_failed:
|
||||
if file is not None:
|
||||
f.write(f"{file}\n")
|
||||
|
||||
end = time.time()
|
||||
print(f'Resample data in {data_dir} and save to {resampled_dir} takes {end-start} seconds.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""This script converts old Jasper/QuartzNet models from NeMo 0.11.* to NeMo v1.0.0*
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
import torch
|
||||
from omegaconf import DictConfig
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
import nemo.collections.asr as nemo_asr
|
||||
from nemo.utils import logging
|
||||
|
||||
|
||||
def get_parser():
|
||||
parser = argparse.ArgumentParser(description="Converts old Jasper/QuartzNet models to NeMo v1.0beta")
|
||||
parser.add_argument("--config_path", default=None, required=True, help="Path to model config (NeMo v1.0beta)")
|
||||
parser.add_argument("--encoder_ckpt", default=None, required=True, help="Encoder checkpoint path")
|
||||
parser.add_argument("--decoder_ckpt", default=None, required=True, help="Decoder checkpoint path")
|
||||
parser.add_argument("--output_path", default=None, required=True, help="Output checkpoint path (should be .nemo)")
|
||||
parser.add_argument(
|
||||
"--model_type",
|
||||
default='asr',
|
||||
type=str,
|
||||
choices=['asr', 'speech_label', 'speaker'],
|
||||
help="Type of decoder used by the model.",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main(config_path, encoder_ckpt, decoder_ckpt, output_path, model_type):
|
||||
|
||||
yaml = YAML(typ='safe')
|
||||
with open(config_path) as f:
|
||||
params = yaml.load(f)
|
||||
|
||||
model = None
|
||||
if model_type == 'asr':
|
||||
logging.info("Creating ASR NeMo 1.0 model")
|
||||
model = nemo_asr.models.EncDecCTCModel(cfg=DictConfig(params['model']))
|
||||
elif model_type == 'speech_label':
|
||||
logging.info("Creating speech label NeMo 1.0 model")
|
||||
model = nemo_asr.models.EncDecClassificationModel(cfg=DictConfig(params['model']))
|
||||
else:
|
||||
logging.info("Creating Speaker Recognition NeMo 1.0 model")
|
||||
model = nemo_asr.models.EncDecSpeakerLabelModel(cfg=DictConfig(params['model']))
|
||||
|
||||
model.encoder.load_state_dict(torch.load(encoder_ckpt))
|
||||
model.decoder.load_state_dict(torch.load(decoder_ckpt))
|
||||
logging.info("Succesfully ported old checkpoint")
|
||||
|
||||
model.save_to(output_path)
|
||||
logging.info("new model saved at {}".format(output_path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = get_parser().parse_args()
|
||||
main(args.config_path, args.encoder_ckpt, args.decoder_ckpt, args.output_path, args.model_type)
|
||||
@@ -0,0 +1,403 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
import re
|
||||
import shutil
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
|
||||
import fasttext
|
||||
from tqdm import tqdm
|
||||
|
||||
"""
|
||||
Usage:
|
||||
python filter_by_language.py --input-src train.en \
|
||||
--input-tgt train.de \
|
||||
--output-src train_lang_filtered.en \
|
||||
--output-tgt train_lang_filtered.de \
|
||||
--source-lang en \
|
||||
--target-lang de \
|
||||
--removed-src train_garbage.en \
|
||||
--removed-tgt train_garbage.de \
|
||||
--fasttext-model lid.176.bin
|
||||
"""
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="It is a script for verifying language in machine translation data sets. If the script is used on "
|
||||
"a parallel corpus, it verifies both a source and a target language. If number of jobs `--num-jobs` is bigger "
|
||||
"than 1 than lines in an input file (or files if parallel corpus is checked) split equally between workers. "
|
||||
"If `num_jobs > 1` is used, the best performance is achieved if dataset is shuffled and lines with different "
|
||||
"lengths are distributed evenly in the input file. Filtered data is stored into `output_src`[, `--output-tgt`]"
|
||||
" and removed lines are put into `removed_src`[, `--removed-tgt`] files. If language cannot be detected "
|
||||
"(e.g. date), the line is removed. Working time on en-de wikimatrix (6.23M pairs: 700 MB German and 625 MB "
|
||||
"English) from wmt20 on machine with 20 CPU cores: less than 1 minute."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-src",
|
||||
"-s",
|
||||
help="Path to the input file which has to contain text in language `source_lang`.",
|
||||
required=True,
|
||||
type=Path,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-tgt",
|
||||
"-t",
|
||||
help="Path to the input file which has to contain text in language `target_lang`. If not provided, data is "
|
||||
"processed as monolingual.",
|
||||
type=Path,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-src",
|
||||
"-S",
|
||||
help="Path to the file where filtered `input_src` will be saved.",
|
||||
required=True,
|
||||
type=Path,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-tgt", "-T", help="Path to the output target file", type=Path,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--source-lang",
|
||||
"-l",
|
||||
required=True,
|
||||
help="Input language. For options see https://fasttext.cc/docs/en/language-identification.html.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-lang",
|
||||
"-L",
|
||||
help="Output language. For options see https://fasttext.cc/docs/en/language-identification.html.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--removed-src", "-r", required=True, help="Path to file where removed source lines will be saved", type=Path,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--removed-tgt", "-R", help="Path to file where removed target lines will be saved", type=Path,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-jobs",
|
||||
"-j",
|
||||
type=int,
|
||||
help="Number of jobs. By default, the number of jobs is equal to the number of CPU cores.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fasttext-model",
|
||||
"-m",
|
||||
help="Path to fasttext model. The description and download links are here "
|
||||
"https://fasttext.cc/docs/en/language-identification.html",
|
||||
type=Path,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if not (
|
||||
args.output_tgt is None
|
||||
and args.input_tgt is None
|
||||
and args.target_lang is None
|
||||
and args.removed_tgt is None
|
||||
or args.output_tgt is not None
|
||||
and args.input_tgt is not None
|
||||
and args.target_lang is not None
|
||||
and args.removed_tgt is not None
|
||||
):
|
||||
raise ValueError(
|
||||
f"Arguments `input_tgt`, `output_tgt`, `target_lang`, `removed_tgt` have to be either `None` "
|
||||
f"simultaneously or not `None` simultaneously. Given "
|
||||
f"input_tgt={args.input_tgt}, output_tgt={args.output_tgt}, target_lang={args.target_lang}, "
|
||||
f"removed_tgt={args.removed_tgt}"
|
||||
)
|
||||
args.input_src = args.input_src.expanduser()
|
||||
if args.input_tgt is not None:
|
||||
args.input_tgt = args.input_tgt.expanduser()
|
||||
args.output_src = args.output_src.expanduser()
|
||||
if args.output_tgt is not None:
|
||||
args.output_tgt = args.output_tgt.expanduser()
|
||||
args.removed_src = args.removed_src.expanduser()
|
||||
if args.removed_tgt is not None:
|
||||
args.removed_tgt = args.removed_tgt.expanduser()
|
||||
args.fasttext_model = args.fasttext_model.expanduser()
|
||||
return args
|
||||
|
||||
|
||||
def get_lang(line, fasttext_model):
|
||||
labels, _ = fasttext_model.predict(line, k=1)
|
||||
lang = labels[0].split('__')[-1]
|
||||
return lang
|
||||
|
||||
|
||||
def get_edges_in_1_file(fn, num_parts):
|
||||
num_lines = 0
|
||||
edges = [0]
|
||||
with open(fn) as f:
|
||||
i = 0
|
||||
for l in f:
|
||||
i += len(l.encode('utf-8'))
|
||||
edges.append(i)
|
||||
num_lines += 1
|
||||
return [edges[int(i * num_lines / num_parts)] for i in range(num_parts)] + [edges[-1]], num_lines
|
||||
|
||||
|
||||
def get_edges_and_num_lines(src_fn, tgt_fn, num_parts):
|
||||
src_edges, src_num_lines = get_edges_in_1_file(src_fn, num_parts)
|
||||
assert num_parts + 1 == len(src_edges)
|
||||
src_edges = [(src_edges[i], src_edges[i + 1]) for i in range(len(src_edges) - 1)]
|
||||
if tgt_fn is not None:
|
||||
tgt_edges, tgt_num_lines = get_edges_in_1_file(tgt_fn, num_parts)
|
||||
tgt_edges = [(tgt_edges[i], tgt_edges[i + 1]) for i in range(len(tgt_edges) - 1)]
|
||||
if tgt_num_lines != src_num_lines:
|
||||
raise ValueError(
|
||||
f"Source {repr(src_fn)} and target {repr(tgt_fn)} files have different number of lines "
|
||||
f"{src_num_lines} and {tgt_num_lines} correspondingly."
|
||||
)
|
||||
|
||||
else:
|
||||
tgt_edges = [None] * num_parts
|
||||
assert len(src_edges) == num_parts
|
||||
return src_edges, tgt_edges, src_num_lines
|
||||
|
||||
|
||||
def filter_pairs(
|
||||
src_edges,
|
||||
tgt_edges,
|
||||
input_src,
|
||||
input_tgt,
|
||||
filtered_dir_src,
|
||||
filtered_dir_tgt,
|
||||
removed_dir_src,
|
||||
removed_dir_tgt,
|
||||
source_lang,
|
||||
target_lang,
|
||||
fasttext_model,
|
||||
rank,
|
||||
):
|
||||
global counter
|
||||
fasttext_model = fasttext.load_model(str(fasttext_model))
|
||||
output_src = filtered_dir_src / Path(f"rank{rank}")
|
||||
output_src_removed = removed_dir_src / Path(f"rank{rank}")
|
||||
output_tgt = filtered_dir_tgt / Path(f"rank{rank}")
|
||||
output_tgt_removed = removed_dir_tgt / Path(f"rank{rank}")
|
||||
with open(input_src) as in_src, open(input_tgt) as in_tgt, open(output_src, 'w') as out_src, open(
|
||||
output_tgt, 'w'
|
||||
) as out_tgt, open(output_src_removed, 'w') as out_r_src, open(output_tgt_removed, 'w') as out_r_tgt:
|
||||
in_src.seek(src_edges[0])
|
||||
in_tgt.seek(tgt_edges[0])
|
||||
src_l, tgt_l, i = in_src.readline(), in_tgt.readline(), 0
|
||||
if in_src.tell() > src_edges[1] or in_tgt.tell() > tgt_edges[1]:
|
||||
return
|
||||
while src_l and tgt_l:
|
||||
with counter.get_lock():
|
||||
counter.value += 1
|
||||
src_l = src_l.strip()
|
||||
tgt_l = tgt_l.strip()
|
||||
src_lang = get_lang(src_l, fasttext_model)
|
||||
if src_lang is not None:
|
||||
tgt_lang = get_lang(tgt_l, fasttext_model)
|
||||
if src_lang is None or tgt_lang is None or src_lang != source_lang or tgt_lang != target_lang:
|
||||
out_r_src.write(src_l + '\n')
|
||||
out_r_tgt.write(tgt_l + '\n')
|
||||
else:
|
||||
out_src.write(src_l + '\n')
|
||||
out_tgt.write(tgt_l + '\n')
|
||||
if in_src.tell() >= src_edges[1]:
|
||||
if in_tgt.tell() < tgt_edges[1]:
|
||||
raise ValueError(
|
||||
f"Edges of target and source has to be reached simultaneously, whereas "
|
||||
f"in_src.tell()={in_src.tell()}, in_tgt.tell()={in_tgt.tell()}, "
|
||||
f"src_edges[1]={src_edges[1]}, tgt_edges[1]={tgt_edges[1]}."
|
||||
)
|
||||
break
|
||||
if in_tgt.tell() >= tgt_edges[1]:
|
||||
raise ValueError(
|
||||
f"Edges of target and source has to be reached simultaneously, whereas "
|
||||
f"in_src.tell()={in_src.tell()}, in_tgt.tell()={in_tgt.tell()}, "
|
||||
f"src_edges[1]={src_edges[1]}, tgt_edges[1]={tgt_edges[1]}."
|
||||
)
|
||||
src_l, tgt_l, i = in_src.readline(), in_tgt.readline(), i + 1
|
||||
with counter.get_lock():
|
||||
counter.value += 1
|
||||
|
||||
|
||||
def filter_singles(
|
||||
src_edges, input_src, filtered_dir_src, removed_dir_src, source_lang, fasttext_model, rank,
|
||||
):
|
||||
logging.debug("filter singles")
|
||||
global counter
|
||||
fasttext_model = fasttext.load_model(str(fasttext_model))
|
||||
output_src = filtered_dir_src / Path(f"rank{rank}")
|
||||
output_src_removed = removed_dir_src / Path(f"rank{rank}")
|
||||
with open(input_src) as in_f, open(output_src, 'w') as out_f, open(output_src_removed, 'w') as out_r_f:
|
||||
in_f.seek(src_edges[0])
|
||||
i, line = 0, in_f.readline()
|
||||
if in_f.tell() > src_edges[1]:
|
||||
return
|
||||
while line:
|
||||
with counter.get_lock():
|
||||
counter.value += 1
|
||||
line = line.strip()
|
||||
in_lang = get_lang(line, fasttext_model)
|
||||
if in_lang is None or in_lang != source_lang:
|
||||
out_r_f.write(line + '\n')
|
||||
else:
|
||||
out_f.write(line + '\n')
|
||||
if in_f.tell() >= src_edges[1]:
|
||||
break
|
||||
i, line = i + 1, in_f.readline()
|
||||
with counter.get_lock():
|
||||
counter.value += 1
|
||||
|
||||
|
||||
def filter_by_lang(args):
|
||||
(
|
||||
src_edges,
|
||||
tgt_edges,
|
||||
input_src,
|
||||
input_tgt,
|
||||
filtered_dir_src,
|
||||
filtered_dir_tgt,
|
||||
removed_dir_src,
|
||||
removed_dir_tgt,
|
||||
source_lang,
|
||||
target_lang,
|
||||
fasttext_model,
|
||||
rank,
|
||||
) = args
|
||||
logging.debug(f"filter by lang input_tgt: {input_tgt}")
|
||||
if input_tgt is None:
|
||||
if tgt_edges is not None:
|
||||
warnings.warn("If input target is not provided `tgt_edges` argument is expected to be `None`")
|
||||
filter_singles(
|
||||
src_edges, input_src, filtered_dir_src, removed_dir_src, source_lang, fasttext_model, rank,
|
||||
)
|
||||
else:
|
||||
filter_pairs(
|
||||
src_edges,
|
||||
tgt_edges,
|
||||
input_src,
|
||||
input_tgt,
|
||||
filtered_dir_src,
|
||||
filtered_dir_tgt,
|
||||
removed_dir_src,
|
||||
removed_dir_tgt,
|
||||
source_lang,
|
||||
target_lang,
|
||||
fasttext_model,
|
||||
rank,
|
||||
)
|
||||
|
||||
|
||||
def _cat_results(out_file, tmp_dir):
|
||||
file_name_pattern = re.compile(r"/rank([1-9][0-9]*)|0$")
|
||||
with out_file.open('w') as out_f:
|
||||
for f in sorted(tmp_dir.iterdir()):
|
||||
if not f.is_file():
|
||||
warnings.warn(f"Unexpected not file {f}")
|
||||
elif not file_name_pattern.search(str(f)):
|
||||
warnings.warn(f"Unexpected file {f}")
|
||||
else:
|
||||
with f.open('r') as in_f:
|
||||
for l in in_f:
|
||||
out_f.write(l)
|
||||
|
||||
|
||||
def cat_results(out_files, tmp_dirs):
|
||||
for o_f, t_d in zip(out_files, tmp_dirs):
|
||||
if o_f is None or t_d is None:
|
||||
if o_f is not None or t_d is not None:
|
||||
warnings.warn(
|
||||
f"Output file and tmp directory are expected to be `None` simultaneously whereas tmp directory "
|
||||
f"is {t_d} and output file is {o_f}."
|
||||
)
|
||||
else:
|
||||
_cat_results(o_f, t_d)
|
||||
|
||||
|
||||
counter = None
|
||||
|
||||
|
||||
def init(args):
|
||||
global counter
|
||||
counter = args
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
tmp_dir = Path("tmp")
|
||||
i = 0
|
||||
while tmp_dir.exists():
|
||||
tmp_dir = Path("tmp" + str(i))
|
||||
i += 1
|
||||
tmp_filtered = tmp_dir / Path("filtered")
|
||||
tmp_filtered_src = tmp_filtered / Path("src")
|
||||
tmp_filtered_src.mkdir(parents=True, exist_ok=True)
|
||||
if args.input_tgt is None:
|
||||
tmp_filtered_tgt = None
|
||||
else:
|
||||
tmp_filtered_tgt = tmp_filtered / Path("tgt")
|
||||
tmp_filtered_tgt.mkdir(parents=True, exist_ok=True)
|
||||
tmp_removed = tmp_dir / Path("removed")
|
||||
tmp_removed_src = tmp_removed / Path("src")
|
||||
tmp_removed_src.mkdir(parents=True, exist_ok=True)
|
||||
if args.input_tgt is None:
|
||||
tmp_removed_tgt = None
|
||||
else:
|
||||
tmp_removed_tgt = tmp_removed / Path("tgt")
|
||||
tmp_removed_tgt.mkdir(parents=True, exist_ok=True)
|
||||
num_jobs = mp.cpu_count() if args.num_jobs is None else args.num_jobs
|
||||
src_edges, tgt_edges, num_lines = get_edges_and_num_lines(args.input_src, args.input_tgt, num_jobs)
|
||||
global counter
|
||||
counter = mp.Value('i', 0)
|
||||
t = tqdm(total=num_lines, desc="processed lines / total number of lines")
|
||||
with mp.Pool(num_jobs, initializer=init, initargs=(counter,)) as pool:
|
||||
async_result = pool.map_async(
|
||||
filter_by_lang,
|
||||
[
|
||||
(
|
||||
se,
|
||||
te,
|
||||
args.input_src,
|
||||
args.input_tgt,
|
||||
tmp_filtered_src,
|
||||
tmp_filtered_tgt,
|
||||
tmp_removed_src,
|
||||
tmp_removed_tgt,
|
||||
args.source_lang,
|
||||
args.target_lang,
|
||||
args.fasttext_model,
|
||||
rank,
|
||||
)
|
||||
for rank, (se, te) in enumerate(zip(src_edges, tgt_edges))
|
||||
],
|
||||
)
|
||||
while not async_result.ready():
|
||||
t.update(counter.value)
|
||||
with counter.get_lock():
|
||||
counter.value = 0
|
||||
sleep(0.1)
|
||||
t.update(counter.value)
|
||||
|
||||
cat_results(
|
||||
[args.output_src, args.output_tgt, args.removed_src, args.removed_tgt],
|
||||
[tmp_filtered_src, tmp_filtered_tgt, tmp_removed_src, tmp_removed_tgt],
|
||||
)
|
||||
shutil.rmtree(tmp_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
|
||||
from nemo.collections.asr.parts.utils.speaker_utils import write_rttm2manifest
|
||||
from nemo.utils import logging
|
||||
|
||||
"""
|
||||
This file converts vad outputs to manifest file for speaker diarization purposes
|
||||
present in vad output directory.
|
||||
every vad line consists of start_time, end_time , speech/non-speech
|
||||
"""
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--paths2rttm_files", help="path to vad output rttm-like files. Could be a list or a text file", required=True
|
||||
)
|
||||
parser.add_argument(
|
||||
"--paths2audio_files",
|
||||
help="path to audio files that vad was computed. Could be a list or a text file",
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument("--manifest_file", help="output manifest file name", type=str, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
write_rttm2manifest(args.paths2audio_files, args.paths2rttm_files, args.manifest_file)
|
||||
logging.info("wrote {} file from vad output files present in {}".format(args.manifest_file, args.paths2rttm_files))
|
||||
@@ -0,0 +1,96 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import librosa as l
|
||||
from sklearn.model_selection import StratifiedShuffleSplit
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
"""
|
||||
This scipt converts a scp file where each line contains
|
||||
<absolute path of wav file>
|
||||
to a manifest json file.
|
||||
Args:
|
||||
--scp: scp file name
|
||||
--id: index of speaker label in filename present in scp file that is separated by '/'
|
||||
--out: output manifest file name
|
||||
--split: True / False if you would want to split the manifest file for training purposes
|
||||
you may not need this for test set. output file names is <out>_<train/dev>.json
|
||||
Defaults to False
|
||||
"""
|
||||
|
||||
|
||||
def write_file(name, lines, idx):
|
||||
with open(name, 'w') as fout:
|
||||
for i in idx:
|
||||
dic = lines[i]
|
||||
json.dump(dic, fout)
|
||||
fout.write('\n')
|
||||
logging.info("wrote", name)
|
||||
|
||||
|
||||
def main(scp, id, out, split=False):
|
||||
if os.path.exists(out):
|
||||
os.remove(out)
|
||||
scp_file = open(scp, 'r').readlines()
|
||||
|
||||
lines = []
|
||||
speakers = []
|
||||
with open(out, 'w') as outfile:
|
||||
for line in tqdm(scp_file):
|
||||
line = line.strip()
|
||||
y, sr = l.load(line, sr=None)
|
||||
dur = l.get_duration(y=y, sr=sr)
|
||||
speaker = line.split('/')[id]
|
||||
speaker = list(speaker)
|
||||
speaker = ''.join(speaker)
|
||||
speakers.append(speaker)
|
||||
meta = {"audio_filepath": line, "offset": 0, "duration": float(dur), "label": speaker}
|
||||
lines.append(meta)
|
||||
json.dump(meta, outfile)
|
||||
outfile.write("\n")
|
||||
|
||||
path = os.path.dirname(out)
|
||||
if split:
|
||||
sss = StratifiedShuffleSplit(n_splits=1, test_size=0.1, random_state=42)
|
||||
for train_idx, test_idx in sss.split(speakers, speakers):
|
||||
logging.info(len(train_idx))
|
||||
|
||||
out = os.path.join(path, 'train.json')
|
||||
write_file(out, lines, train_idx)
|
||||
out = os.path.join(path, 'dev.json')
|
||||
write_file(out, lines, test_idx)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--scp", help="scp file name", type=str, required=True)
|
||||
parser.add_argument(
|
||||
"--id", help="field num seperated by '/' to be considered as speaker label", type=int, required=True
|
||||
)
|
||||
parser.add_argument("--out", help="manifest_file name", type=str, required=True)
|
||||
parser.add_argument(
|
||||
"--split",
|
||||
help="bool if you would want to split the manifest file for training purposes",
|
||||
required=False,
|
||||
action='store_true',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.scp, args.id, args.out, args.split)
|
||||
@@ -0,0 +1,601 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
# This script converts an existing audio dataset with a manifest to
|
||||
# a tarred and sharded audio dataset that can be read by the
|
||||
# TarredAudioToTextDataLayer.
|
||||
|
||||
# Please make sure your audio_filepath DOES NOT CONTAIN '-sub'!
|
||||
# Because we will use it to handle files which have duplicate filenames but with different offsets
|
||||
# (see function create_shard for details)
|
||||
|
||||
# Usage:
|
||||
1) Creating a new tarfile dataset
|
||||
|
||||
python convert_to_tarred_audio_dataset.py \
|
||||
--manifest_path=<path to the manifest file> \
|
||||
--target_dir=<path to output directory> \
|
||||
--num_shards=<number of tarfiles that will contain the audio>
|
||||
--max_duration=<float representing maximum duration of audio samples> \
|
||||
--min_duration=<float representing minimum duration of audio samples> \
|
||||
--shuffle --shuffle_seed=1
|
||||
|
||||
|
||||
2) Concatenating more tarfiles to a pre-existing tarred dataset
|
||||
|
||||
python convert_to_tarred_audio_dataset.py \
|
||||
--manifest_path=<path to the tarred manifest file> \
|
||||
--metadata_path=<path to the metadata.yaml (or metadata_version_{X}.yaml) file> \
|
||||
--target_dir=<path to output directory where the original tarfiles are contained> \
|
||||
--max_duration=<float representing maximum duration of audio samples> \
|
||||
--min_duration=<float representing minimum duration of audio samples> \
|
||||
--shuffle --shuffle_seed=1 \
|
||||
--concat_manifest_paths \
|
||||
<space seperated paths to 1 or more manifest files to concatenate into the original tarred dataset>
|
||||
|
||||
3) Writing an empty metadata file
|
||||
|
||||
python convert_to_tarred_audio_dataset.py \
|
||||
--target_dir=<path to output directory> \
|
||||
# any other optional argument
|
||||
--num_shards=8 \
|
||||
--max_duration=16.7 \
|
||||
--min_duration=0.01 \
|
||||
--shuffle \
|
||||
--shuffle_seed=1 \
|
||||
--write_metadata
|
||||
|
||||
"""
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import tarfile
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from joblib import Parallel, delayed
|
||||
from omegaconf import DictConfig, OmegaConf, open_dict
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert an existing ASR dataset to tarballs compatible with TarredAudioToTextDataLayer."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifest_path", default=None, type=str, required=False, help="Path to the existing dataset's manifest."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--concat_manifest_paths',
|
||||
nargs='+',
|
||||
default=None,
|
||||
type=str,
|
||||
required=False,
|
||||
help="Path to the additional dataset's manifests that will be concatenated with base dataset.",
|
||||
)
|
||||
|
||||
# Optional arguments
|
||||
parser.add_argument(
|
||||
"--target_dir",
|
||||
default='./tarred',
|
||||
type=str,
|
||||
help="Target directory for resulting tarballs and manifest. Defaults to `./tarred`. Creates the path if necessary.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--metadata_path", required=False, default=None, type=str, help="Path to metadata file for the dataset.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--num_shards",
|
||||
default=-1,
|
||||
type=int,
|
||||
help="Number of shards (tarballs) to create. Used for partitioning data among workers.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--max_duration',
|
||||
default=None,
|
||||
type=float,
|
||||
help='Maximum duration of audio clip in the dataset. By default, it is None and will not filter files.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--min_duration',
|
||||
default=None,
|
||||
type=float,
|
||||
help='Minimum duration of audio clip in the dataset. By default, it is None and will not filter files.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--shuffle",
|
||||
action='store_true',
|
||||
help="Whether or not to randomly shuffle the samples in the manifest before tarring/sharding.",
|
||||
)
|
||||
parser.add_argument("--shuffle_seed", type=int, help="Random seed for use if shuffling is enabled.")
|
||||
parser.add_argument(
|
||||
'--write_metadata',
|
||||
action='store_true',
|
||||
help=(
|
||||
"Flag to write a blank metadata with the current call config. "
|
||||
"Note that the metadata will not contain the number of shards, "
|
||||
"and it must be filled out by the user."
|
||||
),
|
||||
)
|
||||
parser.add_argument('--workers', type=int, default=1, help='Number of worker processes')
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ASRTarredDatasetConfig:
|
||||
num_shards: int = -1
|
||||
shuffle: bool = False
|
||||
max_duration: Optional[float] = None
|
||||
min_duration: Optional[float] = None
|
||||
shuffle_seed: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ASRTarredDatasetMetadata:
|
||||
created_datetime: Optional[str] = None
|
||||
version: int = 0
|
||||
num_samples_per_shard: Optional[int] = None
|
||||
is_concatenated_manifest: bool = False
|
||||
|
||||
dataset_config: Optional[ASRTarredDatasetConfig] = ASRTarredDatasetConfig()
|
||||
history: Optional[List[Any]] = field(default_factory=lambda: [])
|
||||
|
||||
def __post_init__(self):
|
||||
self.created_datetime = self.get_current_datetime()
|
||||
|
||||
def get_current_datetime(self):
|
||||
return datetime.now().strftime("%m-%d-%Y %H-%M-%S")
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: DictConfig):
|
||||
obj = cls()
|
||||
obj.__dict__.update(**config)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, filepath: str):
|
||||
config = OmegaConf.load(filepath)
|
||||
return ASRTarredDatasetMetadata.from_config(config=config)
|
||||
|
||||
|
||||
class ASRTarredDatasetBuilder:
|
||||
"""
|
||||
Helper class that constructs a tarred dataset from scratch, or concatenates tarred datasets
|
||||
together and constructs manifests for them.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = None
|
||||
|
||||
def configure(self, config: ASRTarredDatasetConfig):
|
||||
"""
|
||||
Sets the config generated from command line overrides.
|
||||
|
||||
Args:
|
||||
config: ASRTarredDatasetConfig dataclass object.
|
||||
"""
|
||||
self.config = config # type: ASRTarredDatasetConfig
|
||||
|
||||
if self.config.num_shards < 0:
|
||||
raise ValueError("`num_shards` must be > 0. Please fill in the metadata information correctly.")
|
||||
|
||||
def create_new_dataset(self, manifest_path: str, target_dir: str = "./tarred/", num_workers: int = 0):
|
||||
"""
|
||||
Creates a new tarred dataset from a given manifest file.
|
||||
|
||||
Args:
|
||||
manifest_path: Path to the original ASR manifest.
|
||||
target_dir: Output directory.
|
||||
num_workers: Integer denoting number of parallel worker processes which will write tarfiles.
|
||||
Defaults to 1 - which denotes sequential worker process.
|
||||
|
||||
Output:
|
||||
Writes tarfiles, along with the tarred dataset compatible manifest file.
|
||||
Also preserves a record of the metadata used to construct this tarred dataset.
|
||||
"""
|
||||
if self.config is None:
|
||||
raise ValueError("Config has not been set. Please call `configure(config: ASRTarredDatasetConfig)`")
|
||||
|
||||
if manifest_path is None:
|
||||
raise FileNotFoundError("Manifest filepath cannot be None !")
|
||||
|
||||
config = self.config # type: ASRTarredDatasetConfig
|
||||
|
||||
if not os.path.exists(target_dir):
|
||||
os.makedirs(target_dir)
|
||||
|
||||
# Read the existing manifest
|
||||
entries, filtered_entries, filtered_duration = self._read_manifest(manifest_path, config)
|
||||
|
||||
if len(filtered_entries) > 0:
|
||||
print(f"Filtered {len(filtered_entries)} files which amounts to {filtered_duration} seconds of audio.")
|
||||
|
||||
if config.shuffle:
|
||||
random.seed(config.shuffle_seed)
|
||||
print("Shuffling...")
|
||||
random.shuffle(entries)
|
||||
|
||||
# Create shards and updated manifest entries
|
||||
print(f"Number of samples added : {len(entries)}")
|
||||
print(f"Remainder: {len(entries) % config.num_shards}")
|
||||
|
||||
start_indices = []
|
||||
end_indices = []
|
||||
# Build indices
|
||||
for i in range(config.num_shards):
|
||||
start_idx = (len(entries) // config.num_shards) * i
|
||||
end_idx = start_idx + (len(entries) // config.num_shards)
|
||||
print(f"Shard {i} has entries {start_idx} ~ {end_idx}")
|
||||
if i == config.num_shards - 1:
|
||||
# We discard in order to have the same number of entries per shard.
|
||||
print(f"Have {len(entries) - end_idx} entries left over that will be discarded.")
|
||||
|
||||
start_indices.append(start_idx)
|
||||
end_indices.append(end_idx)
|
||||
|
||||
with Parallel(n_jobs=num_workers, verbose=config.num_shards) as parallel:
|
||||
# Call parallel tarfile construction
|
||||
new_entries_list = parallel(
|
||||
delayed(self._create_shard)(entries[start_idx:end_idx], target_dir, i)
|
||||
for i, (start_idx, end_idx) in enumerate(zip(start_indices, end_indices))
|
||||
)
|
||||
|
||||
# Flatten the list of list of entries to a list of entries
|
||||
new_entries = [sample for manifest in new_entries_list for sample in manifest]
|
||||
del new_entries_list
|
||||
|
||||
print("Total number of files in manifest :", len(new_entries))
|
||||
|
||||
# Write manifest
|
||||
new_manifest_path = os.path.join(target_dir, 'tarred_audio_manifest.json')
|
||||
with open(new_manifest_path, 'w') as m2:
|
||||
for entry in new_entries:
|
||||
json.dump(entry, m2)
|
||||
m2.write('\n')
|
||||
|
||||
# Write metadata (default metadata for new datasets)
|
||||
new_metadata_path = os.path.join(target_dir, 'metadata.yaml')
|
||||
metadata = ASRTarredDatasetMetadata()
|
||||
|
||||
# Update metadata
|
||||
metadata.dataset_config = config
|
||||
metadata.num_samples_per_shard = len(new_entries) // config.num_shards
|
||||
|
||||
# Write metadata
|
||||
metadata_yaml = OmegaConf.structured(metadata)
|
||||
OmegaConf.save(metadata_yaml, new_metadata_path, resolve=True)
|
||||
|
||||
def create_concatenated_dataset(
|
||||
self,
|
||||
base_manifest_path: str,
|
||||
manifest_paths: List[str],
|
||||
metadata: ASRTarredDatasetMetadata,
|
||||
target_dir: str = "./tarred_concatenated/",
|
||||
num_workers: int = 1,
|
||||
):
|
||||
"""
|
||||
Creates new tarfiles in order to create a concatenated dataset, whose manifest contains the data for
|
||||
both the original dataset as well as the new data submitted in manifest paths.
|
||||
|
||||
Args:
|
||||
base_manifest_path: Path to the manifest file which contains the information for the original
|
||||
tarred dataset (with flattened paths).
|
||||
manifest_paths: List of one or more paths to manifest files that will be concatenated with above
|
||||
base tarred dataset.
|
||||
metadata: ASRTarredDatasetMetadata dataclass instance with overrides from command line.
|
||||
target_dir: Output directory
|
||||
|
||||
Output:
|
||||
Writes tarfiles which with indices mapping to a "concatenated" tarred dataset,
|
||||
along with the tarred dataset compatible manifest file which includes information
|
||||
about all the datasets that comprise the concatenated dataset.
|
||||
|
||||
Also preserves a record of the metadata used to construct this tarred dataset.
|
||||
"""
|
||||
if not os.path.exists(target_dir):
|
||||
os.makedirs(target_dir)
|
||||
|
||||
if base_manifest_path is None:
|
||||
raise FileNotFoundError("Base manifest filepath cannot be None !")
|
||||
|
||||
if manifest_paths is None or len(manifest_paths) == 0:
|
||||
raise FileNotFoundError("List of additional manifest filepaths cannot be None !")
|
||||
|
||||
config = ASRTarredDatasetConfig(**(metadata.dataset_config))
|
||||
|
||||
# Read the existing manifest (no filtering here)
|
||||
base_entries, _, _ = self._read_manifest(base_manifest_path, config)
|
||||
print(f"Read base manifest containing {len(base_entries)} samples.")
|
||||
|
||||
# Precompute number of samples per shard
|
||||
if metadata.num_samples_per_shard is None:
|
||||
num_samples_per_shard = len(base_entries) // config.num_shards
|
||||
else:
|
||||
num_samples_per_shard = metadata.num_samples_per_shard
|
||||
|
||||
print("Number of samples per shard :", num_samples_per_shard)
|
||||
|
||||
# Compute min and max duration and update config (if no metadata passed)
|
||||
print(f"Selected max duration : {config.max_duration}")
|
||||
print(f"Selected min duration : {config.min_duration}")
|
||||
|
||||
entries = []
|
||||
for new_manifest_idx in range(len(manifest_paths)):
|
||||
new_entries, filtered_new_entries, filtered_duration = self._read_manifest(
|
||||
manifest_paths[new_manifest_idx], config
|
||||
)
|
||||
|
||||
if len(filtered_new_entries) > 0:
|
||||
print(
|
||||
f"Filtered {len(filtered_new_entries)} files which amounts to {filtered_duration:0.2f}"
|
||||
f" seconds of audio from manifest {manifest_paths[new_manifest_idx]}."
|
||||
)
|
||||
|
||||
entries.extend(new_entries)
|
||||
|
||||
if config.shuffle:
|
||||
random.seed(config.shuffle_seed)
|
||||
print("Shuffling...")
|
||||
random.shuffle(entries)
|
||||
|
||||
# Drop last section of samples that cannot be added onto a chunk
|
||||
drop_count = len(entries) % num_samples_per_shard
|
||||
total_new_entries = len(entries)
|
||||
entries = entries[:-drop_count]
|
||||
|
||||
print(
|
||||
f"Dropping {drop_count} samples from total new samples {total_new_entries} since they cannot "
|
||||
f"be added into a uniformly sized chunk."
|
||||
)
|
||||
|
||||
# Create shards and updated manifest entries
|
||||
num_added_shards = len(entries) // num_samples_per_shard
|
||||
|
||||
print(f"Number of samples in base dataset : {len(base_entries)}")
|
||||
print(f"Number of samples in additional datasets : {len(entries)}")
|
||||
print(f"Number of added shards : {num_added_shards}")
|
||||
print(f"Remainder: {len(entries) % num_samples_per_shard}")
|
||||
|
||||
start_indices = []
|
||||
end_indices = []
|
||||
shard_indices = []
|
||||
for i in range(num_added_shards):
|
||||
start_idx = (len(entries) // num_added_shards) * i
|
||||
end_idx = start_idx + (len(entries) // num_added_shards)
|
||||
shard_idx = i + config.num_shards
|
||||
print(f"Shard {shard_idx} has entries {start_idx + len(base_entries)} ~ {end_idx + len(base_entries)}")
|
||||
|
||||
start_indices.append(start_idx)
|
||||
end_indices.append(end_idx)
|
||||
shard_indices.append(shard_idx)
|
||||
|
||||
with Parallel(n_jobs=num_workers, verbose=num_added_shards) as parallel:
|
||||
# Call parallel tarfile construction
|
||||
new_entries_list = parallel(
|
||||
delayed(self._create_shard)(entries[start_idx:end_idx], target_dir, shard_idx)
|
||||
for i, (start_idx, end_idx, shard_idx) in enumerate(zip(start_indices, end_indices, shard_indices))
|
||||
)
|
||||
|
||||
# Flatten the list of list of entries to a list of entries
|
||||
new_entries = [sample for manifest in new_entries_list for sample in manifest]
|
||||
del new_entries_list
|
||||
|
||||
# Write manifest
|
||||
if metadata is None:
|
||||
new_version = 1 # start with `1`, where `0` indicates the base manifest + dataset
|
||||
else:
|
||||
new_version = metadata.version + 1
|
||||
|
||||
print("Total number of files in manifest :", len(base_entries) + len(new_entries))
|
||||
|
||||
new_manifest_path = os.path.join(target_dir, f'tarred_audio_manifest_version_{new_version}.json')
|
||||
with open(new_manifest_path, 'w') as m2:
|
||||
# First write all the entries of base manifest
|
||||
for entry in base_entries:
|
||||
json.dump(entry, m2)
|
||||
m2.write('\n')
|
||||
|
||||
# Finally write the new entries
|
||||
for entry in new_entries:
|
||||
json.dump(entry, m2)
|
||||
m2.write('\n')
|
||||
|
||||
# Preserve historical metadata
|
||||
base_metadata = metadata
|
||||
|
||||
# Write metadata (updated metadata for concatenated datasets)
|
||||
new_metadata_path = os.path.join(target_dir, f'metadata_version_{new_version}.yaml')
|
||||
metadata = ASRTarredDatasetMetadata()
|
||||
|
||||
# Update config
|
||||
config.num_shards = config.num_shards + num_added_shards
|
||||
|
||||
# Update metadata
|
||||
metadata.version = new_version
|
||||
metadata.dataset_config = config
|
||||
metadata.num_samples_per_shard = num_samples_per_shard
|
||||
metadata.is_concatenated_manifest = True
|
||||
metadata.created_datetime = metadata.get_current_datetime()
|
||||
|
||||
# Attach history
|
||||
current_metadata = OmegaConf.structured(base_metadata.history)
|
||||
metadata.history = current_metadata
|
||||
|
||||
# Write metadata
|
||||
metadata_yaml = OmegaConf.structured(metadata)
|
||||
OmegaConf.save(metadata_yaml, new_metadata_path, resolve=True)
|
||||
|
||||
def _read_manifest(self, manifest_path: str, config: ASRTarredDatasetConfig):
|
||||
"""Read and filters data from the manifest"""
|
||||
# Read the existing manifest
|
||||
entries = []
|
||||
filtered_entries = []
|
||||
filtered_duration = 0.0
|
||||
with open(manifest_path, 'r') as m:
|
||||
for line in m:
|
||||
entry = json.loads(line)
|
||||
if (config.max_duration is None or entry['duration'] < config.max_duration) and (
|
||||
config.min_duration is None or entry['duration'] > config.min_duration
|
||||
):
|
||||
entries.append(entry)
|
||||
else:
|
||||
filtered_entries.append(entry)
|
||||
filtered_duration += entry['duration']
|
||||
|
||||
return entries, filtered_entries, filtered_duration
|
||||
|
||||
def _create_shard(self, entries, target_dir, shard_id):
|
||||
"""Creates a tarball containing the audio files from `entries`.
|
||||
"""
|
||||
new_entries = []
|
||||
tar = tarfile.open(os.path.join(target_dir, f'audio_{shard_id}.tar'), mode='w', dereference=True)
|
||||
|
||||
count = dict()
|
||||
for entry in entries:
|
||||
# We squash the filename since we do not preserve directory structure of audio files in the tarball.
|
||||
base, ext = os.path.splitext(entry['audio_filepath'])
|
||||
base = base.replace('/', '_')
|
||||
# Need the following replacement as long as WebDataset splits on first period
|
||||
base = base.replace('.', '_')
|
||||
squashed_filename = f'{base}{ext}'
|
||||
if squashed_filename not in count:
|
||||
tar.add(entry['audio_filepath'], arcname=squashed_filename)
|
||||
|
||||
if 'label' in entry:
|
||||
base, ext = os.path.splitext(squashed_filename)
|
||||
# no suffix if it's single sample or starting sub parts, -sub1 for the second subpart -sub2 -sub3 ,etc.
|
||||
if squashed_filename not in count:
|
||||
to_write = squashed_filename
|
||||
count[squashed_filename] = 1
|
||||
else:
|
||||
to_write = base + "-sub" + str(count[squashed_filename]) + ext
|
||||
count[squashed_filename] += 1
|
||||
|
||||
new_entry = {
|
||||
'audio_filepath': to_write,
|
||||
'duration': entry['duration'],
|
||||
'text': entry['text'],
|
||||
'label': entry['label'],
|
||||
'offset': entry['offset'],
|
||||
'shard_id': shard_id, # Keep shard ID for recordkeeping
|
||||
}
|
||||
else:
|
||||
count[squashed_filename] = 1
|
||||
new_entry = {
|
||||
'audio_filepath': squashed_filename,
|
||||
'duration': entry['duration'],
|
||||
'text': entry['text'],
|
||||
'shard_id': shard_id, # Keep shard ID for recordkeeping
|
||||
}
|
||||
|
||||
new_entries.append(new_entry)
|
||||
|
||||
tar.close()
|
||||
return new_entries
|
||||
|
||||
@classmethod
|
||||
def setup_history(cls, base_metadata: ASRTarredDatasetMetadata, history: List[Any]):
|
||||
if 'history' in base_metadata.keys():
|
||||
for history_val in base_metadata.history:
|
||||
cls.setup_history(history_val, history)
|
||||
|
||||
if base_metadata is not None:
|
||||
metadata_copy = copy.deepcopy(base_metadata)
|
||||
with open_dict(metadata_copy):
|
||||
metadata_copy.pop('history', None)
|
||||
history.append(metadata_copy)
|
||||
|
||||
|
||||
def main():
|
||||
manifest_path = args.manifest_path
|
||||
concat_manifest_paths = args.concat_manifest_paths
|
||||
target_dir = args.target_dir
|
||||
metadata_path = args.metadata_path
|
||||
num_shards = args.num_shards
|
||||
max_duration = args.max_duration
|
||||
min_duration = args.min_duration
|
||||
shuffle = args.shuffle
|
||||
seed = args.shuffle_seed if args.shuffle_seed else None
|
||||
write_metadata = args.write_metadata
|
||||
num_workers = args.workers
|
||||
|
||||
builder = ASRTarredDatasetBuilder()
|
||||
|
||||
if write_metadata:
|
||||
metadata = ASRTarredDatasetMetadata()
|
||||
dataset_cfg = ASRTarredDatasetConfig(
|
||||
num_shards=num_shards,
|
||||
shuffle=shuffle,
|
||||
max_duration=max_duration,
|
||||
min_duration=min_duration,
|
||||
shuffle_seed=seed,
|
||||
)
|
||||
metadata.dataset_config = dataset_cfg
|
||||
|
||||
output_path = os.path.join(target_dir, 'default_metadata.yaml')
|
||||
OmegaConf.save(metadata, output_path, resolve=True)
|
||||
print(f"Default metadata written to {output_path}")
|
||||
exit(0)
|
||||
|
||||
if concat_manifest_paths is None or len(concat_manifest_paths) == 0:
|
||||
print("Creating new tarred dataset ...")
|
||||
|
||||
# Create a tarred dataset from scratch
|
||||
config = ASRTarredDatasetConfig(
|
||||
num_shards=num_shards,
|
||||
shuffle=shuffle,
|
||||
max_duration=max_duration,
|
||||
min_duration=min_duration,
|
||||
shuffle_seed=seed,
|
||||
)
|
||||
builder.configure(config)
|
||||
builder.create_new_dataset(manifest_path=manifest_path, target_dir=target_dir, num_workers=num_workers)
|
||||
|
||||
else:
|
||||
print("Concatenating multiple tarred datasets ...")
|
||||
|
||||
# Implicitly update config from base details
|
||||
if metadata_path is not None:
|
||||
metadata = ASRTarredDatasetMetadata.from_file(metadata_path)
|
||||
else:
|
||||
raise ValueError("`metadata` yaml file path must be provided!")
|
||||
|
||||
# Preserve history
|
||||
history = []
|
||||
builder.setup_history(OmegaConf.structured(metadata), history)
|
||||
metadata.history = history
|
||||
|
||||
# Add command line overrides (everything other than num_shards)
|
||||
metadata.dataset_config.max_duration = max_duration
|
||||
metadata.dataset_config.min_duration = min_duration
|
||||
metadata.dataset_config.shuffle = shuffle
|
||||
metadata.dataset_config.shuffle_seed = seed
|
||||
|
||||
builder.configure(metadata.dataset_config)
|
||||
|
||||
# Concatenate a tarred dataset onto a previous one
|
||||
builder.create_concatenated_dataset(
|
||||
base_manifest_path=manifest_path,
|
||||
manifest_paths=concat_manifest_paths,
|
||||
metadata=metadata,
|
||||
target_dir=target_dir,
|
||||
num_workers=num_workers,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,282 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# USAGE: python process_asr_text_tokenizer.py --manifest=<path to train manifest files, seperated by commas> \
|
||||
# --data_root="<output directory>" \
|
||||
# --vocab_size=<number of tokens in vocabulary> \
|
||||
# --tokenizer=<"spe" or "wpe"> \
|
||||
# --log
|
||||
# where <manifest> can be: train_clean_100, train_clean_360, train_other_500
|
||||
# You can also put more than one data_set comma-separated:
|
||||
# --manifest="train_clean_100,train_clean_360,train_other_500"
|
||||
# or
|
||||
# python process_asr_text_tokenizer.py --data_file=<path to train text file> \
|
||||
# --data_root="<output directory>" \
|
||||
# --vocab_size=<number of tokens in vocabulary> \
|
||||
# --tokenizer=<"bpe" or "wpe"> \
|
||||
# --log
|
||||
# where <manifest> can be: train_clean_100, train_clean_360, train_other_500
|
||||
# You can also put more than one data_set comma-separated:
|
||||
# --manifest="train_clean_100,train_clean_360,train_other_500"
|
||||
#
|
||||
# Args:
|
||||
# --manifest or --data_file: If your text data lies inside of an ASR manifest file,
|
||||
# then use the --manifest path. If instead the text data is inside a file with separate lines
|
||||
# corresponding to different text lines, then use --data_file.
|
||||
# In either case, you can add commas to concatenate different manifests or different data files.
|
||||
#
|
||||
# --data_root: The output directory (whose subdirectories will be created if not present) where
|
||||
# the tokenizers will be placed.
|
||||
#
|
||||
# --vocab_size: The size of the tokenizer vocabulary. Larger vocabularies can accommodate almost entire,
|
||||
# words but the decoder size of any model will grow proportionally.
|
||||
#
|
||||
# --tokenizer: Can be either spe or wpe . spe refers to the Google sentencepiece library tokenizer.
|
||||
# wpe refers to the HuggingFace BERT Word Piece tokenizer.
|
||||
#
|
||||
# --no_lower_case: When this flag is passed, it will force the tokenizer to create seperate tokens for
|
||||
# upper and lower case characters. By default, the script will turn all the text to lower case
|
||||
# before tokenization (and if upper case characters are passed during training/inference, the
|
||||
# tokenizer will emit a token equivalent to Out-Of-Vocabulary). Used primarily for the
|
||||
# English language.
|
||||
#
|
||||
# --spe_type: The sentencepiece library has a few implementations of the tokenization technique, and
|
||||
# spe_type refers to these implementations. Currently supported types are unigram, bpe, char, word.
|
||||
# Defaults to bpe.
|
||||
#
|
||||
# --spe_character_coverage: The sentencepiece library considers how much of the original vocabulary it
|
||||
# should cover in its "base set" of tokens (akin to the lower and upper case characters of the
|
||||
# English language). For almost all languages with small base token sets (<1000 tokens), this
|
||||
# should be kept at its default of 1.0. For languages with larger vocabularies (say Japanese,
|
||||
# Mandarin, Korean etc), the suggested value is 0.9995.
|
||||
#
|
||||
# --spe_sample_size: If the dataset is too large, consider using a sampled dataset indicated by a
|
||||
# positive integer. By default, any negative value (default = -1) will use the entire dataset.
|
||||
#
|
||||
# --spe_train_extremely_large_corpus: When training a sentencepiece tokenizer on very large amounts of text,
|
||||
# sometimes the tokenizer will run out of memory or wont be able to process so much data on RAM.
|
||||
# At some point you might receive the following error - "Input corpus too large, try with
|
||||
# train_extremely_large_corpus=true". If your machine has large amounts of RAM, it might still be possible
|
||||
# to build the tokenizer using the above flag. Will silently fail if it runs out of RAM.
|
||||
#
|
||||
# --spe_max_sentencepiece_length: Limits the maximum length that any any SentencePiece subword can be.
|
||||
# Using this will change the subword tokens generated.
|
||||
#
|
||||
# --log: Whether the script should display log messages
|
||||
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import tokenizers
|
||||
|
||||
from nemo.collections.common.tokenizers.sentencepiece_tokenizer import create_spt_model
|
||||
|
||||
parser = argparse.ArgumentParser(description='Create tokenizer')
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("--manifest", default=None, type=str, help='Comma separated list of manifest files')
|
||||
group.add_argument("--data_file", default=None, help='data file from which to create tokenizer model')
|
||||
parser.add_argument("--data_root", required=True, default=None, type=str, help='Output directory')
|
||||
parser.add_argument("--vocab_size", default=1024, type=int, help='Vocabulary size')
|
||||
parser.add_argument("--tokenizer", default="wpe", choices=["spe", "wpe"], help='Type of tokenization to perform')
|
||||
parser.add_argument(
|
||||
"--spe_type",
|
||||
default="bpe",
|
||||
choices=['bpe', 'unigram', 'char', 'word'],
|
||||
help='Type of the SentencePiece model. Can be `bpe`, `unigram`, `char` or `word`.'
|
||||
'Used only if --tokenizer == `spe`',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--spe_character_coverage',
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Character coverage percentage for SentencePiece tokenization. For languages "
|
||||
"with large vocabulary, should be close to 0.9995, otherwise kept as 1.0",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--spe_sample_size',
|
||||
type=int,
|
||||
default=-1,
|
||||
help="Samples the dataset by `sample_size` if positive integer, otherwise uses whole dataset",
|
||||
)
|
||||
parser.add_argument('--spe_train_extremely_large_corpus', action='store_true', help='')
|
||||
parser.add_argument(
|
||||
'--spe_max_sentencepiece_length',
|
||||
type=int,
|
||||
default=-1,
|
||||
help='Limit the maximum number of tokens in each SentencePiece subword. '
|
||||
'Must be a positive integer > 0. By default places no limit on subword length.',
|
||||
)
|
||||
parser.add_argument('--no_lower_case', dest='lower_case', action='store_false')
|
||||
parser.add_argument("--log", action='store_true')
|
||||
parser.set_defaults(log=False, lower_case=True, spe_train_extremely_large_corpus=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def __build_document_from_manifests(
|
||||
data_root: str, manifests: str,
|
||||
):
|
||||
if ',' in manifests:
|
||||
manifests = manifests.split(',')
|
||||
else:
|
||||
manifests = [manifests]
|
||||
|
||||
document_dir = os.path.join(data_root, 'text_corpus')
|
||||
if not os.path.exists(document_dir):
|
||||
os.makedirs(document_dir)
|
||||
|
||||
document_path = os.path.join(document_dir, 'document.txt')
|
||||
|
||||
if os.path.exists(document_path):
|
||||
logging.info('Corpus already exists at path : %s', document_path)
|
||||
return document_path
|
||||
|
||||
num_lines = 0
|
||||
with open(document_path, 'w') as out_writer:
|
||||
for manifest in manifests:
|
||||
with open(manifest, 'r') as in_reader:
|
||||
for line in in_reader:
|
||||
item = json.loads(line)
|
||||
text = item['text']
|
||||
|
||||
out_writer.write(text + '\n')
|
||||
out_writer.flush()
|
||||
|
||||
num_lines += 1
|
||||
|
||||
logging.info(f"Finished extracting manifest : {manifest}")
|
||||
|
||||
logging.info("Finished extracting all manifests ! Number of sentences : {}".format(num_lines))
|
||||
return document_path
|
||||
|
||||
|
||||
def __process_data(
|
||||
text_path: str,
|
||||
dst_folder: str,
|
||||
vocab_size: int,
|
||||
tokenizer_type: str,
|
||||
spe_type: str,
|
||||
spe_character_coverage: float,
|
||||
spe_train_extremely_large_corpus: bool,
|
||||
spe_sample_size: int,
|
||||
spe_max_sentencepiece_length: int,
|
||||
lower_case: bool,
|
||||
):
|
||||
"""
|
||||
Converts flac to wav and build manifests's json
|
||||
Args:
|
||||
text_path: source with text lines
|
||||
dst_folder: where wav files will be stored
|
||||
vocab_size: vocabular size used in encoding the text
|
||||
tokenizer_type: type of tokenization to perform - wpe or spe
|
||||
spe_type: type of tokenization model used for spe.
|
||||
spe_character_coverage: float value between 0 and 1 (as a percentage). For languages with a vast charset,
|
||||
can be < 1.0, but for all other languages, it should be set as 1.0
|
||||
spe_sample_size: int, default of -1. If positive integer is used, samples the dataset
|
||||
by given sample size.
|
||||
spe_train_extremely_large_corpus: bool. If dataset is too large, and user has sufficient RAM,
|
||||
this flag can be set to try to trained the tokenizer. Will silently fail if it runs out of RAM.
|
||||
spe_max_sentencepiece_length: Limits the maximum length of the SentencePiece subword that can be constructed.
|
||||
By default, no limit is placed.
|
||||
lower_case: whether to tokenize with lower case character set only (for english)
|
||||
|
||||
Returns:
|
||||
"""
|
||||
if tokenizer_type == 'spe':
|
||||
if spe_max_sentencepiece_length > 0:
|
||||
tokenizer_dir = os.path.join(dst_folder, 'tokenizer_{}_{}_v{}_max{}').format(
|
||||
tokenizer_type, spe_type, vocab_size, spe_max_sentencepiece_length
|
||||
)
|
||||
else:
|
||||
tokenizer_dir = os.path.join(dst_folder, 'tokenizer_{}_{}_v{}').format(
|
||||
tokenizer_type, spe_type, vocab_size
|
||||
)
|
||||
|
||||
if not os.path.exists(tokenizer_dir):
|
||||
os.makedirs(tokenizer_dir)
|
||||
|
||||
if os.path.exists(os.path.join(tokenizer_dir, 'tokenizer.model')):
|
||||
logging.warning("Model file already exists, overriding old model file !")
|
||||
os.remove(os.path.join(tokenizer_dir, 'tokenizer.model'))
|
||||
|
||||
tokenizer_path, vocab_path = create_spt_model(
|
||||
data_file=text_path,
|
||||
vocab_size=vocab_size,
|
||||
sample_size=spe_sample_size,
|
||||
do_lower_case=lower_case,
|
||||
output_dir=tokenizer_dir,
|
||||
tokenizer_type=spe_type,
|
||||
character_coverage=spe_character_coverage,
|
||||
train_extremely_large_corpus=spe_train_extremely_large_corpus,
|
||||
max_sentencepiece_length=spe_max_sentencepiece_length,
|
||||
)
|
||||
|
||||
else:
|
||||
tokenizer_dir = os.path.join(dst_folder, 'tokenizer_{}_v{}').format(tokenizer_type, vocab_size)
|
||||
|
||||
if not os.path.exists(tokenizer_dir):
|
||||
os.makedirs(tokenizer_dir)
|
||||
|
||||
tokenizer = tokenizers.BertWordPieceTokenizer(lowercase=lower_case)
|
||||
|
||||
tokenizer.train(text_path, vocab_size=vocab_size)
|
||||
tokenizer.save_model(tokenizer_dir)
|
||||
|
||||
return tokenizer_dir
|
||||
|
||||
|
||||
def main():
|
||||
data_root = args.data_root
|
||||
manifests = args.manifest
|
||||
data_file = args.data_file
|
||||
vocab_size = args.vocab_size
|
||||
tokenizer = args.tokenizer
|
||||
spe_type = args.spe_type
|
||||
spe_character_coverage = args.spe_character_coverage
|
||||
spe_sample_size = args.spe_sample_size
|
||||
spe_train_extremely_large_corpus = args.spe_train_extremely_large_corpus
|
||||
spe_max_sentencepiece_length = args.spe_max_sentencepiece_length
|
||||
lower_case = args.lower_case
|
||||
|
||||
if not os.path.exists(data_root):
|
||||
os.makedirs(data_root)
|
||||
|
||||
if args.log:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
if manifests:
|
||||
text_corpus_path = __build_document_from_manifests(data_root, manifests)
|
||||
else:
|
||||
text_corpus_path = data_file
|
||||
tokenizer_path = __process_data(
|
||||
text_corpus_path,
|
||||
data_root,
|
||||
vocab_size,
|
||||
tokenizer,
|
||||
spe_type,
|
||||
lower_case=lower_case,
|
||||
spe_character_coverage=spe_character_coverage,
|
||||
spe_sample_size=spe_sample_size,
|
||||
spe_train_extremely_large_corpus=spe_train_extremely_large_corpus,
|
||||
spe_max_sentencepiece_length=spe_max_sentencepiece_length,
|
||||
)
|
||||
|
||||
print("Serialized tokenizer at location :", tokenizer_path)
|
||||
logging.info('Done!')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,259 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import glob
|
||||
import os
|
||||
import time
|
||||
from argparse import ArgumentParser
|
||||
from itertools import repeat
|
||||
from multiprocessing import Pool
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
"""
|
||||
This script serves two purposes:
|
||||
|
||||
1) gen_overlap_seq:
|
||||
Generate predictions with overlapping input segments by using the frame level prediction from NeMo/examples/asr/vad_infer.py.
|
||||
Then a smoothing filter is applied to decide the label for a frame spanned by multiple segments.
|
||||
|
||||
2)gen_seg_table:
|
||||
Converting frame level prediction to speech/no-speech segment in start and end times format.
|
||||
|
||||
Usage:
|
||||
|
||||
python vad_overlap_posterior.py --gen_overlap_seq --gen_seg_table --frame_folder=<FULL PATH OF YOU STORED FRAME LEVEL PREDICTION> --method='median' --overlap=0.875 --num_workers=20 --threshold=0.8
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def gen_overlap_seq(frame_filepath, per_args):
|
||||
"""
|
||||
Given a frame level prediction, generate predictions with overlapping input segments by using it
|
||||
Args:
|
||||
frame_filepath : frame prediction file to be processed.
|
||||
per_args:
|
||||
method : Median or mean smoothing filter.
|
||||
overlap : Amounts of overlap.
|
||||
seg_len : Length of window for generating the frame.
|
||||
shift_len : Amount of shift of window for generating the frame.
|
||||
out_dir : Output dir of generated prediction.
|
||||
"""
|
||||
|
||||
try:
|
||||
method = per_args['method']
|
||||
overlap = per_args['overlap']
|
||||
seg_len = per_args['seg_len']
|
||||
shift_len = per_args['shift_len']
|
||||
out_dir = per_args['out_dir']
|
||||
|
||||
frame = np.loadtxt(frame_filepath)
|
||||
name = os.path.basename(frame_filepath).split(".frame")[0] + "." + method
|
||||
overlap_filepath = os.path.join(out_dir, name)
|
||||
|
||||
shift = int(shift_len / 0.01) # number of units of shift
|
||||
seg = int((seg_len / 0.01 + 1)) # number of units of each window/segment
|
||||
|
||||
jump_on_target = int(seg * (1 - overlap)) # jump on target generated sequence
|
||||
jump_on_frame = int(jump_on_target / shift) # jump on input frame sequence
|
||||
|
||||
if jump_on_frame < 1:
|
||||
raise ValueError(
|
||||
f"Note we jump over frame sequence to generate overlapping input segments. \n \
|
||||
Your input makes jump_on_fram={jump_on_frame} < 1 which is invalid because it cannot jump and will stuck.\n \
|
||||
Please try different seg_len, shift_len and overlap choices. \n \
|
||||
jump_on_target = int(seg * (1 - overlap)) \n \
|
||||
jump_on_frame = int(jump_on_frame/shift) "
|
||||
)
|
||||
|
||||
target_len = int(len(frame) * shift)
|
||||
|
||||
if method == 'mean':
|
||||
preds = np.zeros(target_len)
|
||||
pred_count = np.zeros(target_len)
|
||||
|
||||
for i, og_pred in enumerate(frame):
|
||||
if i % jump_on_frame != 0:
|
||||
continue
|
||||
start = i * shift
|
||||
end = start + seg
|
||||
preds[start:end] = preds[start:end] + og_pred
|
||||
pred_count[start:end] = pred_count[start:end] + 1
|
||||
|
||||
preds = preds / pred_count
|
||||
last_non_zero_pred = preds[pred_count != 0][-1]
|
||||
preds[pred_count == 0] = last_non_zero_pred
|
||||
|
||||
elif method == 'median':
|
||||
preds = [[] for _ in range(target_len)]
|
||||
for i, og_pred in enumerate(frame):
|
||||
if i % jump_on_frame != 0:
|
||||
continue
|
||||
|
||||
start = i * shift
|
||||
end = start + seg
|
||||
for j in range(start, end):
|
||||
if j <= target_len - 1:
|
||||
preds[j].append(og_pred)
|
||||
|
||||
preds = np.array([np.median(l) for l in preds])
|
||||
nan_idx = np.isnan(preds)
|
||||
last_non_nan_pred = preds[~nan_idx][-1]
|
||||
preds[nan_idx] = last_non_nan_pred
|
||||
|
||||
else:
|
||||
raise ValueError("method should be either mean or median")
|
||||
|
||||
round_final = np.round(preds, 4)
|
||||
np.savetxt(overlap_filepath, round_final, delimiter='\n')
|
||||
print(f"Finished! {overlap_filepath}!")
|
||||
|
||||
except Exception as e:
|
||||
raise (e)
|
||||
|
||||
|
||||
def gen_seg_table(frame_filepath, per_args):
|
||||
|
||||
"""
|
||||
Convert frame level prediction to speech/no-speech segment in start and end times format.
|
||||
And save to csv file in rttm-like format
|
||||
0, 10, speech
|
||||
10,12, no-speech
|
||||
Args:
|
||||
frame_filepath : frame prediction file to be processed.
|
||||
per_args :
|
||||
threshold : threshold for prediction score (from 0 to 1).
|
||||
shift_len : Amount of shift of window for generating the frame.
|
||||
out_dir : Output dir of generated table/csv file.
|
||||
"""
|
||||
threshold = per_args['threshold']
|
||||
shift_len = per_args['shift_len']
|
||||
out_dir = per_args['out_dir']
|
||||
|
||||
print(f"process {frame_filepath}")
|
||||
name = frame_filepath.split("/")[-1].rsplit(".", 1)[0]
|
||||
|
||||
sequence = np.loadtxt(frame_filepath)
|
||||
start = 0
|
||||
end = 0
|
||||
start_list = [0]
|
||||
end_list = []
|
||||
state_list = []
|
||||
|
||||
for i in range(len(sequence) - 1):
|
||||
current_sate = "non-speech" if sequence[i] <= threshold else "speech"
|
||||
next_state = "non-speech" if sequence[i + 1] <= threshold else "speech"
|
||||
if next_state != current_sate:
|
||||
end = i * shift_len + shift_len # shift_len for handling joint
|
||||
state_list.append(current_sate)
|
||||
end_list.append(end)
|
||||
|
||||
start = (i + 1) * shift_len
|
||||
start_list.append(start)
|
||||
|
||||
end_list.append((i + 1) * shift_len + shift_len)
|
||||
state_list.append(current_sate)
|
||||
|
||||
seg_table = pd.DataFrame({'start': start_list, 'end': end_list, 'vad': state_list})
|
||||
|
||||
save_name = name + ".txt"
|
||||
save_path = os.path.join(out_dir, save_name)
|
||||
seg_table.to_csv(save_path, sep='\t', index=False, header=False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
start = time.time()
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("--gen_overlap_seq", default=False, action='store_true')
|
||||
parser.add_argument("--gen_seg_table", default=False, action='store_true')
|
||||
parser.add_argument("--frame_folder", type=str, required=True)
|
||||
parser.add_argument(
|
||||
"--method",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Use mean/median for overlapped prediction. Use frame for gen_seg_table of frame prediction",
|
||||
)
|
||||
parser.add_argument("--overlap_out_dir", type=str)
|
||||
parser.add_argument("--table_out_dir", type=str)
|
||||
parser.add_argument("--overlap", type=float, default=0.875, help="Overlap percentatge. Default is 0.875")
|
||||
parser.add_argument("--seg_len", type=float, default=0.63)
|
||||
parser.add_argument("--shift_len", type=float, default=0.01)
|
||||
parser.add_argument("--threshold", type=float, default=0.5)
|
||||
parser.add_argument("--num_workers", type=int, default=4)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.gen_overlap_seq:
|
||||
p = Pool(processes=args.num_workers)
|
||||
print("Generating predictions with overlapping input segments")
|
||||
frame_filepathlist = glob.glob(args.frame_folder + "/*.frame")
|
||||
|
||||
if not args.overlap_out_dir:
|
||||
overlap_out_dir = "./overlap_smoothing_output" + "_" + args.method + "_" + str(args.overlap)
|
||||
else:
|
||||
overlap_out_dir = args.overlap_out_dir
|
||||
if not os.path.exists(overlap_out_dir):
|
||||
print(f"Creating output dir {overlap_out_dir}")
|
||||
os.mkdir(overlap_out_dir)
|
||||
|
||||
per_args = {
|
||||
"method": args.method,
|
||||
"overlap": args.overlap,
|
||||
"seg_len": args.seg_len,
|
||||
"shift_len": args.shift_len,
|
||||
"out_dir": overlap_out_dir,
|
||||
}
|
||||
|
||||
p.starmap(gen_overlap_seq, zip(frame_filepathlist, repeat(per_args)))
|
||||
p.close()
|
||||
p.join()
|
||||
|
||||
end = time.time()
|
||||
print(f"Generate overlapped prediction takes {end-start} seconds!\n Save to {overlap_out_dir}")
|
||||
|
||||
if args.gen_seg_table:
|
||||
p = Pool(processes=args.num_workers)
|
||||
print("Converting frame level prediction to speech/no-speech segment in start and end times format.")
|
||||
|
||||
if args.gen_overlap_seq:
|
||||
print("Use overlap prediction. Change if you want to use basic frame level prediction")
|
||||
frame_filepath = overlap_out_dir
|
||||
shift_len = 0.01
|
||||
else:
|
||||
print("Use basic frame level prediction")
|
||||
frame_filepath = args.frame_folder
|
||||
shift_len = args.shift_len
|
||||
|
||||
frame_filepathlist = glob.glob(frame_filepath + "/*." + args.method)
|
||||
|
||||
if not args.table_out_dir:
|
||||
table_out_dir = "table_output_" + str(args.threshold)
|
||||
else:
|
||||
table_out_dir = args.table_out_dir
|
||||
if not os.path.exists(table_out_dir):
|
||||
print(f"Creating rttm-like table output dir {table_out_dir}")
|
||||
os.mkdir(table_out_dir)
|
||||
|
||||
per_args = {
|
||||
"threshold": args.threshold,
|
||||
"shift_len": shift_len,
|
||||
"out_dir": table_out_dir,
|
||||
}
|
||||
|
||||
p.starmap(gen_seg_table, zip(frame_filepathlist, repeat(per_args)))
|
||||
p.close()
|
||||
p.join()
|
||||
|
||||
end = time.time()
|
||||
print(f"Generate rttm-like table for {frame_filepath} takes {end-start} seconds!\n Save to {table_out_dir}")
|
||||
@@ -0,0 +1,58 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
|
||||
import numpy as np
|
||||
|
||||
from nemo.collections.asr.parts.utils.vad_utils import vad_tune_threshold_on_dev
|
||||
from nemo.utils import logging
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--threshold_range", help="range of threshold in list 'START,END,STEP' to be tuned on", required=True
|
||||
)
|
||||
parser.add_argument(
|
||||
"--vad_pred", help="Directory of vad predictions or a file contains the paths of them.", required=True
|
||||
)
|
||||
parser.add_argument(
|
||||
"--groundtruth_RTTM",
|
||||
help="Directory of groundtruch rttm files or a file contains the paths of them",
|
||||
type=str,
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--vad_pred_method",
|
||||
help="suffix of prediction file. Should be either in 'frame', 'mean' or 'median'",
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--focus_metric",
|
||||
help="metrics we care most when tuning threshold. Should be either in 'DetER', 'FA', 'MISS' ",
|
||||
type=str,
|
||||
default='DetER',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
start, stop, step = [float(i) for i in args.threshold_range.split(",")]
|
||||
thresholds = np.arange(start, stop, step)
|
||||
except:
|
||||
raise ValueError("Theshold input is invalid! Please enter it as a 'START,STOP,STEP' ")
|
||||
|
||||
best_threhsold = vad_tune_threshold_on_dev(
|
||||
thresholds, args.vad_pred, args.groundtruth_RTTM, args.vad_pred_method, args.focus_metric
|
||||
)
|
||||
logging.info(f"Best threshold selected from {thresholds} is {best_threhsold}!")
|
||||
@@ -0,0 +1,173 @@
|
||||
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
from argparse import ArgumentParser
|
||||
from itertools import repeat
|
||||
|
||||
import librosa
|
||||
import numpy as np
|
||||
|
||||
|
||||
"""
|
||||
This script is designed for inference of frame level Voice Activity Detection (VAD)
|
||||
|
||||
This script serves three goals:
|
||||
(1) Write audio files to manifest
|
||||
(2) Split audio file for avoiding CUDA memory issue
|
||||
(3) Take care of joint of seperate json line for an audio file
|
||||
|
||||
Usage:
|
||||
python write_long_audio_manifest.py --inp_dir=<FULL PATH OF FOLDER OF AUDIO FILES> --manifest_name=<NAME OF OUTPUT JSON FILE> --split_duration=300 --time_length=0.63 --num_worker=10
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def write_manifest(file, args_func):
|
||||
"""
|
||||
Given a list of files, write them to manifest with restrictions.
|
||||
Args:
|
||||
files : file to be processed
|
||||
label : label for audio snippet.
|
||||
split_duration : Max duration of each audio clip (each line in json)
|
||||
time_length : Used for taking care of joint.
|
||||
Length of window for generating the frame.
|
||||
Returns:
|
||||
res : list of generated metadata line of json for file
|
||||
"""
|
||||
|
||||
label = args_func['label']
|
||||
split_duration = args_func['split_duration']
|
||||
time_length = args_func['time_length']
|
||||
|
||||
res = []
|
||||
try:
|
||||
sr = 16000
|
||||
x, _sr = librosa.load(file, sr=sr)
|
||||
duration = librosa.get_duration(x, sr=sr)
|
||||
|
||||
left = duration
|
||||
current_offset = 0
|
||||
status = 'single'
|
||||
|
||||
while left > 0:
|
||||
if left <= split_duration:
|
||||
if status == 'single':
|
||||
write_duration = left
|
||||
current_offset = 0
|
||||
else:
|
||||
status = 'end'
|
||||
write_duration = left + time_length
|
||||
current_offset -= time_length
|
||||
offset_inc = left
|
||||
left = 0
|
||||
else:
|
||||
if status == 'start' or status == 'next':
|
||||
status = 'next'
|
||||
else:
|
||||
status = 'start'
|
||||
|
||||
if status == 'start':
|
||||
write_duration = split_duration
|
||||
offset_inc = split_duration
|
||||
else:
|
||||
write_duration = split_duration + time_length
|
||||
current_offset -= time_length
|
||||
offset_inc = split_duration + time_length
|
||||
|
||||
left -= split_duration
|
||||
|
||||
metadata = {
|
||||
'audio_filepath': file,
|
||||
'duration': write_duration,
|
||||
'label': label,
|
||||
'text': '_',
|
||||
'offset': current_offset,
|
||||
}
|
||||
res.append(metadata)
|
||||
|
||||
current_offset += offset_inc
|
||||
|
||||
except Exception as e:
|
||||
err_file = "error.log"
|
||||
with open(err_file, 'w') as fout:
|
||||
fout.write(file + ":" + str(e))
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("--inp_dir", type=str, required=True, help="(full path) folder of files to be processed")
|
||||
parser.add_argument(
|
||||
"--inp_list", type=str, help="(full path) a file contains NAME of files inside inp_dir to be processed"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out_dir", type=str, default=".", help="[Optional](full path) location to store generated json file"
|
||||
)
|
||||
parser.add_argument("--manifest_name", type=str, required=True, help="name of generated json file")
|
||||
parser.add_argument("--split_duration", type=int, required=True, help="max duration of each audio clip/line")
|
||||
parser.add_argument(
|
||||
"--time_length", type=float, default=0.63, help="[Optional] time length of segment, default is 0.63s"
|
||||
)
|
||||
parser.add_argument("--num_workers", type=int, default=4, help="[Optional] number of workers for multiprocessing")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.inp_list:
|
||||
input_audios = []
|
||||
for root, dirs, files in os.walk(args.inp_dir):
|
||||
for basename in files:
|
||||
if basename.endswith('.wav'):
|
||||
filename = os.path.join(root, basename)
|
||||
input_audios.append(filename)
|
||||
else:
|
||||
name_list = np.loadtxt(args.inp_list, dtype='str')
|
||||
input_audios = [os.path.join(args.inp_dir, name + ".wav") for name in name_list]
|
||||
print(f"Number of wav files to be processed: {len(input_audios)}")
|
||||
|
||||
output_path = os.path.join(args.out_dir, args.manifest_name + '.json')
|
||||
print(f"Save generate manifest to {output_path}!")
|
||||
if not os.path.exists(args.out_dir):
|
||||
logging.info(f'Outdir {args.out_dir} does not exist. Creat directory.')
|
||||
os.mkdir(args.out_dir)
|
||||
if os.path.exists(output_path):
|
||||
print(f"Manifest {output_path} exists! Overwriting")
|
||||
os.remove(output_path)
|
||||
|
||||
p = multiprocessing.Pool(processes=args.num_workers)
|
||||
|
||||
args_func = {
|
||||
'label': 'infer',
|
||||
'split_duration': args.split_duration,
|
||||
'time_length': args.time_length,
|
||||
}
|
||||
|
||||
results = p.starmap(write_manifest, zip(input_audios, repeat(args_func)))
|
||||
|
||||
with open(output_path, 'a') as fout:
|
||||
for res in results:
|
||||
for r in res:
|
||||
json.dump(r, fout)
|
||||
fout.write('\n')
|
||||
fout.flush()
|
||||
|
||||
p.close()
|
||||
|
||||
print(f"Done! Save to {output_path}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user