chore: import upstream snapshot with attribution
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import get_ctm_line, read_manifest, write_ctm, write_manifest
|
||||
from nemo.utils import logging
|
||||
|
||||
|
||||
def get_seg_info_from_ctm_line(
|
||||
ctm_list: List[str],
|
||||
output_precision: int,
|
||||
speaker_index: int = 7,
|
||||
start_time_index: int = 2,
|
||||
duration_index: int = 3,
|
||||
):
|
||||
"""
|
||||
Get time stamp information and speaker labels from CTM lines.
|
||||
This is following CTM format appeared in `Rich Transcription Meeting Eval Plan: RT09` document.
|
||||
|
||||
CTM Format:
|
||||
<SOURCE>< <CHANNEL> <BEG-TIME> <DURATION> <TOKEN> <CONF> <TYPE> <SPEAKER>
|
||||
|
||||
Args:
|
||||
ctm_list (list): List containing CTM items. e.g.: ['sw02001-A', '1', '0.000', '0.200', 'hello', '0.98', 'lex', 'speaker3']
|
||||
output_precision (int): Precision for CTM outputs in integer.
|
||||
|
||||
Returns:
|
||||
start (float): Start time of the segment.
|
||||
end (float): End time of the segment.
|
||||
speaker_id (str): Speaker ID of the segment.
|
||||
"""
|
||||
speaker_id = ctm_list[speaker_index]
|
||||
start = float(ctm_list[start_time_index])
|
||||
end = float(ctm_list[start_time_index]) + float(ctm_list[duration_index])
|
||||
start = round(start, output_precision)
|
||||
end = round(end, output_precision)
|
||||
if type(speaker_id) == str:
|
||||
speaker_id = speaker_id.strip()
|
||||
return start, end, speaker_id
|
||||
|
||||
|
||||
def get_unaligned_files(unaligned_path: str) -> List[str]:
|
||||
"""
|
||||
Get files without alignments in order to filter them out (as they cannot be used for data simulation).
|
||||
In the unaligned file, each line contains the file name and the reason for the unalignment, if necessary to specify.
|
||||
|
||||
Example: unaligned.txt
|
||||
|
||||
<utterance_id> <comment>
|
||||
1272-128104-0000 (no such file)
|
||||
2289-152257-0025 (no such file)
|
||||
2289-152257-0026 (mapping failed)
|
||||
...
|
||||
|
||||
Args:
|
||||
unaligned_path (str): Path to the file containing unaligned examples
|
||||
|
||||
Returns:
|
||||
skip_files (list): Unaligned file names to skip
|
||||
"""
|
||||
skip_files = []
|
||||
with open(unaligned_path, 'r', encoding='utf-8') as f:
|
||||
for line in f.readlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
unaligned_file = line.split()[0]
|
||||
skip_files.append(unaligned_file)
|
||||
return skip_files
|
||||
|
||||
|
||||
def get_new_ctm_lines_from_alignments(session_name, speaker_id, wordlist, alignments, output_precision=3) -> List[str]:
|
||||
"""
|
||||
Create new CTM entry (to write to output ctm file)
|
||||
|
||||
Args:
|
||||
session_name (str): Current session name.
|
||||
speaker_id (int): LibriSpeech speaker ID for the current entry.
|
||||
wordlist (list): List of words
|
||||
alignments (list): List of alignments
|
||||
output_precision (int): Precision for CTM outputs
|
||||
Returns:
|
||||
arr (list): List of ctm entries, each entry is a tuple of (start_time, text)
|
||||
"""
|
||||
arr = []
|
||||
for i in range(len(wordlist)):
|
||||
word = wordlist[i]
|
||||
if word != "":
|
||||
# note that using the current alignments the first word is always empty, so there is no error from indexing the array with i-1
|
||||
align1 = float(round(alignments[i - 1], output_precision))
|
||||
align2 = float(
|
||||
round(
|
||||
alignments[i] - alignments[i - 1],
|
||||
output_precision,
|
||||
)
|
||||
)
|
||||
text = get_ctm_line(
|
||||
source=session_name,
|
||||
channel=speaker_id,
|
||||
start_time=align1,
|
||||
duration=align2,
|
||||
token=word,
|
||||
conf=None,
|
||||
type_of_token='lex',
|
||||
speaker=speaker_id,
|
||||
output_precision=output_precision,
|
||||
)
|
||||
arr.append((align1, text))
|
||||
return arr
|
||||
|
||||
|
||||
def load_librispeech_alignment(alignment_filepath: str) -> dict:
|
||||
"""
|
||||
Load alignment data for librispeech
|
||||
|
||||
Args:
|
||||
alignment_filepath (str): Path to the file containing alignments
|
||||
Returns:
|
||||
alignments (dict[tuple]): A dictionary containing file index and alignments
|
||||
"""
|
||||
alignments = {}
|
||||
with open(alignment_filepath, "r") as fin:
|
||||
for line in fin.readlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
file_id, words, timestamps = line.split()
|
||||
alignments[file_id] = (words, timestamps)
|
||||
return alignments
|
||||
|
||||
|
||||
def create_librispeech_ctm_alignments(
|
||||
input_manifest_filepath, base_alignment_path, ctm_output_directory, libri_dataset_split
|
||||
):
|
||||
"""
|
||||
Create new CTM alignments using input LibriSpeech word alignments.
|
||||
|
||||
Args:
|
||||
input_manifest_filepath (str): Path to the input LibriSpeech manifest file
|
||||
base_alignment_path (str): Path to the base directory containing the LibriSpeech word alignments
|
||||
ctm_source_dir (str): Directory to write the CTM files to
|
||||
libri_dataset_split (str): Which split of the LibriSpeech dataset is being used
|
||||
"""
|
||||
manifest = read_manifest(input_manifest_filepath)
|
||||
unaligned_path = os.path.join(base_alignment_path, "unaligned.txt")
|
||||
|
||||
if os.path.exists(unaligned_path):
|
||||
unaligned_file_ids = set(get_unaligned_files(unaligned_path))
|
||||
else:
|
||||
unaligned_file_ids = set()
|
||||
|
||||
libri_dataset_split = libri_dataset_split.replace("_", "-")
|
||||
|
||||
# delete output directory if it exists or throw warning
|
||||
if os.path.isdir(ctm_output_directory):
|
||||
logging.info(f"Removing existing output directory: {ctm_output_directory}")
|
||||
shutil.rmtree(ctm_output_directory)
|
||||
if not os.path.exists(ctm_output_directory):
|
||||
logging.info(f"Creating output directory: {ctm_output_directory}")
|
||||
os.mkdir(ctm_output_directory)
|
||||
|
||||
if len(manifest) == 0:
|
||||
raise Exception(f"Input manifest is empty: {input_manifest_filepath}")
|
||||
|
||||
for entry in manifest:
|
||||
audio_file = entry['audio_filepath']
|
||||
file_id = Path(audio_file).stem
|
||||
|
||||
if file_id in unaligned_file_ids:
|
||||
continue
|
||||
|
||||
speaker_id = file_id.split('-')[0]
|
||||
book_id = file_id.split('-')[1]
|
||||
book_dir = os.path.join(base_alignment_path, "LibriSpeech", libri_dataset_split, speaker_id, book_id)
|
||||
alignment_filepath = os.path.join(book_dir, f"{speaker_id}-{book_id}.alignment.txt")
|
||||
|
||||
alignment_data = load_librispeech_alignment(alignment_filepath)
|
||||
if file_id not in alignment_data:
|
||||
logging.warning(f"Cannot find alignment data for {audio_file} in {alignment_filepath}")
|
||||
continue
|
||||
|
||||
words, end_times = alignment_data[file_id]
|
||||
words = words.replace('\"', '').lower().split(',')
|
||||
end_times = [float(e) for e in end_times.replace('\"', '').split(',')]
|
||||
ctm_list = get_new_ctm_lines_from_alignments(file_id, speaker_id, words, end_times)
|
||||
write_ctm(os.path.join(ctm_output_directory, file_id + '.ctm'), ctm_list)
|
||||
|
||||
|
||||
def create_manifest_with_alignments(
|
||||
input_manifest_filepath,
|
||||
ctm_source_dir,
|
||||
output_manifest_filepath,
|
||||
data_format_style,
|
||||
silence_dur_threshold=0.1,
|
||||
output_precision=3,
|
||||
):
|
||||
"""
|
||||
Create new manifest file with word alignments using CTM files
|
||||
|
||||
Args:
|
||||
input_manifest_filepath (str): Path to the input manifest file
|
||||
ctm_source_dir (str): Directory to read the CTM files from
|
||||
output_manifest_filepath (str): Path to the output manifest file containing word alignments
|
||||
precision (int): How many decimal places to keep in the manifest file
|
||||
"""
|
||||
manifest = read_manifest(input_manifest_filepath)
|
||||
|
||||
target_manifest = []
|
||||
src_i = 0
|
||||
tgt_i = 0
|
||||
while src_i < len(manifest):
|
||||
f = manifest[src_i]
|
||||
fn = f['audio_filepath'].split('/')[-1]
|
||||
filename = fn.split('.')[0] # assuming that there is only one period in the input filenames
|
||||
if "voxceleb" in data_format_style:
|
||||
fn_split = f['audio_filepath'].split('/')
|
||||
filename = fn_split[-3] + '-' + fn_split[-2] + '-' + fn_split[-1].split('.')[0]
|
||||
ctm_filepath = os.path.join(ctm_source_dir, filename + '.ctm')
|
||||
else:
|
||||
ctm_filepath = os.path.join(ctm_source_dir, filename + '.ctm')
|
||||
|
||||
if not os.path.isfile(ctm_filepath):
|
||||
logging.info(f"Skipping {filename}.wav as there is no corresponding CTM file")
|
||||
src_i += 1
|
||||
continue
|
||||
|
||||
with open(ctm_filepath, 'r') as ctm_file:
|
||||
lines = ctm_file.readlines()
|
||||
|
||||
# One-word samples should be filtered out.
|
||||
if len(lines) <= 1:
|
||||
src_i += 1
|
||||
continue
|
||||
|
||||
words = []
|
||||
end_times = []
|
||||
i = 0
|
||||
prev_end = 0
|
||||
for i in range(len(lines)):
|
||||
ctm = lines[i].split(' ')
|
||||
start, end, speaker_id = get_seg_info_from_ctm_line(ctm_list=ctm, output_precision=output_precision)
|
||||
interval = start - prev_end
|
||||
|
||||
if (i == 0 and interval > 0) or (i > 0 and interval > silence_dur_threshold):
|
||||
words.append("")
|
||||
end_times.append(start)
|
||||
elif i > 0:
|
||||
end_times[-1] = start
|
||||
|
||||
words.append(ctm[4])
|
||||
end_times.append(end)
|
||||
|
||||
i += 1
|
||||
prev_end = end
|
||||
|
||||
# append last end
|
||||
if f['duration'] > prev_end:
|
||||
words.append("")
|
||||
end_times.append(f['duration'])
|
||||
|
||||
# build target manifest entry
|
||||
target_manifest.append(
|
||||
{
|
||||
'audio_filepath': f['audio_filepath'],
|
||||
'duration': f['duration'],
|
||||
'text': f['text'],
|
||||
'words': words,
|
||||
'alignments': end_times,
|
||||
'speaker_id': speaker_id,
|
||||
}
|
||||
)
|
||||
|
||||
src_i += 1
|
||||
tgt_i += 1
|
||||
|
||||
logging.info(f"Writing output manifest file to {output_manifest_filepath}")
|
||||
write_manifest(output_manifest_filepath, target_manifest)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Create a combined manifest file including word alignments and speaker IDs
|
||||
"""
|
||||
input_manifest_filepath = args.input_manifest_filepath
|
||||
base_alignment_path = args.base_alignment_path
|
||||
output_manifest_filepath = args.output_manifest_filepath
|
||||
ctm_output_directory = args.ctm_output_directory
|
||||
libri_dataset_split = args.libri_dataset_split
|
||||
use_ctm_alignment_source = args.use_ctm_alignment_source
|
||||
output_precision = args.output_precision
|
||||
|
||||
# Case 1: args.base_alignment_path is containing the ctm files
|
||||
if use_ctm_alignment_source:
|
||||
ctm_source_dir = args.base_alignment_path
|
||||
# Case 2: args.base_alignment_path is containing *.lab style alignments for the dataset
|
||||
else:
|
||||
create_librispeech_ctm_alignments(
|
||||
input_manifest_filepath, base_alignment_path, ctm_output_directory, libri_dataset_split
|
||||
)
|
||||
ctm_source_dir = ctm_output_directory
|
||||
|
||||
create_manifest_with_alignments(
|
||||
input_manifest_filepath,
|
||||
ctm_source_dir,
|
||||
output_manifest_filepath,
|
||||
data_format_style=args.data_format_style,
|
||||
silence_dur_threshold=args.silence_dur_threshold,
|
||||
output_precision=output_precision,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""
|
||||
This script creates a manifest file to be used for generating synthetic
|
||||
multispeaker audio sessions. The script takes in the default manifest file
|
||||
for a LibriSpeech dataset and corresponding word alignments and produces
|
||||
a combined manifest file that contains word alignments and speaker IDs
|
||||
per example. It can also be used to produce a manifest file for a different
|
||||
dataset if alignments are passed in CTM files.
|
||||
|
||||
The alignments are obtained from: https://github.com/CorentinJ/librispeech-alignments
|
||||
|
||||
Args:
|
||||
input_manifest_filepath (str): Path to input manifest file
|
||||
base_alignment_path (str): Path to the base directory for the LibriSpeech alignment dataset
|
||||
(specifically to the LibriSpeech-Alignments directory containing
|
||||
both the LibriSpeech folder as well as the unaligned.txt file)
|
||||
or to a directory containing the requisite CTM files
|
||||
output_manifest_filepath (str): Path to output manifest file
|
||||
ctm_output_directory (str): Path to output CTM directory (only used for LibriSpeech)
|
||||
libri_dataset_split (str): Which dataset split to create a combined manifest file for
|
||||
use_ctm_alignment_source (bool): If true, base_alignment_path points to a directory containing ctm files
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="LibriSpeech Alignment Manifest Creator")
|
||||
parser.add_argument("--input_manifest_filepath", help="path to input manifest file", type=str, required=True)
|
||||
parser.add_argument("--base_alignment_path", help="path to alignments (LibriSpeech)", type=str, required=False)
|
||||
parser.add_argument("--output_manifest_filepath", help="path to output manifest file", type=str, required=True)
|
||||
parser.add_argument(
|
||||
"--ctm_output_directory",
|
||||
help="path to output ctm directory for LibriSpeech (or to input CTM directory)",
|
||||
type=str,
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--libri_dataset_split",
|
||||
help="which test/dev/training set to create a manifest for (only used for LibriSpeech)",
|
||||
type=str,
|
||||
required=False,
|
||||
default="",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use_ctm_alignment_source",
|
||||
help="if true, base_alignment_path points to a directory containing ctm files",
|
||||
action='store_true',
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--data_format_style",
|
||||
help="Use specific format for speaker IDs and utterance IDs. e.g. 'voxceleb', 'librispeech', 'swbd'",
|
||||
default="",
|
||||
type=str,
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output_precision", help="precision for output alignments", type=int, required=False, default=3
|
||||
)
|
||||
parser.add_argument(
|
||||
"--silence_dur_threshold", help="threshold for inserting silence", type=float, required=False, default=0.1
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,205 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
This script creates a manifest file for diarization training. If you specify `pairwise_rttm_output_folder`, the script generates
|
||||
a two-speaker subset of the original RTTM files. For example, an RTTM file with 4 speakers will obtain 6 different pairs and
|
||||
6 RTTM files with two speakers in each RTTM file.
|
||||
|
||||
Args:
|
||||
--input_manifest_path: input json file name
|
||||
--output_manifest_path: output manifest_file name
|
||||
--pairwise_rttm_output_folder: Save two-speaker pair RTTM files
|
||||
--window: Window length for segmentation
|
||||
--shift: Shift length for segmentation
|
||||
--decimals: Rounding decimals
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import itertools
|
||||
import os
|
||||
import random
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import (
|
||||
get_input_manifest_dict,
|
||||
get_subsegment_dict,
|
||||
rreplace,
|
||||
write_truncated_subsegments,
|
||||
)
|
||||
from nemo.collections.asr.parts.utils.speaker_utils import (
|
||||
audio_rttm_map,
|
||||
rttm_to_labels,
|
||||
segments_manifest_to_subsegments_manifest,
|
||||
write_rttm2manifest,
|
||||
)
|
||||
from nemo.utils import logging
|
||||
|
||||
random.seed(42)
|
||||
|
||||
|
||||
def labels_to_rttmfile(labels, uniq_id, filename, out_rttm_dir):
|
||||
"""
|
||||
Write rttm file with uniq_id name in out_rttm_dir with time_stamps in labels
|
||||
"""
|
||||
filename = os.path.join(out_rttm_dir, filename + '.rttm')
|
||||
with open(filename, 'w') as f:
|
||||
for line in labels:
|
||||
line = line.strip()
|
||||
start, end, speaker = line.split()
|
||||
duration = float(end) - float(start)
|
||||
start = float(start)
|
||||
log = 'SPEAKER {} 1 {:.3f} {:.3f} <NA> <NA> {} <NA> <NA>\n'.format(uniq_id, start, duration, speaker)
|
||||
f.write(log)
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def split_into_pairwise_rttm(audio_rttm_map, input_manifest_path, output_dir):
|
||||
"""
|
||||
Create pairwise RTTM files and save it to `output_dir`. This function picks two speakers from the original RTTM files
|
||||
then saves the two-speaker subset of RTTM to `output_dir`.
|
||||
|
||||
Args:
|
||||
audio_rttm_map (dict):
|
||||
A dictionary with keys of uniq id, which is being used to map audio files and corresponding rttm files
|
||||
input_manifest_path (str):
|
||||
Path of the input manifest file.
|
||||
output_dir (str):
|
||||
Path to the directory where the new RTTM files are saved.
|
||||
"""
|
||||
input_manifest_dict = get_input_manifest_dict(input_manifest_path)
|
||||
rttmlist = []
|
||||
rttm_split_manifest_dict = {}
|
||||
split_audio_rttm_map = {}
|
||||
logging.info("Creating split RTTM files.")
|
||||
for uniq_id, line in tqdm(input_manifest_dict.items(), total=len(input_manifest_dict)):
|
||||
audiopath = line['audio_filepath']
|
||||
num_speakers = line['num_speakers']
|
||||
rttm_filepath = line['rttm_filepath']
|
||||
|
||||
rttm = rttm_to_labels(rttm_filepath)
|
||||
speakers = []
|
||||
j = 0
|
||||
while len(speakers) < num_speakers:
|
||||
if rttm[j].split(' ')[2] not in speakers:
|
||||
speakers.append(rttm[j].split(' ')[2])
|
||||
j += 1
|
||||
base_fn = audiopath.split('/')[-1].replace('.wav', '')
|
||||
for pair in itertools.combinations(speakers, 2):
|
||||
i, target_rttm = 0, []
|
||||
while i < len(rttm):
|
||||
entry = rttm[i]
|
||||
sp_id = entry.split(' ')[2]
|
||||
if sp_id in pair:
|
||||
target_rttm.append(entry)
|
||||
i += 1
|
||||
|
||||
pair_string = f".{pair[0]}_{pair[1]}"
|
||||
uniq_id_pair = uniq_id + pair_string
|
||||
filename = base_fn + pair_string
|
||||
labels_to_rttmfile(target_rttm, base_fn, filename, output_dir)
|
||||
rttm_path = output_dir + filename + ".rttm"
|
||||
rttmlist.append(rttm_path)
|
||||
line_mod = copy.deepcopy(line)
|
||||
line_mod['rttm_filepath'] = rttm_path
|
||||
meta = copy.deepcopy(audio_rttm_map[uniq_id])
|
||||
meta['rttm_filepath'] = rttm_path
|
||||
rttm_split_manifest_dict[uniq_id_pair] = line_mod
|
||||
split_audio_rttm_map[uniq_id_pair] = meta
|
||||
|
||||
return rttm_split_manifest_dict, split_audio_rttm_map
|
||||
|
||||
|
||||
def main(input_manifest_path, output_manifest_path, pairwise_rttm_output_folder, window, shift, step_count, decimals):
|
||||
|
||||
if '.json' not in input_manifest_path:
|
||||
raise ValueError("input_manifest_path file should be .json file format")
|
||||
if output_manifest_path and '.json' not in output_manifest_path:
|
||||
raise ValueError("output_manifest_path file should be .json file format")
|
||||
elif not output_manifest_path:
|
||||
output_manifest_path = rreplace(input_manifest_path, '.json', f'.{step_count}seg.json')
|
||||
|
||||
if pairwise_rttm_output_folder is not None:
|
||||
if not pairwise_rttm_output_folder.endswith('/'):
|
||||
pairwise_rttm_output_folder = f"{pairwise_rttm_output_folder}/"
|
||||
org_audio_rttm_map = audio_rttm_map(input_manifest_path)
|
||||
input_manifest_dict, AUDIO_RTTM_MAP = split_into_pairwise_rttm(
|
||||
audio_rttm_map=org_audio_rttm_map,
|
||||
input_manifest_path=input_manifest_path,
|
||||
output_dir=pairwise_rttm_output_folder,
|
||||
)
|
||||
else:
|
||||
input_manifest_dict = get_input_manifest_dict(input_manifest_path)
|
||||
AUDIO_RTTM_MAP = audio_rttm_map(input_manifest_path)
|
||||
|
||||
segment_manifest_path = rreplace(input_manifest_path, '.json', '_seg.json')
|
||||
subsegment_manifest_path = rreplace(input_manifest_path, '.json', '_subseg.json')
|
||||
|
||||
# todo: do we need to expose this?
|
||||
min_subsegment_duration = 0.05
|
||||
step_count = int(step_count)
|
||||
|
||||
segments_manifest_file = write_rttm2manifest(AUDIO_RTTM_MAP, segment_manifest_path, decimals)
|
||||
subsegments_manifest_file = subsegment_manifest_path
|
||||
|
||||
logging.info("Creating subsegments.")
|
||||
segments_manifest_to_subsegments_manifest(
|
||||
segments_manifest_file=segments_manifest_file,
|
||||
subsegments_manifest_file=subsegments_manifest_file,
|
||||
window=window,
|
||||
shift=shift,
|
||||
min_subsegment_duration=min_subsegment_duration,
|
||||
include_uniq_id=True,
|
||||
)
|
||||
subsegments_dict = get_subsegment_dict(subsegments_manifest_file, window, shift, decimals)
|
||||
write_truncated_subsegments(input_manifest_dict, subsegments_dict, output_manifest_path, step_count, decimals)
|
||||
os.remove(segment_manifest_path)
|
||||
os.remove(subsegment_manifest_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--input_manifest_path", help="input json file name", type=str, required=True)
|
||||
parser.add_argument(
|
||||
"--output_manifest_path", help="output manifest_file name", type=str, default=None, required=False
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pairwise_rttm_output_folder",
|
||||
help="Save two-speaker pair RTTM files",
|
||||
type=str,
|
||||
default=None,
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument("--window", help="Window length for segmentation", type=float, required=True)
|
||||
parser.add_argument("--shift", help="Shift length for segmentation", type=float, required=True)
|
||||
parser.add_argument("--decimals", help="Rounding decimals", type=int, default=3, required=False)
|
||||
parser.add_argument(
|
||||
"--step_count",
|
||||
help="Number of the unit segments you want to create per utterance",
|
||||
required=True,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(
|
||||
args.input_manifest_path,
|
||||
args.output_manifest_path,
|
||||
args.pairwise_rttm_output_folder,
|
||||
args.window,
|
||||
args.shift,
|
||||
args.step_count,
|
||||
args.decimals,
|
||||
)
|
||||
@@ -0,0 +1,108 @@
|
||||
# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import multiprocessing as mp
|
||||
from itertools import repeat
|
||||
from pathlib import Path
|
||||
|
||||
import librosa
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import read_manifest, write_manifest
|
||||
from nemo.collections.asr.parts.utils.vad_utils import get_frame_labels, load_speech_segments_from_rttm
|
||||
|
||||
"""
|
||||
This script generates a manifest file for synthetic data generated using the NeMo multispeaker speech data simulator.
|
||||
The audio created from the simulator can be used to train a VAD model using the manifest file contains the following fields:
|
||||
The manifest file contains the following fields:
|
||||
|
||||
audio_filepath (str): Path to audio file.
|
||||
offset (float): Offset in seconds for the start of the audio file.
|
||||
duration (float): Duration in seconds for the audio file.
|
||||
text (str): Transcription of the audio file.
|
||||
label (list): List of frame labels for the audio file.
|
||||
orig_sample_rate (int): Original sample rate of the audio file.
|
||||
vad_frame_unit_secs (float): Duration in seconds for each frame label.
|
||||
|
||||
Usage:
|
||||
python build_synthetic_vad_manifest.py \
|
||||
--input_dir /path/to/synthetic/data \
|
||||
--frame_length 0.04 \
|
||||
--output_file /path/to/output/manifest.json
|
||||
"""
|
||||
|
||||
|
||||
def generate_manifest_entry(inputs):
|
||||
"""
|
||||
Generates a manifest entry for a single audio file.
|
||||
This function is parallelized using multiprocessing.Pool.
|
||||
|
||||
Args:
|
||||
inputs (tuple): Tuple containing audio file path and frame length in seconds.
|
||||
inputs[0]:
|
||||
audio_filepath (str): Path to audio file.
|
||||
inputs[1]:
|
||||
vad_frame_unit_secs (float): Duration in seconds for each frame label.
|
||||
|
||||
Returns:
|
||||
entry (dict): Dictionary containing manifest entry.
|
||||
"""
|
||||
audio_filepath, vad_frame_unit_secs = inputs
|
||||
audio_filepath = Path(audio_filepath)
|
||||
y, sr = librosa.load(str(audio_filepath))
|
||||
dur = librosa.get_duration(y=y, sr=sr)
|
||||
|
||||
manifest_path = audio_filepath.parent / Path(f"{audio_filepath.stem}.json")
|
||||
audio_manifest = read_manifest(manifest_path)
|
||||
text = " ".join([x["text"] for x in audio_manifest])
|
||||
|
||||
rttm_path = audio_filepath.parent / Path(f"{audio_filepath.stem}.rttm")
|
||||
segments = load_speech_segments_from_rttm(rttm_path)
|
||||
labels = get_frame_labels(segments, vad_frame_unit_secs, 0.0, dur)
|
||||
|
||||
entry = {
|
||||
"audio_filepath": str(audio_filepath.absolute()),
|
||||
"offset": 0.0,
|
||||
"duration": dur,
|
||||
"text": text,
|
||||
"label": labels,
|
||||
"orig_sample_rate": sr,
|
||||
"vad_frame_unit_secs": vad_frame_unit_secs,
|
||||
}
|
||||
return entry
|
||||
|
||||
|
||||
def main(args):
|
||||
wav_list = list(Path(args.input_dir).glob("*.wav"))
|
||||
print(f"Found {len(wav_list)} in directory: {args.input_dir}")
|
||||
|
||||
inputs = zip(wav_list, repeat(args.frame_length))
|
||||
with mp.Pool(processes=mp.cpu_count()) as pool:
|
||||
manifest_data = list(tqdm(pool.imap(generate_manifest_entry, inputs), total=len(wav_list)))
|
||||
|
||||
write_manifest(args.output_file, manifest_data)
|
||||
print(f"Manifest saved to: {args.output_file}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("input_dir", default=None, help="Path to directory containing synthetic data")
|
||||
parser.add_argument(
|
||||
"-l", "--frame_length", default=0.04, type=float, help="Duration in seconds for each frame label"
|
||||
)
|
||||
parser.add_argument("-o", "--output_file", default=None, help="Path to output manifest file")
|
||||
|
||||
args = parser.parse_args()
|
||||
main(args)
|
||||
@@ -0,0 +1,242 @@
|
||||
# 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 os
|
||||
|
||||
from nemo.collections.asr.metrics.der import evaluate_der
|
||||
from nemo.collections.asr.parts.utils.diarization_utils import OfflineDiarWithASR
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import read_file
|
||||
from nemo.collections.asr.parts.utils.speaker_utils import (
|
||||
get_uniqname_from_filepath,
|
||||
labels_to_supervisions,
|
||||
rttm_to_labels,
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
Evaluation script for diarization with ASR.
|
||||
Calculates Diarization Error Rate (DER) with RTTM files and WER and cpWER with CTM files.
|
||||
In the output ctm_eval.csv file in the output folder,
|
||||
session-level DER, WER, cpWER and speaker counting accuracies are evaluated.
|
||||
|
||||
- Evaluation mode
|
||||
|
||||
diar_eval_mode == "full":
|
||||
DIHARD challenge style evaluation, the most strict way of evaluating diarization
|
||||
(collar, ignore_overlap) = (0.0, False)
|
||||
diar_eval_mode == "fair":
|
||||
Evaluation setup used in VoxSRC challenge
|
||||
(collar, ignore_overlap) = (0.25, False)
|
||||
diar_eval_mode == "forgiving":
|
||||
Traditional evaluation setup
|
||||
(collar, ignore_overlap) = (0.25, True)
|
||||
diar_eval_mode == "all":
|
||||
Compute all three modes (default)
|
||||
|
||||
|
||||
Use CTM files to calculate WER and cpWER
|
||||
```
|
||||
python eval_diar_with_asr.py \
|
||||
--hyp_rttm_list="/path/to/hypothesis_rttm_filepaths.list" \
|
||||
--ref_rttm_list="/path/to/reference_rttm_filepaths.list" \
|
||||
--hyp_ctm_list="/path/to/hypothesis_ctm_filepaths.list" \
|
||||
--ref_ctm_list="/path/to/reference_ctm_filepaths.list" \
|
||||
--root_path="/path/to/output/directory"
|
||||
```
|
||||
|
||||
Use .json files to calculate WER and cpWER
|
||||
```
|
||||
python eval_diar_with_asr.py \
|
||||
--hyp_rttm_list="/path/to/hypothesis_rttm_filepaths.list" \
|
||||
--ref_rttm_list="/path/to/reference_rttm_filepaths.list" \
|
||||
--hyp_json_list="/path/to/hypothesis_json_filepaths.list" \
|
||||
--ref_ctm_list="/path/to/reference_ctm_filepaths.list" \
|
||||
--root_path="/path/to/output/directory"
|
||||
```
|
||||
|
||||
Only use RTTMs to calculate DER
|
||||
```
|
||||
python eval_diar_with_asr.py \
|
||||
--hyp_rttm_list="/path/to/hypothesis_rttm_filepaths.list" \
|
||||
--ref_rttm_list="/path/to/reference_rttm_filepaths.list" \
|
||||
--root_path="/path/to/output/directory"
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def get_supervisions_from_rttms(rttm_file_path_list):
|
||||
"""Generate diarization annotation objects from a list of RTTM files.
|
||||
|
||||
Each entry in the returned list is ``[uniq_id, list[SupervisionSegment]]``.
|
||||
"""
|
||||
annotation_obj_list = []
|
||||
for rttm_file in rttm_file_path_list:
|
||||
rttm_file = rttm_file.strip()
|
||||
if rttm_file is not None and os.path.exists(rttm_file):
|
||||
uniq_id = get_uniqname_from_filepath(rttm_file)
|
||||
ref_labels = rttm_to_labels(rttm_file)
|
||||
reference = labels_to_supervisions(ref_labels, uniq_name=uniq_id)
|
||||
annotation_obj_list.append([uniq_id, reference])
|
||||
return annotation_obj_list
|
||||
|
||||
|
||||
def make_meta_dict(hyp_rttm_list, ref_rttm_list):
|
||||
"""Create a temporary `audio_rttm_map_dict` for evaluation"""
|
||||
meta_dict = {}
|
||||
for k, rttm_file in enumerate(ref_rttm_list):
|
||||
uniq_id = get_uniqname_from_filepath(rttm_file)
|
||||
meta_dict[uniq_id] = {"rttm_filepath": rttm_file.strip()}
|
||||
if hyp_rttm_list is not None:
|
||||
hyp_rttm_file = hyp_rttm_list[k]
|
||||
meta_dict[uniq_id].update({"hyp_rttm_filepath": hyp_rttm_file.strip()})
|
||||
return meta_dict
|
||||
|
||||
|
||||
def make_trans_info_dict(hyp_json_list_path):
|
||||
"""Create `trans_info_dict` from the `.json` files"""
|
||||
trans_info_dict = {}
|
||||
for json_file in hyp_json_list_path:
|
||||
json_file = json_file.strip()
|
||||
with open(json_file) as jsf:
|
||||
json_data = json.load(jsf)
|
||||
uniq_id = get_uniqname_from_filepath(json_file)
|
||||
trans_info_dict[uniq_id] = json_data
|
||||
return trans_info_dict
|
||||
|
||||
|
||||
def read_file_path(list_path):
|
||||
"""Read file path and strip to remove line change symbol"""
|
||||
return sorted([x.strip() for x in read_file(list_path)])
|
||||
|
||||
|
||||
def main(
|
||||
hyp_rttm_list_path: str,
|
||||
ref_rttm_list_path: str,
|
||||
hyp_ctm_list_path: str,
|
||||
ref_ctm_list_path: str,
|
||||
hyp_json_list_path: str,
|
||||
diar_eval_mode: str = "all",
|
||||
root_path: str = "./",
|
||||
):
|
||||
|
||||
# Read filepath list files
|
||||
hyp_rttm_list = read_file_path(hyp_rttm_list_path) if hyp_rttm_list_path else None
|
||||
ref_rttm_list = read_file_path(ref_rttm_list_path) if ref_rttm_list_path else None
|
||||
hyp_ctm_list = read_file_path(hyp_ctm_list_path) if hyp_ctm_list_path else None
|
||||
ref_ctm_list = read_file_path(ref_ctm_list_path) if ref_ctm_list_path else None
|
||||
hyp_json_list = read_file_path(hyp_json_list_path) if hyp_json_list_path else None
|
||||
|
||||
audio_rttm_map_dict = make_meta_dict(hyp_rttm_list, ref_rttm_list)
|
||||
|
||||
trans_info_dict = make_trans_info_dict(hyp_json_list) if hyp_json_list else None
|
||||
|
||||
all_hypothesis = get_supervisions_from_rttms(hyp_rttm_list)
|
||||
all_reference = get_supervisions_from_rttms(ref_rttm_list)
|
||||
|
||||
diar_score = evaluate_der(
|
||||
audio_rttm_map_dict=audio_rttm_map_dict,
|
||||
all_reference=all_reference,
|
||||
all_hypothesis=all_hypothesis,
|
||||
diar_eval_mode=diar_eval_mode,
|
||||
)
|
||||
|
||||
# Get session-level diarization error rate and speaker counting error
|
||||
der_results = OfflineDiarWithASR.gather_eval_results(
|
||||
diar_score=diar_score,
|
||||
audio_rttm_map_dict=audio_rttm_map_dict,
|
||||
trans_info_dict=trans_info_dict,
|
||||
root_path=root_path,
|
||||
)
|
||||
|
||||
if ref_ctm_list is not None:
|
||||
# Calculate WER and cpWER if reference CTM files exist
|
||||
if hyp_ctm_list is not None:
|
||||
wer_results = OfflineDiarWithASR.evaluate(
|
||||
audio_file_list=hyp_rttm_list,
|
||||
hyp_trans_info_dict=None,
|
||||
hyp_ctm_file_list=hyp_ctm_list,
|
||||
ref_ctm_file_list=ref_ctm_list,
|
||||
)
|
||||
elif hyp_json_list is not None:
|
||||
wer_results = OfflineDiarWithASR.evaluate(
|
||||
audio_file_list=hyp_rttm_list,
|
||||
hyp_trans_info_dict=trans_info_dict,
|
||||
hyp_ctm_file_list=None,
|
||||
ref_ctm_file_list=ref_ctm_list,
|
||||
)
|
||||
else:
|
||||
raise ValueError("Hypothesis information is not provided in the correct format.")
|
||||
else:
|
||||
wer_results = {}
|
||||
|
||||
# Print average DER, WER and cpWER
|
||||
OfflineDiarWithASR.print_errors(der_results=der_results, wer_results=wer_results)
|
||||
|
||||
# Save detailed session-level evaluation results in `root_path`.
|
||||
OfflineDiarWithASR.write_session_level_result_in_csv(
|
||||
der_results=der_results,
|
||||
wer_results=wer_results,
|
||||
root_path=root_path,
|
||||
csv_columns=OfflineDiarWithASR.get_csv_columns(),
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--hyp_rttm_list", help="path to the filelist of hypothesis RTTM files", type=str, required=True, default=None
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ref_rttm_list", help="path to the filelist of reference RTTM files", type=str, required=True, default=None
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hyp_ctm_list", help="path to the filelist of hypothesis CTM files", type=str, required=False, default=None
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ref_ctm_list", help="path to the filelist of reference CTM files", type=str, required=False, default=None
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hyp_json_list",
|
||||
help="(Optional) path to the filelist of hypothesis JSON files",
|
||||
type=str,
|
||||
required=False,
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--diar_eval_mode",
|
||||
help='evaluation mode: "all", "full", "fair", "forgiving"',
|
||||
type=str,
|
||||
required=False,
|
||||
default="all",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--root_path", help='directory for saving result files', type=str, required=False, default="./"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
main(
|
||||
args.hyp_rttm_list,
|
||||
args.ref_rttm_list,
|
||||
args.hyp_ctm_list,
|
||||
args.ref_ctm_list,
|
||||
args.hyp_json_list,
|
||||
args.diar_eval_mode,
|
||||
args.root_path,
|
||||
)
|
||||
@@ -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.
|
||||
|
||||
"""
|
||||
This script converts a filelist file where each line contains
|
||||
<absolute path of wav file> to a manifest json file.
|
||||
Optionally post processes the manifest file to create dev and train split for speaker embedding
|
||||
training, also optionally segment an audio file in to segments of random DURATIONS and create those
|
||||
wav files in CWD.
|
||||
|
||||
Args:
|
||||
--filelist: path to file containing list of audio files
|
||||
--manifest(optional): if you already have manifest file, but would like to process it for creating
|
||||
segments and splitting then use manifest ignoring filelist
|
||||
--id: index of speaker label in filename present in filelist file that is separated by '/'
|
||||
--out: output manifest file name
|
||||
--split: 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
|
||||
--create_segments: if you would want to segment each manifest line to segments of [1,2,3,4] sec or less
|
||||
you may not need this for test set, defaults to False
|
||||
--min_spkrs_count: min number of samples per speaker to consider and ignore otherwise, defaults to 0 (all speakers)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
|
||||
import librosa as l
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
from sklearn.model_selection import StratifiedShuffleSplit
|
||||
from tqdm.contrib.concurrent import process_map
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import read_manifest
|
||||
|
||||
random.seed(42)
|
||||
|
||||
DURATIONS = sorted([3], reverse=True)
|
||||
MIN_ENERGY = 0.01
|
||||
CWD = os.getcwd()
|
||||
|
||||
|
||||
def _load_sox():
|
||||
try:
|
||||
import sox
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Optional dependency 'sox' is required by this script. Install it with: pip install sox"
|
||||
) from None
|
||||
|
||||
return sox
|
||||
|
||||
|
||||
def filter_manifest_line(manifest_line):
|
||||
split_manifest = []
|
||||
audio_path = manifest_line['audio_filepath']
|
||||
start = manifest_line.get('offset', 0)
|
||||
dur = manifest_line['duration']
|
||||
label = manifest_line['label']
|
||||
endname = os.path.splitext(audio_path.split(label, 1)[-1])[0]
|
||||
to_path = os.path.join(CWD, 'segments', label)
|
||||
to_path = os.path.join(to_path, endname[1:])
|
||||
os.makedirs(os.path.dirname(to_path), exist_ok=True)
|
||||
|
||||
if dur >= min(DURATIONS):
|
||||
signal, sr = sf.read(audio_path)
|
||||
remaining_dur = dur - start
|
||||
|
||||
segments = DURATIONS.copy()
|
||||
mode = int(remaining_dur // sum(DURATIONS))
|
||||
rem = remaining_dur % sum(DURATIONS)
|
||||
segments = mode * segments
|
||||
|
||||
for val in DURATIONS:
|
||||
if rem >= val:
|
||||
segments.append(val)
|
||||
rem = rem - val
|
||||
|
||||
for temp_dur in segments:
|
||||
segment_audio = signal[int(start * sr) : int(start * sr + temp_dur * sr)]
|
||||
if l.feature.rms(y=segment_audio).mean() > MIN_ENERGY:
|
||||
final_string = '_' + str(start) + '_' + str(temp_dur)
|
||||
final_string = final_string.replace('.', '-')
|
||||
to_file = to_path + final_string + '.wav'
|
||||
|
||||
c_start = int(float(start * sr))
|
||||
c_end = c_start + int(float(temp_dur * sr))
|
||||
segment = signal[c_start:c_end]
|
||||
sf.write(to_file, segment, sr)
|
||||
|
||||
meta = manifest_line.copy()
|
||||
meta['audio_filepath'] = to_file
|
||||
meta['offset'] = 0
|
||||
meta['duration'] = temp_dur
|
||||
split_manifest.append(meta)
|
||||
|
||||
start = start + temp_dur
|
||||
|
||||
return split_manifest
|
||||
|
||||
|
||||
def count_and_consider_only(speakers, lines, min_count=10):
|
||||
"""
|
||||
consider speakers only if samples per speaker is at least min_count
|
||||
"""
|
||||
uniq_speakers, indices, counts = np.unique(speakers, return_index=True, return_counts=True)
|
||||
print("speaker count before filtering minimum number of speaker counts: ", len(uniq_speakers))
|
||||
required_speakers = {}
|
||||
for idx, count in enumerate(counts):
|
||||
if count >= min_count:
|
||||
required_speakers[uniq_speakers[idx]] = count
|
||||
|
||||
print("speaker count after filtering minimum number of speaker counts: ", len(required_speakers))
|
||||
required_lines = []
|
||||
speakers_only = []
|
||||
for idx, speaker in enumerate(speakers):
|
||||
if speaker in required_speakers:
|
||||
required_lines.append(lines[idx])
|
||||
speakers_only.append(speaker)
|
||||
|
||||
return speakers_only, required_lines
|
||||
|
||||
|
||||
def write_file(name, lines, idx):
|
||||
with open(name, 'w', encoding='utf-8') as fout:
|
||||
for i in idx:
|
||||
dic = lines[i]
|
||||
json.dump(dic, fout)
|
||||
fout.write('\n')
|
||||
print("wrote", name)
|
||||
|
||||
|
||||
def read_file(filelist, id=-1):
|
||||
json_lines = []
|
||||
with open(filelist, 'r') as fo:
|
||||
lines = fo.readlines()
|
||||
lines = sorted(lines)
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
speaker = line.split('/')[id]
|
||||
speaker = list(speaker)
|
||||
speaker = ''.join(speaker)
|
||||
meta = {"audio_filepath": line, "offset": 0, "duration": None, "label": speaker}
|
||||
json_lines.append(meta)
|
||||
return json_lines
|
||||
|
||||
|
||||
def get_duration(json_line):
|
||||
dur = json_line['duration']
|
||||
if dur is None:
|
||||
sox = _load_sox()
|
||||
wav_path = json_line['audio_filepath']
|
||||
json_line['duration'] = sox.file_info.duration(wav_path)
|
||||
return json_line
|
||||
|
||||
|
||||
def get_labels(lines):
|
||||
labels = []
|
||||
for line in lines:
|
||||
label = line['label']
|
||||
labels.append(label)
|
||||
return labels
|
||||
|
||||
|
||||
def main(filelist, manifest, id, out, split=False, create_segments=False, min_count=10):
|
||||
if os.path.exists(out):
|
||||
os.remove(out)
|
||||
if filelist:
|
||||
lines = read_file(filelist=filelist, id=id)
|
||||
lines = process_map(get_duration, lines, chunksize=100)
|
||||
out_file = os.path.splitext(filelist)[0] + '_manifest.json'
|
||||
write_file(out_file, lines, range(len(lines)))
|
||||
else:
|
||||
lines = read_manifest(manifest)
|
||||
|
||||
lines = process_map(get_duration, lines, chunksize=100)
|
||||
|
||||
if create_segments:
|
||||
print(f"creating and writing segments to {CWD}")
|
||||
lines = process_map(filter_manifest_line, lines, chunksize=100)
|
||||
temp = []
|
||||
for line in lines:
|
||||
temp.extend(line)
|
||||
del lines
|
||||
lines = temp
|
||||
|
||||
speakers = [x['label'] for x in lines]
|
||||
|
||||
if min_count:
|
||||
speakers, lines = count_and_consider_only(speakers, lines, abs(min_count))
|
||||
|
||||
write_file(out, lines, range(len(lines)))
|
||||
path = os.path.dirname(out)
|
||||
if split:
|
||||
speakers = [x['label'] for x in lines]
|
||||
sss = StratifiedShuffleSplit(n_splits=1, test_size=0.1, random_state=42)
|
||||
for train_idx, test_idx in sss.split(speakers, speakers):
|
||||
print("number of train samples after split: ", 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("--filelist", help="path to filelist file", type=str, required=False, default=None)
|
||||
parser.add_argument("--manifest", help="manifest file name", type=str, required=False, default=None)
|
||||
parser.add_argument(
|
||||
"--id",
|
||||
help="field num seperated by '/' to be considered as speaker label from filelist file, can be ignored if manifest file is already provided with labels",
|
||||
type=int,
|
||||
required=False,
|
||||
default=None,
|
||||
)
|
||||
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',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--create_segments",
|
||||
help="bool if you would want to segment each manifest line to segments of 4 sec or less",
|
||||
required=False,
|
||||
action='store_true',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--min_spkrs_count",
|
||||
default=0,
|
||||
type=int,
|
||||
help="min number of samples per speaker to consider and ignore otherwise",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
main(
|
||||
args.filelist,
|
||||
args.manifest,
|
||||
args.id,
|
||||
args.out,
|
||||
args.split,
|
||||
args.create_segments,
|
||||
args.min_spkrs_count,
|
||||
)
|
||||
@@ -0,0 +1,301 @@
|
||||
# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import multiprocessing
|
||||
import shutil
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
from typing import Dict
|
||||
|
||||
import numpy as np
|
||||
from scipy.stats import expon
|
||||
from tqdm import tqdm
|
||||
|
||||
from nemo.collections.asr.parts.utils.vad_utils import (
|
||||
get_nonspeech_segments,
|
||||
load_speech_overlap_segments_from_rttm,
|
||||
plot_sample_from_rttm,
|
||||
)
|
||||
from nemo.utils.dependency import import_optional_dependency
|
||||
|
||||
"""
|
||||
This script analyzes multi-speaker speech dataset and generates statistics.
|
||||
The input directory </path/to/rttm_and_wav_directory> is required to contain the following files:
|
||||
- rttm files (*.rttm)
|
||||
- wav files (*.wav)
|
||||
|
||||
Usage:
|
||||
python <NEMO_ROOT>/scripts/speaker_tasks/multispeaker_data_analysis.py \
|
||||
</path/to/rttm_and_wav_directory> \
|
||||
--session_dur 20 \
|
||||
--silence_mean 0.2 \
|
||||
--silence_var 100 \
|
||||
--overlap_mean 0.15 \
|
||||
--overlap_var 50 \
|
||||
--num_workers 8 \
|
||||
--num_samples 10 \
|
||||
--output_dir <path/to/output_directory>
|
||||
"""
|
||||
|
||||
|
||||
def process_sample(sess_dict: Dict) -> Dict:
|
||||
"""
|
||||
Process each synthetic sample
|
||||
|
||||
Args:
|
||||
sess_dict (dict): dictionary containing the following keys
|
||||
rttm_file (str): path to the rttm file
|
||||
session_dur (float): duration of the session (specified by argument)
|
||||
precise (bool): whether to measure the precise duration of the session using sox
|
||||
|
||||
Returns:
|
||||
results (dict): dictionary containing the following keys
|
||||
session_dur (float): duration of the session
|
||||
silence_len_list (list): list of silence durations of each silence occurrence
|
||||
silence_dur (float): total silence duration in a session
|
||||
silence_ratio (float): ratio of silence duration to session duration
|
||||
overlap_len_list (list): list of overlap durations of each overlap occurrence
|
||||
overlap_dur (float): total overlap duration
|
||||
overlap_ratio (float): ratio of overlap duration to speech (non-silence) duration
|
||||
"""
|
||||
|
||||
rttm_file = sess_dict["rttm_file"]
|
||||
session_dur = sess_dict["session_dur"]
|
||||
precise = sess_dict["precise"]
|
||||
if precise or session_dur is None:
|
||||
sox = import_optional_dependency("sox")
|
||||
wav_file = rttm_file.parent / Path(rttm_file.stem + ".wav")
|
||||
session_dur = sox.file_info.duration(str(wav_file))
|
||||
|
||||
speech_seg, overlap_seg = load_speech_overlap_segments_from_rttm(rttm_file)
|
||||
speech_dur = sum([sess_dict[1] - sess_dict[0] for sess_dict in speech_seg])
|
||||
|
||||
silence_seg = get_nonspeech_segments(speech_seg, session_dur)
|
||||
silence_len_list = [sess_dict[1] - sess_dict[0] for sess_dict in silence_seg]
|
||||
silence_dur = max(0, session_dur - speech_dur)
|
||||
silence_ratio = silence_dur / session_dur
|
||||
|
||||
overlap_len_list = [sess_dict[1] - sess_dict[0] for sess_dict in overlap_seg]
|
||||
overlap_dur = sum(overlap_len_list) if len(overlap_len_list) else 0
|
||||
overlap_ratio = overlap_dur / speech_dur
|
||||
|
||||
results = {
|
||||
"session_dur": session_dur,
|
||||
"silence_len_list": silence_len_list,
|
||||
"silence_dur": silence_dur,
|
||||
"silence_ratio": silence_ratio,
|
||||
"overlap_len_list": overlap_len_list,
|
||||
"overlap_dur": overlap_dur,
|
||||
"overlap_ratio": overlap_ratio,
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def run_multispeaker_data_analysis(
|
||||
input_dir,
|
||||
session_dur=None,
|
||||
silence_mean=None,
|
||||
silence_var=None,
|
||||
overlap_mean=None,
|
||||
overlap_var=None,
|
||||
precise=False,
|
||||
save_path=None,
|
||||
num_workers=1,
|
||||
) -> Dict:
|
||||
rttm_list = list(Path(input_dir).glob("*.rttm"))
|
||||
"""
|
||||
Analyze the multispeaker data and plot the distribution of silence and overlap durations.
|
||||
|
||||
Args:
|
||||
input_dir (str): path to the directory containing the rttm files
|
||||
session_dur (float): duration of the session (specified by argument)
|
||||
silence_mean (float): mean of the silence duration distribution
|
||||
silence_var (float): variance of the silence duration distribution
|
||||
overlap_mean (float): mean of the overlap duration distribution
|
||||
overlap_var (float): variance of the overlap duration distribution
|
||||
precise (bool): whether to measure the precise duration of the session using sox
|
||||
save_path (str): path to save the plots
|
||||
|
||||
Returns:
|
||||
stats (dict): dictionary containing the statistics of the analyzed data
|
||||
"""
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
sns = import_optional_dependency("seaborn")
|
||||
|
||||
print(f"Found {len(rttm_list)} files to be processed")
|
||||
if len(rttm_list) == 0:
|
||||
raise ValueError(f"No rttm files found in {input_dir}")
|
||||
|
||||
silence_duration = 0.0
|
||||
total_duration = 0.0
|
||||
overlap_duration = 0.0
|
||||
|
||||
silence_ratio_all = []
|
||||
overlap_ratio_all = []
|
||||
silence_length_all = []
|
||||
overlap_length_all = []
|
||||
|
||||
queue = []
|
||||
for rttm_file in tqdm(rttm_list):
|
||||
queue.append(
|
||||
{
|
||||
"rttm_file": rttm_file,
|
||||
"session_dur": session_dur,
|
||||
"precise": precise,
|
||||
}
|
||||
)
|
||||
|
||||
if num_workers <= 1:
|
||||
results = [process_sample(sess_dict) for sess_dict in tqdm(queue)]
|
||||
else:
|
||||
with multiprocessing.Pool(processes=num_workers) as p:
|
||||
results = list(
|
||||
tqdm(
|
||||
p.imap(process_sample, queue),
|
||||
total=len(queue),
|
||||
desc='Processing',
|
||||
leave=True,
|
||||
)
|
||||
)
|
||||
|
||||
for item in results:
|
||||
total_duration += item["session_dur"]
|
||||
silence_duration += item["silence_dur"]
|
||||
overlap_duration += item["overlap_dur"]
|
||||
|
||||
silence_length_all += item["silence_len_list"]
|
||||
overlap_length_all += item["overlap_len_list"]
|
||||
|
||||
silence_ratio_all.append(item["silence_ratio"])
|
||||
overlap_ratio_all.append(item["overlap_ratio"])
|
||||
|
||||
actual_silence_mean = silence_duration / total_duration
|
||||
actual_silence_var = np.var(silence_ratio_all)
|
||||
actual_overlap_mean = overlap_duration / (total_duration - silence_duration)
|
||||
actual_overlap_var = np.var(overlap_ratio_all)
|
||||
|
||||
stats = OrderedDict()
|
||||
stats["total duration (hours)"] = f"{total_duration / 3600:.2f}"
|
||||
stats["number of sessions"] = len(rttm_list)
|
||||
stats["average session duration (seconds)"] = f"{total_duration / len(rttm_list):.2f}"
|
||||
stats["actual silence ratio mean/var"] = f"{actual_silence_mean:.4f}/{actual_silence_var:.4f}"
|
||||
stats["actual overlap ratio mean/var"] = f"{actual_overlap_mean:.4f}/{actual_overlap_var:.4f}"
|
||||
stats["expected silence ratio mean/var"] = f"{silence_mean}/{silence_var}"
|
||||
stats["expected overlap ratio mean/var"] = f"{overlap_mean}/{overlap_var}"
|
||||
stats["save_path"] = save_path
|
||||
|
||||
print("-----------------------------------------------")
|
||||
print(" Results ")
|
||||
print("-----------------------------------------------")
|
||||
for k, v in stats.items():
|
||||
print(k, ": ", v)
|
||||
print("-----------------------------------------------")
|
||||
|
||||
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(14, 14))
|
||||
fig.suptitle(
|
||||
f"Average session={total_duration/len(rttm_list):.2f} seconds, num sessions={len(rttm_list)}, total={total_duration/3600:.2f} hours"
|
||||
)
|
||||
sns.histplot(silence_ratio_all, ax=ax1)
|
||||
ax1.set_xlabel("Silence ratio in a session")
|
||||
ax1.set_title(
|
||||
f"Target silence mean={silence_mean}, var={silence_var}. \nActual silence ratio={actual_silence_mean:.4f}, var={actual_silence_var:.4f}"
|
||||
)
|
||||
|
||||
_, scale = expon.fit(silence_length_all, floc=0)
|
||||
sns.histplot(silence_length_all, ax=ax2)
|
||||
ax2.set_xlabel("Per-silence length in seconds")
|
||||
ax2.set_title(f"Per-silence length histogram, \nfitted exponential distribution with mean={scale:.4f}")
|
||||
|
||||
sns.histplot(overlap_ratio_all, ax=ax3)
|
||||
ax3.set_title(
|
||||
f"Target overlap mean={overlap_mean}, var={overlap_var}. \nActual ratio={actual_overlap_mean:.4f}, var={actual_overlap_var:.4f}"
|
||||
)
|
||||
ax3.set_xlabel("Overlap ratio in a session")
|
||||
_, scale2 = expon.fit(overlap_length_all, floc=0)
|
||||
sns.histplot(overlap_length_all, ax=ax4)
|
||||
ax4.set_title(f"Per overlap length histogram, \nfitted exponential distribution with mean={scale2:.4f}")
|
||||
ax4.set_xlabel("Duration in seconds")
|
||||
|
||||
if save_path:
|
||||
fig.savefig(save_path)
|
||||
print(f"Figure saved at: {save_path}")
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def visualize_multispeaker_data(input_dir: str, output_dir: str, num_samples: int = 10) -> None:
|
||||
"""
|
||||
Visualize a set of randomly sampled data in the input directory
|
||||
|
||||
Args:
|
||||
input_dir (str): Path to the input directory
|
||||
output_dir (str): Path to the output directory
|
||||
num_samples (int): Number of samples to visualize
|
||||
"""
|
||||
rttm_list = list(Path(input_dir).glob("*.rttm"))
|
||||
idx_list = np.random.permutation(len(rttm_list))[:num_samples]
|
||||
print(f"Visualizing {num_samples} random samples")
|
||||
for idx in idx_list:
|
||||
rttm_file = rttm_list[idx]
|
||||
audio_file = rttm_file.parent / Path(rttm_file.stem + ".wav")
|
||||
output_file = Path(output_dir) / Path(rttm_file.stem + ".png")
|
||||
plot_sample_from_rttm(audio_file=audio_file, rttm_file=rttm_file, save_path=str(output_file), show=False)
|
||||
print(f"Sample plots saved at: {output_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("input_dir", default="", help="Input directory")
|
||||
parser.add_argument("-sd", "--session_dur", default=None, type=float, help="Duration per session in seconds")
|
||||
parser.add_argument("-sm", "--silence_mean", default=None, type=float, help="Expected silence ratio mean")
|
||||
parser.add_argument("-sv", "--silence_var", default=None, type=float, help="Expected silence ratio variance")
|
||||
parser.add_argument("-om", "--overlap_mean", default=None, type=float, help="Expected overlap ratio mean")
|
||||
parser.add_argument("-ov", "--overlap_var", default=None, type=float, help="Expected overlap ratio variance")
|
||||
parser.add_argument("-w", "--num_workers", default=1, type=int, help="Number of CPU workers to use")
|
||||
parser.add_argument("-s", "--num_samples", default=10, type=int, help="Number of random samples to plot")
|
||||
parser.add_argument("-o", "--output_dir", default="analysis/", type=str, help="Directory for saving output figure")
|
||||
parser.add_argument(
|
||||
"--precise", action="store_true", help="Set to get precise duration, with significant time cost"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print("Running with params:")
|
||||
pprint(vars(args))
|
||||
|
||||
output_dir = Path(args.output_dir)
|
||||
if output_dir.exists():
|
||||
print(f"Removing existing output directory: {args.output_dir}")
|
||||
shutil.rmtree(str(output_dir))
|
||||
output_dir.mkdir(parents=True)
|
||||
|
||||
run_multispeaker_data_analysis(
|
||||
input_dir=args.input_dir,
|
||||
session_dur=args.session_dur,
|
||||
silence_mean=args.silence_mean,
|
||||
silence_var=args.silence_var,
|
||||
overlap_mean=args.overlap_mean,
|
||||
overlap_var=args.overlap_var,
|
||||
precise=args.precise,
|
||||
save_path=str(Path(args.output_dir, "statistics.png")),
|
||||
num_workers=args.num_workers,
|
||||
)
|
||||
|
||||
visualize_multispeaker_data(input_dir=args.input_dir, output_dir=args.output_dir, num_samples=args.num_samples)
|
||||
|
||||
print("The multispeaker data analysis has been completed.")
|
||||
print(f"Please check the output directory: \n{args.output_dir}")
|
||||
@@ -0,0 +1,69 @@
|
||||
# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import random
|
||||
|
||||
from nemo.collections.asr.parts.utils.manifest_utils import create_manifest
|
||||
|
||||
random.seed(42)
|
||||
|
||||
"""
|
||||
This script creates manifest file for speaker diarization inference purposes.
|
||||
Useful to get manifest when you have list of audio files and optionally rttm and uem files for evaluation
|
||||
|
||||
Note: make sure basename for each file is unique and rttm files also has the corresponding base name for mapping
|
||||
"""
|
||||
|
||||
|
||||
def main(
|
||||
wav_path, text_path=None, rttm_path=None, uem_path=None, ctm_path=None, manifest_filepath=None, add_duration=False
|
||||
):
|
||||
create_manifest(
|
||||
wav_path,
|
||||
manifest_filepath,
|
||||
text_path=text_path,
|
||||
rttm_path=rttm_path,
|
||||
uem_path=uem_path,
|
||||
ctm_path=ctm_path,
|
||||
add_duration=add_duration,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--paths2audio_files", help="path to text file containing list of audio files", type=str, required=True
|
||||
)
|
||||
parser.add_argument("--paths2txt_files", help="path to text file containing list of transcription files", type=str)
|
||||
parser.add_argument("--paths2rttm_files", help="path to text file containing list of rttm files", type=str)
|
||||
parser.add_argument("--paths2uem_files", help="path to uem files", type=str)
|
||||
parser.add_argument("--paths2ctm_files", help="path to ctm files", type=str)
|
||||
parser.add_argument("--manifest_filepath", help="path to output manifest file", type=str, required=True)
|
||||
parser.add_argument(
|
||||
"--add_duration",
|
||||
help="add duration of audio files to output manifest files.",
|
||||
action='store_true',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(
|
||||
args.paths2audio_files,
|
||||
args.paths2txt_files,
|
||||
args.paths2rttm_files,
|
||||
args.paths2uem_files,
|
||||
args.paths2ctm_files,
|
||||
args.manifest_filepath,
|
||||
args.add_duration,
|
||||
)
|
||||
Reference in New Issue
Block a user