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

488 lines
22 KiB
Python

# 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.
from typing import Callable, Dict, List, Optional, Tuple, Union
import torch
from nemo.collections.asr.data.feature_to_label import _audio_feature_collate_fn
from nemo.collections.asr.parts.preprocessing.feature_loader import ExternalFeatureLoader
from nemo.collections.asr.parts.preprocessing.features import normalize_batch
from nemo.collections.asr.parts.preprocessing.segment import ChannelSelectorType
from nemo.collections.asr.parts.utils.vad_utils import load_speech_segments_from_rttm
from nemo.collections.common import tokenizers
from nemo.collections.common.parts.preprocessing import collections, parsers
from nemo.core.classes import Dataset
from nemo.core.neural_types import AcousticEncodedRepresentation, LabelsType, LengthsType, NeuralType
class ASRFeatureManifestProcessor:
def __init__(
self,
manifest_filepath: str,
parser: Union[str, Callable],
max_duration: Optional[float] = None,
min_duration: Optional[float] = None,
max_utts: int = 0,
bos_id: Optional[int] = None,
eos_id: Optional[int] = None,
pad_id: int = 0,
index_by_file_id: bool = False,
):
self.parser = parser
self.collection = collections.ASRFeatureText(
manifests_files=manifest_filepath,
parser=parser,
min_duration=min_duration,
max_duration=max_duration,
max_number=max_utts,
index_by_file_id=index_by_file_id,
)
self.eos_id = eos_id
self.bos_id = bos_id
self.pad_id = pad_id
def process_text_by_id(self, index: int) -> Tuple[List[int], int]:
sample = self.collection[index]
return self.process_text_by_sample(sample)
def process_text_by_file_id(self, file_id: str) -> Tuple[List[int], int]:
manifest_idx = self.collection.mapping[file_id][0]
sample = self.collection[manifest_idx]
return self.process_text_by_sample(sample)
def process_text_by_sample(self, sample: collections.ASRAudioText.OUTPUT_TYPE) -> Tuple[List[int], int]:
t, tl = sample.text_tokens, len(sample.text_tokens)
if self.bos_id is not None:
t = [self.bos_id] + t
tl += 1
if self.eos_id is not None:
t = t + [self.eos_id]
tl += 1
return t, tl
class _FeatureTextDataset(Dataset):
"""
Dataset that loads tensors via a json file containing paths to audio feature files, transcripts,
durations (in seconds) and optional RTTM files. Each new line is a different sample. Example below:
{"feature_filepath": "/path/to/audio_feature.pt", "text_filepath": "/path/to/audio.txt",
"rttm_filepath": "/path/to/audio_rttm.rttm", "duration": 23.147}
...
{"feature_filepath": "/path/to/audio_feature.pt", "text": "the transcription", "offset": 301.75, "duration": 0.82, "utt":
"utterance_id", "ctm_utt": "en_4156", "side": "A"}
Args:
manifest_filepath (str): Path to manifest json as described above. Can be comma-separated paths.
parser: Str for a language specific preprocessor or a callable.
normalize (bool): whether and where to normalize feature, must be one of [None, "post_norm", "pre_norm"]
normalize_type (Union[str, dict]): how to normalize feature, see `nemo.collections.asr.parts.preprocessing.features.normalize_batch`
use_rttm (bool): whether to use RTTM files if there is any, default to False
rttm_mode (str): how to use RTTM files, must be one of ['mask', 'drop'], default to 'mask'
feat_min_len (int): minimum length of feature when rttm_mode=deop, default to 4.
feat_mask_val (Optional[float]): value used to mask features with RTTM files, default to None to use zero mel-spectralgram
frame_unit_time_secs (float): time in seconds for each frame
sample_rate (int): Sample rate to resample loaded audio to
int_values (bool): If true, load samples as 32-bit integers. Defauts to False.
augmentor (nemo.collections.asr.parts.perturb.AudioAugmentor): An AudioAugmentor object used to augment loaded audio
max_duration (float): If audio exceeds this length, do not include in dataset
min_duration (float): If audio is less than this length, do not include in dataset
max_utts (int): Limit number of utterances
trim (bool): whether or not to trim silence. Defaults to False
bos_id (int): Id of beginning of sequence symbol to append if not None
eos_id (int): Id of end of sequence symbol to append if not None
pad_id (int): Id of pad symbol. Defaults to 0
return_sample_id (bool): whether to return the sample_id as a part of each sample
channel_selector (int | Iterable[int] | str): select a single channel or a subset of channels from multi-channel audio. If set to `'average'`, it performs averaging across channels. Disabled if set to `None`. Defaults to `None`. Uses zero-based indexing.
"""
ZERO_LEVEL_SPEC_DB_VAL = -16.635 # Log-Melspectrogram value for zero signal
NORM_MODES = ["pre_norm", "post_norm"]
RTTM_MODES = ["mask", "drop"]
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""Returns definitions of module output ports."""
return {
'features': NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()),
'feature_length': NeuralType(tuple('B'), LengthsType()),
'transcripts': NeuralType(('B', 'T'), LabelsType()),
'transcript_length': NeuralType(tuple('B'), LengthsType()),
'sample_id': NeuralType(tuple('B'), LengthsType(), optional=True),
}
def __init__(
self,
manifest_filepath: str,
parser: Union[str, Callable],
normalize: Optional[str] = "post_norm",
normalize_type: Union[str, dict] = "per_feature",
use_rttm: bool = False,
rttm_mode: str = "mask",
feat_min_len: int = 4,
feat_mask_val: Optional[float] = None,
frame_unit_time_secs: float = 0.01,
sample_rate: Optional[int] = 16000,
augmentor: 'nemo.collections.asr.parts.perturb.FeatureAugmentor' = None,
max_duration: Optional[int] = None,
min_duration: Optional[int] = None,
max_utts: int = 0,
trim: bool = False,
bos_id: Optional[int] = None,
eos_id: Optional[int] = None,
pad_id: int = 0,
return_sample_id: bool = False,
channel_selector: Optional[ChannelSelectorType] = None,
):
if type(manifest_filepath) == str:
manifest_filepath = manifest_filepath.split(",")
self.sample_rate = sample_rate
self.normalize = normalize
self.normalize_type = normalize_type
self.use_rttm = use_rttm
self.rttm_mode = rttm_mode
if self.use_rttm and self.rttm_mode not in self.RTTM_MODES:
raise ValueError(f"`rttm_mode` must be one of {self.RTTM_MODES}, got `{rttm_mode}` instead")
self.feat_min_len = feat_min_len
if feat_mask_val is not None:
self.feat_mask_val = feat_mask_val
elif normalize == "pre_norm":
self.feat_mask_val = 0.0 # similar to SpectralAugmentation
else:
self.feat_mask_val = self.ZERO_LEVEL_SPEC_DB_VAL
if normalize is not None and normalize not in self.NORM_MODES:
raise ValueError(f"`normalize` must be one of {self.NORM_MODES}, got `{normalize}` instead")
self.frame_unit_time_secs = frame_unit_time_secs
self.manifest_processor = ASRFeatureManifestProcessor(
manifest_filepath=manifest_filepath,
parser=parser,
max_duration=max_duration,
min_duration=min_duration,
max_utts=max_utts,
bos_id=bos_id,
eos_id=eos_id,
pad_id=pad_id,
)
self.featurizer = ExternalFeatureLoader(augmentor=augmentor)
self.trim = trim
self.return_sample_id = return_sample_id
self.channel_selector = channel_selector
def get_manifest_sample(self, sample_id):
return self.manifest_processor.collection[sample_id]
def __getitem__(self, index):
sample = self.manifest_processor.collection[index]
offset = sample.offset
if offset is None:
offset = 0
features = self.featurizer.process(sample.feature_file)
f, fl = features, torch.tensor(features.shape[1]).long()
t, tl = self.manifest_processor.process_text_by_sample(sample=sample)
# Feature normalization
if self.normalize is None:
if self.use_rttm and sample.rttm_file:
f = self.process_features_with_rttm(f, offset, sample.rttm_file, self.feat_mask_val)
elif self.normalize == "post_norm":
# (Optional) Masking based on RTTM file
if self.use_rttm and sample.rttm_file:
f = self.process_features_with_rttm(f, offset, sample.rttm_file, self.feat_mask_val)
f = self.normalize_feature(f)
else: # pre-norm
f = self.normalize_feature(f)
# (Optional) Masking based on RTTM file
if self.use_rttm and sample.rttm_file:
f = self.process_features_with_rttm(f, offset, sample.rttm_file, self.feat_mask_val)
if self.return_sample_id:
output = f, fl, torch.tensor(t).long(), torch.tensor(tl).long(), index
else:
output = f, fl, torch.tensor(t).long(), torch.tensor(tl).long()
return output
def process_features_with_rttm(self, features, offset, rttm_file, mask_val):
segments = load_speech_segments_from_rttm(rttm_file)
new_features = features.clone()
sid, fid = 0, 0
for i in range(features.size(1)):
t = offset + i * self.frame_unit_time_secs
while sid < len(segments) - 1 and segments[sid][1] < t:
sid += 1
if segments[sid][1] == 0 or t < segments[sid][0] or t > segments[sid][1]:
# not in speech segment
if self.rttm_mode == "drop":
# drop the frame
continue
else:
# mask the frame with specified value
new_features[:, i] = mask_val
fid += 1
else:
# in speech segment
new_features[:, fid] = features[:, i]
fid += 1
if fid < self.feat_min_len and self.rttm_mode == "drop":
new_features[:, : self.feat_min_len] = mask_val
return new_features[:, : self.feat_min_len]
return new_features[:, :fid]
def __len__(self):
return len(self.manifest_processor.collection)
def _collate_fn(self, batch):
return _audio_feature_collate_fn(
batch, feat_pad_val=self.feat_mask_val, label_pad_id=self.manifest_processor.pad_id
)
def normalize_feature(self, feat):
"""
Args:
feat: feature tensor of shape [M, T]
"""
feat = feat.unsqueeze(0) # add batch dim
feat, _, _ = normalize_batch(feat, torch.tensor([feat.size(-1)]), self.normalize_type)
return feat.squeeze(0) # delete batch dim
class FeatureToCharDataset(_FeatureTextDataset):
"""
Dataset that loads tensors via a json file containing paths to audio feature
files, transcripts, durations (in seconds) and optional RTTM files. Each new line is a
different sample. Example below:
{"feature_filepath": "/path/to/audio_feature.pt", "text_filepath":
"/path/to/audio.txt", "duration": 23.147, "rttm_filepath": "/path/to/audio_rttm.rttm",}
...
{"feature_filepath": "/path/to/audio_feature.pt", "text": "the
transcription", "offset": 301.75, "duration": 0.82, "utt":
"utterance_id", "ctm_utt": "en_4156", "side": "A"}
Args:
manifest_filepath (str): Path to manifest json as described above. Can
be comma-separated paths.
labels (str): String containing all the possible characters to map to
normalize (str): how to normalize feature, must be one of [None, "post_norm", "pre_norm"]
normalize_type (Union[str, dict]): how to normalize feature, see `nemo.collections.asr.parts.preprocessing.features.normalize_batch`
use_rttm (bool): whether to use RTTM files if there is any, default to False
rttm_mode (str): how to use RTTM files, must be one of ['mask', 'drop'], default to 'mask'
feat_min_len (int): minimum length of feature, default to 4
feat_mask_val (Optional[float]): value used to mask features with RTTM files, default to None to use zero mel-spectralgram
frame_unit_time_secs: time in seconds for each frame
sample_rate (int): Sample rate to resample loaded audio to
int_values (bool): If true, load samples as 32-bit integers. Defauts to False.
augmentor (nemo.collections.asr.parts.perturb.AudioAugmentor): An AudioAugmentor
object used to augment loaded audio
max_duration: If audio exceeds this length, do not include in dataset
min_duration: If audio is less than this length, do not include
in dataset
max_utts: Limit number of utterances
blank_index: blank character index, default = -1
unk_index: unk_character index, default = -1
bos_id: Id of beginning of sequence symbol to append if not None
eos_id: Id of end of sequence symbol to append if not None
return_sample_id (bool): whether to return the sample_id as a part of each sample
channel_selector (int | Iterable[int] | str): select a single channel or a subset of channels from multi-channel audio. If set to `'average'`, it performs averaging across channels. Disabled if set to `None`. Defaults to `None`. Uses zero-based indexing.
"""
def __init__(
self,
manifest_filepath: str,
labels: Union[str, List[str]],
normalize: Optional[str] = "post_norm",
normalize_type: Union[str, dict] = "per_feature",
use_rttm: bool = False,
rttm_mode: str = "mask",
feat_min_len: int = 4,
feat_mask_val: Optional[float] = None,
frame_unit_time_secs: float = 0.01,
sample_rate: Optional[int] = 16000,
augmentor: 'nemo.collections.asr.parts.perturb.FeatureAugmentor' = None,
max_duration: Optional[int] = None,
min_duration: Optional[int] = None,
max_utts: int = 0,
blank_index: int = -1,
unk_index: int = -1,
trim: bool = False,
bos_id: Optional[int] = None,
eos_id: Optional[int] = None,
pad_id: int = 0,
parser: Union[str, Callable] = 'en',
return_sample_id: bool = False,
channel_selector: Optional[ChannelSelectorType] = None,
):
self.labels = labels
parser = parsers.make_parser(
labels=labels, name=parser, unk_id=unk_index, blank_id=blank_index, do_normalize=normalize
)
super().__init__(
manifest_filepath=manifest_filepath,
parser=parser,
normalize=normalize,
normalize_type=normalize_type,
use_rttm=use_rttm,
rttm_mode=rttm_mode,
feat_min_len=feat_min_len,
feat_mask_val=feat_mask_val,
frame_unit_time_secs=frame_unit_time_secs,
sample_rate=sample_rate,
augmentor=augmentor,
max_duration=max_duration,
min_duration=min_duration,
max_utts=max_utts,
trim=trim,
bos_id=bos_id,
eos_id=eos_id,
pad_id=pad_id,
return_sample_id=return_sample_id,
channel_selector=channel_selector,
)
class FeatureToBPEDataset(_FeatureTextDataset):
"""
Dataset that loads tensors via a json file containing paths to audio feature
files, transcripts, durations (in seconds) and optional RTTM files. Each new line is a different sample.
Example below:
{"audio_filepath": "/path/to/audio.wav", "text_filepath":
"/path/to/audio.txt", "duration": 23.147, "rttm_filepath": "/path/to/audio_rttm.rttm",}
...
{"audio_filepath": "/path/to/audio.wav", "text": "the
transcription", "offset": 301.75, "duration": 0.82, "utt":
"utterance_id", "ctm_utt": "en_4156", "side": "A"}
In practice, the dataset and manifest used for character encoding and byte pair encoding
are exactly the same. The only difference lies in how the dataset tokenizes the text in
the manifest.
Args:
manifest_filepath (str): Path to manifest json as described above. Can
be comma-separated paths.
tokenizer: A subclass of the Tokenizer wrapper found in the common collection,
nemo.collections.common.tokenizers.TokenizerSpec. ASR Models support a subset of
all available tokenizers.
normalize (str): how to normalize feature, must be one of [None, "post_norm", "pre_norm"]
normalize_type (Union[str, dict]): how to normalize feature, see `nemo.collections.asr.parts.preprocessing.features.normalize_batch`
use_rttm (bool): whether to use RTTM files if there is any, default to False
rttm_mode (str): how to use RTTM files, must be one of ['mask', 'drop'], default to 'mask'
feat_min_len (int): minimum length of feature, default to 4
feat_mask_val (Optional[float]): value used to mask features with RTTM files, default to None to use zero mel-spectralgram
frame_unit_time_secs: time in seconds for each frame
sample_rate (int): Sample rate to resample loaded audio to
int_values (bool): If true, load samples as 32-bit integers. Defauts to False.
augmentor (nemo.collections.asr.parts.perturb.AudioAugmentor): An AudioAugmentor
object used to augment loaded audio
max_duration: If audio exceeds this length, do not include in dataset
min_duration: If audio is less than this length, do not include
in dataset
max_utts: Limit number of utterances
trim: Whether to trim silence segments
use_start_end_token: Boolean which dictates whether to add [BOS] and [EOS]
tokens to beginning and ending of speech respectively.
return_sample_id (bool): whether to return the sample_id as a part of each sample
channel_selector (int | Iterable[int] | str): select a single channel or a subset of channels from multi-channel audio. If set to `'average'`, it performs averaging across channels. Disabled if set to `None`. Defaults to `None`. Uses zero-based indexing.
"""
def __init__(
self,
manifest_filepath: str,
tokenizer: 'nemo.collections.common.tokenizers.TokenizerSpec',
normalize: Optional[str] = "post_norm",
normalize_type: Union[str, dict] = "per_feature",
use_rttm: bool = False,
rttm_mode: str = "mask",
feat_min_len: int = 4,
feat_mask_val: Optional[float] = None,
frame_unit_time_secs: float = 0.01,
sample_rate: Optional[int] = 16000,
augmentor: 'nemo.collections.asr.parts.perturb.FeatureAugmentor' = None,
max_duration: Optional[int] = None,
min_duration: Optional[int] = None,
max_utts: int = 0,
use_start_end_token: bool = True,
trim: bool = False,
return_sample_id: bool = False,
channel_selector: Optional[ChannelSelectorType] = None,
):
if use_start_end_token and hasattr(tokenizer, "bos_id") and tokenizer.bos_id > 0:
bos_id = tokenizer.bos_id
else:
bos_id = None
if use_start_end_token and hasattr(tokenizer, "eos_id") and tokenizer.eos_id > 0:
eos_id = tokenizer.eos_id
else:
eos_id = None
if hasattr(tokenizer, "pad_id") and tokenizer.pad_id > 0:
pad_id = tokenizer.pad_id
else:
pad_id = 0
class TokenizerWrapper:
def __init__(self, tokenizer):
if isinstance(tokenizer, tokenizers.aggregate_tokenizer.AggregateTokenizer):
self.is_aggregate = True
else:
self.is_aggregate = False
self._tokenizer = tokenizer
def __call__(self, *args):
if isinstance(args[0], List) and self.is_aggregate:
t = []
for span in args[0]:
t.extend(self._tokenizer.text_to_ids(span['str'], span['lang']))
return t
t = self._tokenizer.text_to_ids(*args)
return t
super().__init__(
manifest_filepath=manifest_filepath,
parser=TokenizerWrapper(tokenizer),
normalize=normalize,
normalize_type=normalize_type,
use_rttm=use_rttm,
rttm_mode=rttm_mode,
feat_min_len=feat_min_len,
feat_mask_val=feat_mask_val,
frame_unit_time_secs=frame_unit_time_secs,
sample_rate=sample_rate,
augmentor=augmentor,
max_duration=max_duration,
min_duration=min_duration,
max_utts=max_utts,
trim=trim,
bos_id=bos_id,
eos_id=eos_id,
pad_id=pad_id,
return_sample_id=return_sample_id,
channel_selector=channel_selector,
)