ba4be087d5
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
508 lines
20 KiB
Python
508 lines
20 KiB
Python
# 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.
|
|
from typing import TYPE_CHECKING, Dict, List, Optional
|
|
|
|
import torch
|
|
|
|
from nemo.collections.asr.parts.preprocessing.feature_loader import ExternalFeatureLoader
|
|
from nemo.collections.common.parts.preprocessing import collections
|
|
from nemo.core.classes import Dataset
|
|
from nemo.core.neural_types import AcousticEncodedRepresentation, LabelsType, LengthsType, NeuralType
|
|
from nemo.utils import logging
|
|
|
|
if TYPE_CHECKING:
|
|
from nemo.collections.asr.parts.preprocessing.perturb import AudioAugmentor
|
|
|
|
|
|
def _feature_collate_fn(batch):
|
|
"""collate batch of feat sig, feat len, labels, labels len, assuming all features have the same shape.
|
|
Args:
|
|
batch (FloatTensor, LongTensor, LongTensor, LongTensor): A tuple of tuples of feature, feature lengths,
|
|
encoded labels, and encoded labels length.
|
|
"""
|
|
packed_batch = list(zip(*batch))
|
|
if len(packed_batch) == 5:
|
|
_, feat_lengths, _, labels_lengths, sample_ids = packed_batch
|
|
elif len(packed_batch) == 4:
|
|
sample_ids = None
|
|
_, feat_lengths, _, labels_lengths = packed_batch
|
|
else:
|
|
raise ValueError("Expects 4 or 5 tensors in the batch!")
|
|
|
|
features, labels = [], []
|
|
for b in batch:
|
|
feat_i, labels_i = b[0], b[2]
|
|
features.append(feat_i)
|
|
labels.append(labels_i)
|
|
|
|
features = torch.stack(features)
|
|
feat_lengths = torch.stack(feat_lengths)
|
|
|
|
labels = torch.stack(labels)
|
|
labels_lengths = torch.stack(labels_lengths)
|
|
|
|
if sample_ids is None:
|
|
return features, feat_lengths, labels, labels_lengths
|
|
else:
|
|
sample_ids = torch.tensor(sample_ids, dtype=torch.int32)
|
|
return features, feat_lengths, labels, labels_lengths, sample_ids
|
|
|
|
|
|
def _audio_feature_collate_fn(batch, feat_pad_val, label_pad_id):
|
|
"""collate batch of audio feature, audio len, labels, labels len
|
|
Args:
|
|
batch (Optional[FloatTensor], Optional[LongTensor], LongTensor,
|
|
LongTensor): A tuple of tuples of feature, feature lengths,
|
|
labels, and label lengths. This collate func assumes the
|
|
features are torch tensors of Log-Melspectrogram (i.e. [N_MEL, T]).
|
|
"""
|
|
packed_batch = list(zip(*batch))
|
|
if len(packed_batch) == 5:
|
|
_, feat_lengths, _, labels_lengths, sample_ids = packed_batch
|
|
elif len(packed_batch) == 4:
|
|
sample_ids = None
|
|
_, feat_lengths, _, labels_lengths = packed_batch
|
|
else:
|
|
raise ValueError("Expects 4 or 5 tensors in the batch!")
|
|
max_feat_len = 0
|
|
has_feat = feat_lengths[0] is not None
|
|
if has_feat:
|
|
max_feat_len = max(feat_lengths).item()
|
|
max_labels_len = max(labels_lengths).item()
|
|
|
|
features, labels = [], []
|
|
for b in batch:
|
|
feat_i, feat_i_len, label_i, label_i_len = b[0], b[1], b[2], b[3]
|
|
|
|
if has_feat:
|
|
feat_i_len = feat_i_len.item()
|
|
if feat_i_len < max_feat_len:
|
|
pad = (0, max_feat_len - feat_i_len)
|
|
feat_i = torch.nn.functional.pad(feat_i, pad, value=feat_pad_val)
|
|
features.append(feat_i)
|
|
|
|
label_i_len = label_i_len.item()
|
|
if label_i_len < max_labels_len:
|
|
pad = (0, max_labels_len - label_i_len)
|
|
label_i = torch.nn.functional.pad(label_i, pad, value=label_pad_id)
|
|
labels.append(label_i)
|
|
|
|
if has_feat:
|
|
features = torch.stack(features)
|
|
feature_lengths = torch.stack(feat_lengths)
|
|
else:
|
|
features, feat_lengths = None, None
|
|
labels = torch.stack(labels)
|
|
labels_lengths = torch.stack(labels_lengths)
|
|
|
|
if sample_ids is None:
|
|
return features, feature_lengths, labels, labels_lengths
|
|
else:
|
|
sample_ids = torch.tensor(sample_ids, dtype=torch.int32)
|
|
return features, feature_lengths, labels, labels_lengths, sample_ids
|
|
|
|
|
|
def _vad_feature_segment_collate_fn(batch, window_length_in_sec, shift_length_in_sec, frame_unit_in_sec):
|
|
"""collate batch of audio features, features len, tokens, tokens len
|
|
Args:
|
|
batch (Optional[FloatTensor], Optional[LongTensor], LongTensor,
|
|
LongTensor): A tuple of tuples of signal, signal lengths,
|
|
encoded tokens, and encoded tokens length. This collate func
|
|
assumes the signals are 1d torch tensors (i.e. mono audio).
|
|
batch size equals to 1.
|
|
"""
|
|
slice_length = int(window_length_in_sec / frame_unit_in_sec)
|
|
audio_features, feat_lengths, _, tokens_lengths = zip(*batch)
|
|
|
|
slice_length = int(min(slice_length, max(feat_lengths)))
|
|
shift = int(shift_length_in_sec / frame_unit_in_sec)
|
|
has_audio = feat_lengths[0] is not None
|
|
|
|
f_dim = audio_features[0].shape[0]
|
|
audio_features, num_slices, tokens, feat_lengths = [], [], [], []
|
|
append_len_start = torch.div(slice_length, 2, rounding_mode='trunc')
|
|
append_len_end = slice_length - torch.div(slice_length, 2, rounding_mode='trunc')
|
|
for feat_i, feat_i_len, tokens_i, _ in batch:
|
|
start = torch.zeros(f_dim, append_len_start)
|
|
end = torch.zeros(f_dim, append_len_end)
|
|
feat_i = torch.cat((start, feat_i, end), dim=1)
|
|
feat_i_len += slice_length
|
|
|
|
if has_audio:
|
|
slices = max(1, torch.div(feat_i_len - slice_length, shift, rounding_mode='trunc'))
|
|
|
|
for slice_id in range(slices):
|
|
start_idx = slice_id * shift
|
|
end_idx = start_idx + slice_length
|
|
feat_slice = feat_i[:, start_idx:end_idx]
|
|
audio_features.append(feat_slice)
|
|
|
|
num_slices.append(slices)
|
|
tokens.extend([tokens_i] * slices)
|
|
feat_lengths.extend([slice_length] * slices)
|
|
|
|
if has_audio:
|
|
audio_features = torch.stack(audio_features)
|
|
feat_lengths = torch.tensor(feat_lengths)
|
|
else:
|
|
audio_features, feat_lengths = None, None
|
|
|
|
tokens = torch.stack(tokens)
|
|
tokens_lengths = torch.tensor(num_slices)
|
|
return audio_features, feat_lengths, tokens, tokens_lengths
|
|
|
|
|
|
class _FeatureSeqSpeakerLabelDataset(Dataset):
|
|
"""
|
|
Dataset that loads tensors via a json file containing paths to feature files, sequences of labels.
|
|
Each new line is a different sample. Example below:
|
|
and their target labels. JSON files should be of the following format:
|
|
{"feature_filepath": "/path/to/feature_0.p", "seq_label": speakerA speakerB SpeakerA ....} \
|
|
...
|
|
{"feature_filepath": "/path/to/feature_n.p", "seq_label": target_seq_label_n}
|
|
target_seq_label_n is the string of sequence of speaker label, separated by space.
|
|
|
|
Args:
|
|
manifest_filepath (str): Dataset parameter. Path to JSON containing data.
|
|
labels (Optional[list]): Dataset parameter. List of unique labels collected from all samples.
|
|
feature_loader : Dataset parameter. Feature loader to load (external) feature.
|
|
"""
|
|
|
|
@property
|
|
def output_types(self) -> Optional[Dict[str, NeuralType]]:
|
|
"""Returns definitions of module output ports."""
|
|
# TODO output type for external features
|
|
output_types = {
|
|
'external_feat': NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()),
|
|
'feat_length': NeuralType(tuple('B'), LengthsType()),
|
|
}
|
|
|
|
if self.is_speaker_emb:
|
|
output_types.update(
|
|
{
|
|
'embs': NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()),
|
|
'embs_length': NeuralType(tuple('B'), LengthsType()),
|
|
'label': NeuralType(('B', 'T'), LabelsType()),
|
|
'label_length': NeuralType(tuple('B'), LengthsType()),
|
|
}
|
|
)
|
|
else:
|
|
output_types.update(
|
|
{
|
|
'label': NeuralType(('B', 'T'), LabelsType()),
|
|
'label_length': NeuralType(tuple('B'), LengthsType()),
|
|
}
|
|
)
|
|
|
|
return output_types
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
manifest_filepath: str,
|
|
labels: List[str],
|
|
feature_loader,
|
|
is_speaker_emb: bool = False,
|
|
):
|
|
super().__init__()
|
|
self.collection = collections.ASRFeatureSequenceLabel(
|
|
manifests_files=manifest_filepath.split(','),
|
|
)
|
|
|
|
self.feature_loader = feature_loader
|
|
self.labels = labels if labels else self.collection.uniq_labels
|
|
self.is_speaker_emb = is_speaker_emb
|
|
|
|
self.label2id, self.id2label = {}, {}
|
|
for label_id, label in enumerate(self.labels):
|
|
self.label2id[label] = label_id
|
|
self.id2label[label_id] = label
|
|
|
|
for idx in range(len(self.labels[:5])):
|
|
logging.debug(" label id {} and its mapped label {}".format(idx, self.id2label[idx]))
|
|
|
|
def __len__(self):
|
|
return len(self.collection)
|
|
|
|
def __getitem__(self, index):
|
|
sample = self.collection[index]
|
|
|
|
features = self.feature_loader.process(sample.feature_file)
|
|
f, fl = features, torch.tensor(features.shape[0]).long()
|
|
|
|
t = torch.tensor(sample.seq_label).float()
|
|
tl = torch.tensor(len(sample.seq_label)).long()
|
|
|
|
return f, fl, t, tl
|
|
|
|
|
|
class FeatureToSeqSpeakerLabelDataset(_FeatureSeqSpeakerLabelDataset):
|
|
"""
|
|
Dataset that loads tensors via a json file containing paths to feature
|
|
files and sequence of speakers. Each new line is a
|
|
different sample. Example below:
|
|
{"feature_filepath": "/path/to/feature_0.p", "seq_label": speakerA speakerB SpeakerA ....} \
|
|
...
|
|
{"feature_filepath": "/path/to/feature_n.p", "seq_label": target_seq_label_n}
|
|
target_seq_label_n is the string of sequence of speaker label, separated by space.
|
|
|
|
Args:
|
|
manifest_filepath (str): Path to manifest json as described above. Canbe comma-separated paths.
|
|
labels (Optional[list]): String containing all the possible labels to map to
|
|
if None then automatically picks from ASRFeatureSequenceLabel collection.
|
|
feature_loader, Feature load to loader (external) feature.
|
|
|
|
"""
|
|
|
|
def _collate_fn(self, batch):
|
|
return _feature_collate_fn(batch)
|
|
|
|
|
|
class FeatureToLabelDataset(Dataset):
|
|
"""
|
|
Dataset that loads tensors via a json file containing paths to feature files and their labels.
|
|
Each new line is a different sample. Example below:
|
|
and their target labels. JSON files should be of the following format:
|
|
{"feature_filepath": "/path/to/audio_feature.pt", "label": "1"}
|
|
...
|
|
{"feature_filepath": "/path/to/audio_feature.pt", "label": "0"}
|
|
Args:
|
|
manifest_filepath (str): Path to JSON containing data.
|
|
labels (Optional[list]): List of unique labels collected from all samples.
|
|
augmentor (Optional): feature augmentation
|
|
window_length_in_sec (float): Window length in seconds.
|
|
shift_length_in_sec (float): Shift length in seconds.
|
|
is_regression_task (bool): if True, the labels are treated as for a regression task.
|
|
cal_labels_occurrence (bool): if True, the labels occurrence will be calculated.
|
|
zero_spec_db_val (float): Value to replace non-speech signals in log-melspectrogram.
|
|
min_duration (float): Minimum duration of the audio file in seconds.
|
|
max_duration (float): Maximum duration of the audio file in seconds.
|
|
"""
|
|
|
|
ZERO_LEVEL_SPEC_DB_VAL = -16.635 # Log-Melspectrogram value for zero signal
|
|
FRAME_UNIT_TIME_SECS = 0.01
|
|
|
|
@property
|
|
def output_types(self) -> Optional[Dict[str, NeuralType]]:
|
|
"""Returns definitions of module output ports."""
|
|
output_types = {
|
|
'audio_feat': NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()),
|
|
'feat_length': NeuralType(tuple('B'), LengthsType()),
|
|
'labels': NeuralType(('B'), LabelsType()),
|
|
'labels_length': NeuralType(tuple('B'), LengthsType()),
|
|
}
|
|
|
|
return output_types
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
manifest_filepath: str,
|
|
labels: List[str] = None,
|
|
augmentor: 'AudioAugmentor' = None,
|
|
window_length_in_sec: float = 0.63,
|
|
shift_length_in_sec: float = 0.01,
|
|
is_regression_task: bool = False,
|
|
cal_labels_occurrence: Optional[bool] = False,
|
|
zero_spec_db_val: float = -16.635,
|
|
min_duration: Optional[float] = None,
|
|
max_duration: Optional[float] = None,
|
|
):
|
|
super().__init__()
|
|
self.window_length_in_sec = window_length_in_sec
|
|
self.shift_length_in_sec = shift_length_in_sec
|
|
self.zero_spec_db_val = zero_spec_db_val
|
|
|
|
if isinstance(manifest_filepath, str):
|
|
manifest_filepath = manifest_filepath.split(',')
|
|
|
|
self.collection = collections.ASRFeatureLabel(
|
|
manifests_files=manifest_filepath,
|
|
is_regression_task=is_regression_task,
|
|
cal_labels_occurrence=cal_labels_occurrence,
|
|
min_duration=min_duration,
|
|
max_duration=max_duration,
|
|
)
|
|
|
|
self.feature_loader = ExternalFeatureLoader(augmentor=augmentor)
|
|
self.labels = labels if labels else self.collection.uniq_labels
|
|
|
|
self.is_regression_task = is_regression_task
|
|
|
|
if not is_regression_task:
|
|
self.labels = labels if labels else self.collection.uniq_labels
|
|
self.num_classes = len(self.labels) if self.labels is not None else 1
|
|
self.label2id, self.id2label = {}, {}
|
|
self.id2occurrence, self.labels_occurrence = {}, []
|
|
|
|
for label_id, label in enumerate(self.labels):
|
|
self.label2id[label] = label_id
|
|
self.id2label[label_id] = label
|
|
if cal_labels_occurrence:
|
|
self.id2occurrence[label_id] = self.collection.labels_occurrence[label]
|
|
|
|
if cal_labels_occurrence:
|
|
self.labels_occurrence = [self.id2occurrence[k] for k in sorted(self.id2occurrence)]
|
|
|
|
for idx in range(len(self.labels[:5])):
|
|
logging.debug(" label id {} and its mapped label {}".format(idx, self.id2label[idx]))
|
|
else:
|
|
self.labels = []
|
|
self.num_classes = 1
|
|
|
|
def __len__(self):
|
|
return len(self.collection)
|
|
|
|
def __getitem__(self, index):
|
|
sample = self.collection[index]
|
|
|
|
features = self.feature_loader.process(sample.feature_file)
|
|
f, fl = features, torch.tensor(features.shape[1]).long()
|
|
|
|
t = torch.tensor(self.label2id[sample.label])
|
|
tl = torch.tensor(1).long()
|
|
|
|
return f, fl, t, tl
|
|
|
|
def _collate_fn(self, batch):
|
|
return _audio_feature_collate_fn(batch, self.zero_spec_db_val, 0)
|
|
|
|
def _vad_segment_collate_fn(self, batch):
|
|
return _vad_feature_segment_collate_fn(
|
|
batch, self.window_length_in_sec, self.shift_length_in_sec, self.FRAME_UNIT_TIME_SECS
|
|
)
|
|
|
|
|
|
class FeatureToMultiLabelDataset(Dataset):
|
|
"""
|
|
Dataset that loads tensors via a json file containing paths to feature files and their labels.
|
|
Each new line is a different sample. Example below:
|
|
and their target labels. JSON files should be of the following format:
|
|
{"feature_filepath": "/path/to/audio_feature.pt", "label": "1 1 0 0 1"}
|
|
...
|
|
{"feature_filepath": "/path/to/audio_feature.pt", "label": "0 1 0 0"}
|
|
Args:
|
|
manifest_filepath (str): Path to JSON containing data.
|
|
labels (Optional[list]): List of unique labels collected from all samples.
|
|
augmentor (Optional): feature augmentation
|
|
delimiter (str): delimiter to split the labels.
|
|
is_regression_task (bool): if True, the labels are treated as for a regression task.
|
|
cal_labels_occurrence (bool): if True, the labels occurrence will be calculated.
|
|
zero_spec_db_val (float): Value to replace non-speech signals in log-melspectrogram.
|
|
min_duration (float): Minimum duration of the audio file in seconds.
|
|
max_duration (float): Maximum duration of the audio file in seconds.
|
|
"""
|
|
|
|
ZERO_LEVEL_SPEC_DB_VAL = -16.635 # Log-Melspectrogram value for zero signal
|
|
|
|
@property
|
|
def output_types(self) -> Optional[Dict[str, NeuralType]]:
|
|
"""Returns definitions of module output ports."""
|
|
output_types = {
|
|
'audio_feat': NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()),
|
|
'feat_length': NeuralType(tuple('B'), LengthsType()),
|
|
'labels': NeuralType(('B', 'T'), LabelsType()),
|
|
'labels_length': NeuralType(tuple('B'), LengthsType()),
|
|
}
|
|
|
|
return output_types
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
manifest_filepath: str,
|
|
labels: List[str] = None,
|
|
augmentor: 'AudioAugmentor' = None,
|
|
delimiter: Optional[str] = None,
|
|
is_regression_task: bool = False,
|
|
cal_labels_occurrence: Optional[bool] = False,
|
|
zero_spec_db_val: float = -16.635,
|
|
min_duration: Optional[float] = None,
|
|
max_duration: Optional[float] = None,
|
|
):
|
|
super().__init__()
|
|
self.delimiter = delimiter
|
|
self.zero_spec_db_val = zero_spec_db_val
|
|
|
|
if isinstance(manifest_filepath, str):
|
|
manifest_filepath = manifest_filepath.split(',')
|
|
|
|
self.collection = collections.ASRFeatureLabel(
|
|
manifests_files=manifest_filepath,
|
|
is_regression_task=is_regression_task,
|
|
cal_labels_occurrence=cal_labels_occurrence,
|
|
delimiter=delimiter,
|
|
min_duration=min_duration,
|
|
max_duration=max_duration,
|
|
)
|
|
|
|
self.is_regression_task = is_regression_task
|
|
self.feature_loader = ExternalFeatureLoader(augmentor=augmentor)
|
|
self.labels = labels if labels else self.collection.uniq_labels
|
|
|
|
self.label2id, self.id2label = {}, {}
|
|
if not is_regression_task:
|
|
self.labels = labels if labels else self._get_label_set()
|
|
self.num_classes = len(self.labels) if self.labels is not None else 1
|
|
self.label2id, self.id2label = {}, {}
|
|
for label_id, label in enumerate(self.labels):
|
|
self.label2id[label] = label_id
|
|
self.id2label[label_id] = label
|
|
if cal_labels_occurrence:
|
|
self.id2occurrence[label_id] = self.collection.labels_occurrence[label]
|
|
self.labels_occurrence.append(self.id2occurrence[label_id])
|
|
|
|
for idx in range(len(self.labels[:5])):
|
|
logging.debug(" label id {} and its mapped label {}".format(idx, self.id2label[idx]))
|
|
else:
|
|
self.labels = []
|
|
self.num_classes = 1
|
|
|
|
def _get_label_set(self):
|
|
labels = []
|
|
for sample in self.collection:
|
|
label_str = sample.label
|
|
if label_str:
|
|
label_str_list = label_str.split(self.delimiter) if self.delimiter else label_str.split()
|
|
labels.extend(label_str_list)
|
|
return sorted(set(labels))
|
|
|
|
def _label_str_to_tensor(self, label_str: str):
|
|
labels = label_str.split(self.delimiter) if self.delimiter else label_str.split()
|
|
|
|
if self.is_regression_task:
|
|
labels = [float(s) for s in labels]
|
|
labels = torch.tensor(labels).float()
|
|
else:
|
|
labels = [self.label2id[s] for s in labels]
|
|
labels = torch.tensor(labels).long()
|
|
return labels
|
|
|
|
def __len__(self):
|
|
return len(self.collection)
|
|
|
|
def __getitem__(self, index):
|
|
sample = self.collection[index]
|
|
|
|
features = self.feature_loader.process(sample.feature_file)
|
|
f, fl = features, torch.tensor(features.shape[1]).long()
|
|
|
|
t = self._label_str_to_tensor(sample.label)
|
|
tl = torch.tensor(t.size(0)).long()
|
|
|
|
return f, fl, t, tl
|
|
|
|
def _collate_fn(self, batch):
|
|
return _audio_feature_collate_fn(batch, self.zero_spec_db_val, 0)
|