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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:28:58 +08:00
commit ba4be087d5
2316 changed files with 2668701 additions and 0 deletions
@@ -0,0 +1,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)
+242
View File
@@ -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,
)