Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:28:58 +08:00

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)