# 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 io import json import math import multiprocessing import os from collections.abc import Iterable as IterableABC from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union import braceexpand import numpy as np import torch from torch.utils.data import ChainDataset from tqdm import tqdm from nemo.collections.asr.parts.preprocessing.features import WaveformFeaturizer from nemo.collections.asr.parts.preprocessing.segment import ChannelSelectorType from nemo.collections.asr.parts.preprocessing.segment import available_formats as valid_sf_formats from nemo.collections.common import tokenizers from nemo.collections.common.parts.preprocessing import collections, parsers from nemo.core.classes import Dataset, IterableDataset from nemo.core.neural_types import * from nemo.utils import logging from nemo.utils import webdataset as wds from nemo.utils.data_utils import DataStoreObject, datastore_object_get, is_datastore_cache_shared, is_datastore_path from nemo.utils.decorators import deprecated from nemo.utils.distributed import webdataset_split_by_workers from nemo.utils.get_rank import is_global_rank_zero __all__ = [ 'AudioToCharDataset', 'AudioToBPEDataset', 'TarredAudioToCharDataset', 'TarredAudioToBPEDataset', ] VALID_FILE_FORMATS = ';'.join(['wav', 'mp3', 'flac', 'opus'] + [fmt.lower() for fmt in valid_sf_formats.keys()]) def _speech_collate_fn(batch, pad_id): """collate batch of audio sig, audio 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). """ packed_batch = list(zip(*batch)) if len(packed_batch) == 5: _, audio_lengths, _, tokens_lengths, sample_ids = packed_batch elif len(packed_batch) == 4: sample_ids = None _, audio_lengths, _, tokens_lengths = packed_batch else: raise ValueError("Expects 4 or 5 tensors in the batch!") max_audio_len = 0 has_audio = audio_lengths[0] is not None if has_audio: max_audio_len = max(audio_lengths).item() has_tokens = tokens_lengths[0] is not None if has_tokens: max_tokens_len = max(tokens_lengths).item() audio_signal, tokens = [], [] for b in batch: if len(b) == 5: sig, sig_len, tokens_i, tokens_i_len, _ = b else: sig, sig_len, tokens_i, tokens_i_len = b if has_audio: sig_len = sig_len.item() if sig_len < max_audio_len: pad = (0, max_audio_len - sig_len) sig = torch.nn.functional.pad(sig, pad) audio_signal.append(sig) if has_tokens: tokens_i_len = tokens_i_len.item() if tokens_i_len < max_tokens_len: pad = (0, max_tokens_len - tokens_i_len) tokens_i = torch.nn.functional.pad(tokens_i, pad, value=pad_id) tokens.append(tokens_i) if has_audio: audio_signal = torch.stack(audio_signal) audio_lengths = torch.stack(audio_lengths) else: audio_signal, audio_lengths = None, None if has_tokens: tokens = torch.stack(tokens) tokens_lengths = torch.stack(tokens_lengths) else: tokens = None tokens_lengths = None if sample_ids is None: return audio_signal, audio_lengths, tokens, tokens_lengths else: sample_ids = torch.tensor(sample_ids, dtype=torch.int32) return audio_signal, audio_lengths, tokens, tokens_lengths, sample_ids class ASRManifestProcessor: """ Class that processes a manifest json file containing paths to audio files, transcripts, and durations (in seconds). Each new line is a different sample. Example below: {"audio_filepath": "/path/to/audio.wav", "text_filepath": "/path/to/audio.txt", "duration": 23.147} ... {"audio_filepath": "/path/to/audio.wav", "text": "the transcription", "offset": 301.75, "duration": 0.82, "utt": "utterance_id", "ctm_utt": "en_4156", "side": "A"} Args: manifest_filepath: Path to manifest json as described above. Can be comma-separated paths. parser: Str for a language specific preprocessor or a callable. 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. 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. pad_id: Id of pad symbol. Defaults to 0. """ 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, manifest_parse_func: Optional[Callable] = None, ): self.parser = parser self.collection = collections.ASRAudioText( 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, parse_func=manifest_parse_func, ) 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 def expand_sharded_filepaths(sharded_filepaths, shard_strategy: str, world_size: int, global_rank: int): valid_shard_strategies = ['scatter', 'replicate'] if shard_strategy not in valid_shard_strategies: raise ValueError(f"`shard_strategy` must be one of {valid_shard_strategies}") if isinstance(sharded_filepaths, str): # Replace '(' and '[' with '{' brace_keys_open = ['(', '[', '<', '_OP_'] for bkey in brace_keys_open: if bkey in sharded_filepaths: sharded_filepaths = sharded_filepaths.replace(bkey, "{") # Replace ')' and ']' with '}' brace_keys_close = [')', ']', '>', '_CL_'] for bkey in brace_keys_close: if bkey in sharded_filepaths: sharded_filepaths = sharded_filepaths.replace(bkey, "}") if isinstance(sharded_filepaths, str): # Brace expand, set escape=False for Windows compatibility sharded_filepaths = list(braceexpand.braceexpand(sharded_filepaths, escape=False)) # Check for distributed and partition shards accordingly if world_size > 1: if shard_strategy == 'scatter': logging.info("All tarred dataset shards will be scattered evenly across all nodes.") if len(sharded_filepaths) % world_size != 0: logging.warning( f"Number of shards in tarred dataset ({len(sharded_filepaths)}) is not divisible " f"by number of distributed workers ({world_size})." ) begin_idx = (len(sharded_filepaths) // world_size) * global_rank end_idx = begin_idx + len(sharded_filepaths) // world_size sharded_filepaths = sharded_filepaths[begin_idx:end_idx] logging.info( "Partitioning tarred dataset: process (%d) taking shards [%d, %d)", global_rank, begin_idx, end_idx ) elif shard_strategy == 'replicate': logging.info("All tarred dataset shards will be replicated across all nodes.") else: raise ValueError(f"Invalid shard strategy ! Allowed values are : {valid_shard_strategies}") return sharded_filepaths def cache_datastore_manifests( manifest_filepaths: Union[str, List[str]], cache_audio: bool = False, shared_cache: Optional[bool] = None, num_workers: Optional[int] = None, max_num_workers: int = 20, ): """Cache manifests and audio from an object store. It is assumed that remote manifests are using relative paths. Args: manifest_filepaths: list of paths to manifest files (list of strings or a string with `,` as separator) cache_audio: If True, audio from manifest will also be cached shared_cache: Optional, True if cache is shared across all nodes num_workers: Optional, number of workers to be used for download max_num_workers: max number of workers to be used for download, used when setting num_workers automatically """ if isinstance(manifest_filepaths, str): manifest_filepaths = manifest_filepaths.split(',') num_datastore_manifests = sum([is_datastore_path(f) for f in manifest_filepaths]) if num_datastore_manifests > 0: # Local utility function def cache_data(manifest_filepaths, cache_audio, num_workers, max_num_workers): """Cache manifests and audio data from object store.""" # Determine the number of workers to use if num_workers is None: num_workers = os.cpu_count() - 1 num_workers = min(num_workers, max_num_workers) # Process each manifest file for manifest_file in manifest_filepaths: # If manifest is on a data store, then cache it. # Otherwise, nothing to do. if is_datastore_path(manifest_file): logging.info('Cache manifest file: %s', manifest_file) cached_manifest_file = DataStoreObject(manifest_file).get() logging.info('Cached at: %s', str(cached_manifest_file)) if cache_audio: # Each audio file from manifest will be cached. logging.info('Cache audio from manifest file: %s', manifest_file) # Assumes that manifest is using relative paths manifest_dir = os.path.dirname(manifest_file) # Prepare all store objects audio_objects = [] with open(cached_manifest_file, 'r') as f: for line in f: item = json.loads(line) store_path = os.path.join(manifest_dir, item['audio_filepath']) audio_objects.append(DataStoreObject(store_path=store_path)) if num_workers is not None and num_workers > 1: logging.debug('Using multiprocessing with num_workers: %d.', num_workers) with multiprocessing.Pool(processes=num_workers) as p: result = list( tqdm(p.imap(datastore_object_get, audio_objects), total=len(audio_objects)) ) else: logging.debug('Using a single process.') result = [] for audio_object in tqdm(audio_objects): result.append(audio_object.get() is not None) if not all(result): raise RuntimeError('Some files not downloaded successfully') logging.info('Caching complete') else: # Nothing to do here logging.debug('Manifest is not on a data store: %s', manifest_file) if torch.distributed.is_available() and torch.distributed.is_initialized(): logging.debug('Distributed environment is available and initialized.') # Handle distributed environment if shared_cache is None: shared_cache = is_datastore_cache_shared() if shared_cache: logging.debug('Cache is shared among nodes, cache data on global rank zero.') is_rank_zero = is_global_rank_zero() else: logging.debug('Cache is not shared among nodes, cache data on local rank zero.') local_rank = int(os.environ.get("LOCAL_RANK", 0)) is_rank_zero = local_rank == 0 if is_rank_zero: logging.info('Cache data from %s rank 0', 'global' if shared_cache else 'local') cache_data( manifest_filepaths=manifest_filepaths, cache_audio=cache_audio, num_workers=num_workers, max_num_workers=max_num_workers, ) logging.debug('Reached barrier') torch.distributed.barrier() elif is_global_rank_zero(): # Handle non-distributed environment, e.g., if running on a single GPU logging.warning( 'Torch distributed is not initialized and caching may be prone to data race conditions. ' 'Now caching data from global rank 0. If there are other ranks and they pass this ' 'before rank 0, errors might result.' ) cache_data( manifest_filepaths=manifest_filepaths, cache_audio=cache_audio, num_workers=num_workers, max_num_workers=max_num_workers, ) else: raise RuntimeError( 'Torch distributed is not initialized and caching on nodes other than global rank zero is disabled ' 'to avoid race condition between different ranks. To ensure distributed environment is ' 'initialized, please update data config to use `defer_setup = True`.' ) """Optionally expand / shard the list of manifests This is made to use the same notation as the sharded audio files Args: manifest_filepaths: list of manifest files (the sharded notation) shard_strategy: scatter or replicate (scatter by default) shard_manifests: bool, if False, no sharding / manifest filepath expansion will be attempted global_rank: int, the rank of this worker world_size: int, total number of workers """ def shard_manifests_if_needed( manifest_filepaths: Union[str, List[str]], shard_strategy: str, shard_manifests: bool, global_rank: int, world_size: int, ): if shard_manifests: if not torch.distributed.is_available(): logging.warning("Not running in torch.distributed mode. Manifest sharding not available") return manifest_filepaths if not torch.distributed.is_initialized(): logging.warning( 'Manifest sharding was requested but torch.distributed is not initialized ' 'Did you intend to set the defer_setup flag?' ) return manifest_filepaths manifest_filepaths = expand_sharded_filepaths( sharded_filepaths=manifest_filepaths, shard_strategy=shard_strategy, world_size=world_size, global_rank=global_rank, ) return manifest_filepaths class _AudioTextDataset(Dataset): """ Dataset that loads tensors via a json file containing paths to audio files, transcripts, and durations (in seconds). Each new line is a different sample. Example below: {"audio_filepath": "/path/to/audio.wav", "text_filepath": "/path/to/audio.txt", "duration": 23.147} ... {"audio_filepath": "/path/to/audio.wav", "text": "the transcription", "offset": 301.75, "duration": 0.82, "utt": "utterance_id", "ctm_utt": "en_4156", "side": "A"} Args: manifest_filepath: Path to manifest json as described above. Can be comma-separated paths. parser: Str for a language specific preprocessor or a callable. 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 or not to trim silence. Defaults to False 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 pad_id: 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. manifest_parse_func: Optional function to parse manifest entries. Defaults to None. """ @property def output_types(self) -> Optional[Dict[str, NeuralType]]: """Returns definitions of module output ports.""" return { 'audio_signal': NeuralType(('B', 'T'), AudioSignal()), 'a_sig_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], sample_rate: int, int_values: bool = False, augmentor: 'nemo.collections.asr.parts.perturb.AudioAugmentor' = 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, manifest_parse_func: Optional[Callable] = None, ): if type(manifest_filepath) == str: manifest_filepath = manifest_filepath.split(",") # If necessary, cache manifests and audio from object store cache_datastore_manifests(manifest_filepaths=manifest_filepath, cache_audio=True) self.manifest_processor = ASRManifestProcessor( 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, manifest_parse_func=manifest_parse_func, ) self.featurizer = WaveformFeaturizer(sample_rate=sample_rate, int_values=int_values, 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): if isinstance(index, IterableABC): return [self._process_sample(_index) for _index in index] else: return self._process_sample(index) def _process_sample(self, index): sample = self.manifest_processor.collection[index] offset = sample.offset if offset is None: offset = 0 features = self.featurizer.process( sample.audio_file, offset=offset, duration=sample.duration, trim=self.trim, orig_sr=sample.orig_sr, channel_selector=self.channel_selector, ) f, fl = features, torch.tensor(features.shape[0]).long() t, tl = self.manifest_processor.process_text_by_sample(sample=sample) 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 __len__(self): return len(self.manifest_processor.collection) def _collate_fn(self, batch): return _speech_collate_fn(batch, pad_id=self.manifest_processor.pad_id) class AudioToCharDataset(_AudioTextDataset): """ Dataset that loads tensors via a json file containing paths to audio files, transcripts, and durations (in seconds). Each new line is a different sample. Example below: {"audio_filepath": "/path/to/audio.wav", "text_filepath": "/path/to/audio.txt", "duration": 23.147} ... {"audio_filepath": "/path/to/audio.wav", "text": "the transcription", "offset": 301.75, "duration": 0.82, "utt": "utterance_id", "ctm_utt": "en_4156", "side": "A"} Args: manifest_filepath: Path to manifest json as described above. Can be comma-separated paths. labels: String containing all the possible characters to map to 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 normalize: whether to normalize transcript text (default): True 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. manifest_parse_func: Optional function to parse manifest entries. Defaults to None. """ @property def output_types(self) -> Optional[Dict[str, NeuralType]]: """Returns definitions of module output ports.""" return { 'audio_signal': NeuralType(('B', 'T'), AudioSignal()), 'a_sig_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, labels: Union[str, List[str]], sample_rate: int, int_values: bool = False, augmentor: 'nemo.collections.asr.parts.perturb.AudioAugmentor' = None, max_duration: Optional[float] = None, min_duration: Optional[float] = None, max_utts: int = 0, blank_index: int = -1, unk_index: int = -1, normalize: bool = True, 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, manifest_parse_func: Optional[Callable] = 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, sample_rate=sample_rate, int_values=int_values, 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, manifest_parse_func=manifest_parse_func, ) class AudioToBPEDataset(_AudioTextDataset): """ Dataset that loads tensors via a json file containing paths to audio files, transcripts, and durations (in seconds). Each new line is a different sample. Example below: {"audio_filepath": "/path/to/audio.wav", "text_filepath": "/path/to/audio.txt", "duration": 23.147} ... {"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: 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. 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. manifest_parse_func: Optional function to parse manifest entries. Defaults to None. """ @property def output_types(self) -> Optional[Dict[str, NeuralType]]: """Returns definitions of module output ports.""" return { 'audio_signal': NeuralType(('B', 'T'), AudioSignal()), 'a_sig_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, tokenizer: 'nemo.collections.common.tokenizers.TokenizerSpec', sample_rate: int, int_values: bool = False, augmentor: 'nemo.collections.asr.parts.perturb.AudioAugmentor' = None, max_duration: Optional[int] = None, min_duration: Optional[int] = None, max_utts: int = 0, trim: bool = False, use_start_end_token: bool = True, return_sample_id: bool = False, channel_selector: Optional[ChannelSelectorType] = None, manifest_parse_func: Optional[Callable] = 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), sample_rate=sample_rate, int_values=int_values, augmentor=augmentor, max_duration=max_duration, min_duration=min_duration, max_utts=max_utts, bos_id=bos_id, eos_id=eos_id, pad_id=pad_id, trim=trim, return_sample_id=return_sample_id, channel_selector=channel_selector, manifest_parse_func=manifest_parse_func, ) @deprecated( explanation='Webdataset support will be removed in v2.1.0 versions, please use LhotseSpeechToTextBpeDataset class instead' ) class _TarredAudioToTextDataset(IterableDataset): """ A similar Dataset to the AudioToCharDataset/AudioToBPEDataset, but which loads tarred audio files. Accepts a single comma-separated JSON manifest file (in the same style as for the AudioToCharDataset/AudioToBPEDataset), as well as the path(s) to the tarball(s) containing the wav files. Each line of the manifest should contain the information for one audio file, including at least the transcript and name of the audio file within the tarball. Valid formats for the audio_tar_filepaths argument include: (1) a single string that can be brace-expanded, e.g. 'path/to/audio.tar' or 'path/to/audio_{1..100}.tar.gz', or (2) a list of file paths that will not be brace-expanded, e.g. ['audio_1.tar', 'audio_2.tar', ...]. Note: For brace expansion in (1), there may be cases where `{x..y}` syntax cannot be used due to shell interference. This occurs most commonly inside SLURM scripts. Therefore we provide a few equivalent replacements. Supported opening braces - { <=> (, [, < and the special tag _OP_. Supported closing braces - } <=> ), ], > and the special tag _CL_. For SLURM based tasks, we suggest the use of the special tags for ease of use. See the WebDataset documentation for more information about accepted data and input formats. If using multiple workers the number of shards should be divisible by world_size to ensure an even split among workers. If it is not divisible, logging will give a warning but training will proceed. In addition, if using mutiprocessing, each shard MUST HAVE THE SAME NUMBER OF ENTRIES after filtering is applied. We currently do not check for this, but your program may hang if the shards are uneven! Notice that a few arguments are different from the AudioToCharDataset; for example, shuffle (bool) has been replaced by shuffle_n (int). Additionally, please note that the len() of this DataLayer is assumed to be the length of the manifest after filtering. An incorrect manifest length may lead to some DataLoader issues down the line. Args: audio_tar_filepaths: Either a list of audio tarball filepaths, or a string (can be brace-expandable). manifest_filepath (str): Path to the manifest. parser (callable): A callable which is used to pre-process the text output. 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 shuffle_n (int): How many samples to look ahead and load to be shuffled. See WebDataset documentation for more details. Defaults to 0. min_duration (float): Dataset parameter. All training files which have a duration less than min_duration are dropped. Note: Duration is read from the manifest JSON. Defaults to 0.1. max_duration (float): Dataset parameter. All training files which have a duration more than max_duration are dropped. Note: Duration is read from the manifest JSON. Defaults to None. blank_index (int): Blank character index, defaults to -1. unk_index (int): Unknown character index, defaults to -1. normalize (bool): Dataset parameter. Whether to use automatic text cleaning. It is highly recommended to manually clean text for best results. Defaults to True. trim (bool): Whether to use trim silence from beginning and end of audio signal using librosa.effects.trim(). Defaults to False. bos_id (id): Dataset parameter. Beginning of string symbol id used for seq2seq models. Defaults to None. eos_id (id): Dataset parameter. End of string symbol id used for seq2seq models. Defaults to None. pad_id (id): Token used to pad when collating samples in batches. If this is None, pads using 0s. Defaults to None. shard_strategy (str): Tarred dataset shard distribution strategy chosen as a str value during ddp. - `scatter`: The default shard strategy applied by WebDataset, where each node gets a unique set of shards, which are permanently pre-allocated and never changed at runtime. - `replicate`: Optional shard strategy, where each node gets all of the set of shards available in the tarred dataset, which are permanently pre-allocated and never changed at runtime. The benefit of replication is that it allows each node to sample data points from the entire dataset independently of other nodes, and reduces dependence on value of `shuffle_n`. .. warning:: Replicated strategy allows every node to sample the entire set of available tarfiles, and therefore more than one node may sample the same tarfile, and even sample the same data points! As such, there is no assured guarantee that all samples in the dataset will be sampled at least once during 1 epoch. Scattered strategy, on the other hand, on specific occasions (when the number of shards is not divisible with ``world_size``), will not sample the entire dataset. For these reasons it is not advisable to use tarred datasets as validation or test datasets. shard_manifests (bool): Whether or not to try / shard manifests. Defaults to False. global_rank (int): Worker rank, used for partitioning shards. Defaults to 0. world_size (int): Total number of processes, used for partitioning shards. Defaults to 0. return_sample_id (bool): whether to return the sample_id as a part of each sample manifest_parse_func: Optional function to parse manifest entries. Defaults to None. """ def __init__( self, audio_tar_filepaths: Union[str, List[str]], manifest_filepath: str, parser: Callable, sample_rate: int, int_values: bool = False, augmentor: Optional['nemo.collections.asr.parts.perturb.AudioAugmentor'] = None, shuffle_n: int = 0, min_duration: Optional[float] = None, max_duration: Optional[float] = None, trim: bool = False, bos_id: Optional[int] = None, eos_id: Optional[int] = None, pad_id: int = 0, shard_strategy: str = "scatter", shard_manifests: bool = False, global_rank: int = 0, world_size: int = 0, return_sample_id: bool = False, manifest_parse_func: Optional[Callable] = None, ): self.shard_manifests = shard_manifests # Shard manifests if necessary and possible and then expand the paths manifest_filepath = shard_manifests_if_needed( shard_manifests=shard_manifests, shard_strategy=shard_strategy, manifest_filepaths=manifest_filepath, world_size=world_size, global_rank=global_rank, ) # If necessary, cache manifests from object store cache_datastore_manifests(manifest_filepaths=manifest_filepath) self.manifest_processor = ASRManifestProcessor( manifest_filepath=manifest_filepath, parser=parser, max_duration=max_duration, min_duration=min_duration, max_utts=0, bos_id=bos_id, eos_id=eos_id, pad_id=pad_id, index_by_file_id=True, # Must set this so the manifest lines can be indexed by file ID manifest_parse_func=manifest_parse_func, ) self.len = self._compute_len() self.featurizer = WaveformFeaturizer(sample_rate=sample_rate, int_values=int_values, augmentor=augmentor) self.trim = trim self.eos_id = eos_id self.bos_id = bos_id self.pad_id = pad_id self.return_sample_id = return_sample_id audio_tar_filepaths = expand_sharded_filepaths( sharded_filepaths=audio_tar_filepaths, shard_strategy=shard_strategy, world_size=world_size, global_rank=global_rank, ) # Put together WebDataset pipeline self._dataset = wds.DataPipeline( wds.SimpleShardList(urls=audio_tar_filepaths), webdataset_split_by_workers, wds.shuffle(shuffle_n), wds.tarfile_to_samples(), wds.rename(audio=VALID_FILE_FORMATS, key='__key__'), wds.to_tuple('audio', 'key'), self._filter, self._loop_offsets, wds.map(self._build_sample), ) def _filter(self, iterator): """This function is used to remove samples that have been filtered out by ASRAudioText already. Otherwise, we would get a KeyError as _build_sample attempts to find the manifest entry for a sample that was filtered out (e.g. for duration). Note that if using multi-GPU training, filtering may lead to an imbalance in samples in each shard, which may make your code hang as one process will finish before the other. """ class TarredAudioFilter: def __init__(self, collection): self.iterator = iterator self.collection = collection def __iter__(self): return self def __next__(self): while True: audio_bytes, audio_filename = next(self.iterator) file_id, _ = os.path.splitext(os.path.basename(audio_filename)) if file_id in self.collection.mapping: return audio_bytes, audio_filename return TarredAudioFilter(self.manifest_processor.collection) def _loop_offsets(self, iterator): """This function is used to iterate through utterances with different offsets for each file.""" class TarredAudioLoopOffsets: def __init__(self, collection): self.iterator = iterator self.collection = collection self.current_fn = None self.current_bytes = None self.offset_id = 0 def __iter__(self): return self def __next__(self): if self.current_fn is None: self.current_bytes, self.current_fn = next(self.iterator) self.offset_id = 0 else: offset_list = self.collection.mapping[self.current_fn] if len(offset_list) == self.offset_id + 1: self.current_bytes, self.current_fn = next(self.iterator) self.offset_id = 0 else: self.offset_id += 1 return self.current_bytes, self.current_fn, self.offset_id return TarredAudioLoopOffsets(self.manifest_processor.collection) def _collate_fn(self, batch): return _speech_collate_fn(batch, self.pad_id) def _build_sample(self, tup): """Builds the training sample by combining the data from the WebDataset with the manifest info.""" audio_bytes, audio_filename, offset_id = tup # Grab manifest entry from self.manifest_preprocessor.collection file_id, _ = os.path.splitext(os.path.basename(audio_filename)) manifest_idx = self.manifest_processor.collection.mapping[file_id][offset_id] manifest_entry = self.manifest_processor.collection[manifest_idx] offset = manifest_entry.offset if offset is None: offset = 0 # Convert audio bytes to IO stream for processing (for SoundFile to read) audio_filestream = io.BytesIO(audio_bytes) features = self.featurizer.process( audio_filestream, offset=offset, duration=manifest_entry.duration, trim=self.trim, orig_sr=manifest_entry.orig_sr, ) audio_filestream.close() # Audio features f, fl = features, torch.tensor(features.shape[0]).long() # Text features t, tl = manifest_entry.text_tokens, len(manifest_entry.text_tokens) self.manifest_processor.process_text_by_sample(sample=manifest_entry) 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 if self.return_sample_id: return f, fl, torch.tensor(t).long(), torch.tensor(tl).long(), manifest_idx else: return f, fl, torch.tensor(t).long(), torch.tensor(tl).long() def get_manifest_sample(self, sample_id): return self.manifest_processor.collection[sample_id] def __iter__(self): return self._dataset.__iter__() def _compute_len(self): if self.shard_manifests and torch.distributed.is_available() and torch.distributed.is_initialized(): my_len = torch.tensor(len(self.manifest_processor.collection), dtype=torch.int32).cuda() torch.distributed.all_reduce(my_len) my_len = my_len.int() logging.info(f'Sharded manifests: Total length: {my_len}') else: my_len = len(self.manifest_processor.collection) return my_len def __len__(self): return self.len class TarredAudioToCharDataset(_TarredAudioToTextDataset): """ A similar Dataset to the AudioToCharDataset, but which loads tarred audio files. Accepts a single comma-separated JSON manifest file (in the same style as for the AudioToCharDataset), as well as the path(s) to the tarball(s) containing the wav files. Each line of the manifest should contain the information for one audio file, including at least the transcript and name of the audio file within the tarball. Valid formats for the audio_tar_filepaths argument include: (1) a single string that can be brace-expanded, e.g. 'path/to/audio.tar' or 'path/to/audio_{1..100}.tar.gz', or (2) a list of file paths that will not be brace-expanded, e.g. ['audio_1.tar', 'audio_2.tar', ...]. See the WebDataset documentation for more information about accepted data and input formats. If using multiple workers the number of shards should be divisible by world_size to ensure an even split among workers. If it is not divisible, logging will give a warning but training will proceed. In addition, if using mutiprocessing, each shard MUST HAVE THE SAME NUMBER OF ENTRIES after filtering is applied. We currently do not check for this, but your program may hang if the shards are uneven! Notice that a few arguments are different from the AudioToCharDataset; for example, shuffle (bool) has been replaced by shuffle_n (int). Additionally, please note that the len() of this DataLayer is assumed to be the length of the manifest after filtering. An incorrect manifest length may lead to some DataLoader issues down the line. Args: audio_tar_filepaths: Either a list of audio tarball filepaths, or a string (can be brace-expandable). manifest_filepath (str): Path to the manifest. labels (list): List of characters that can be output by the ASR model. For Jasper, this is the 28 character set {a-z '}. The CTC blank symbol is automatically added later for models using ctc. 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 shuffle_n (int): How many samples to look ahead and load to be shuffled. See WebDataset documentation for more details. Defaults to 0. min_duration (float): Dataset parameter. All training files which have a duration less than min_duration are dropped. Note: Duration is read from the manifest JSON. Defaults to 0.1. max_duration (float): Dataset parameter. All training files which have a duration more than max_duration are dropped. Note: Duration is read from the manifest JSON. Defaults to None. blank_index (int): Blank character index, defaults to -1. unk_index (int): Unknown character index, defaults to -1. normalize (bool): Dataset parameter. Whether to use automatic text cleaning. It is highly recommended to manually clean text for best results. Defaults to True. trim (bool): Whether to use trim silence from beginning and end of audio signal using librosa.effects.trim(). Defaults to False. bos_id (id): Dataset parameter. Beginning of string symbol id used for seq2seq models. Defaults to None. eos_id (id): Dataset parameter. End of string symbol id used for seq2seq models. Defaults to None. pad_id (id): Token used to pad when collating samples in batches. If this is None, pads using 0s. Defaults to None. shard_strategy (str): Tarred dataset shard distribution strategy chosen as a str value during ddp. - `scatter`: The default shard strategy applied by WebDataset, where each node gets a unique set of shards, which are permanently pre-allocated and never changed at runtime. - `replicate`: Optional shard strategy, where each node gets all of the set of shards available in the tarred dataset, which are permanently pre-allocated and never changed at runtime. The benefit of replication is that it allows each node to sample data points from the entire dataset independently of other nodes, and reduces dependence on value of `shuffle_n`. .. warning:: Replicated strategy allows every node to sample the entire set of available tarfiles, and therefore more than one node may sample the same tarfile, and even sample the same data points! As such, there is no assured guarantee that all samples in the dataset will be sampled at least once during 1 epoch. Scattered strategy, on the other hand, on specific occasions (when the number of shards is not divisible with ``world_size``), will not sample the entire dataset. For these reasons it is not advisable to use tarred datasets as validation or test datasets. global_rank (int): Worker rank, used for partitioning shards. Defaults to 0. world_size (int): Total number of processes, used for partitioning shards. Defaults to 0. return_sample_id (bool): whether to return the sample_id as a part of each sample manifest_parse_func: Optional function to parse manifest entries. Defaults to None. """ def __init__( self, audio_tar_filepaths: Union[str, List[str]], manifest_filepath: str, labels: List[str], sample_rate: int, int_values: bool = False, augmentor: Optional['nemo.collections.asr.parts.perturb.AudioAugmentor'] = None, shuffle_n: int = 0, min_duration: Optional[float] = None, max_duration: Optional[float] = None, blank_index: int = -1, unk_index: int = -1, normalize: bool = True, trim: bool = False, bos_id: Optional[int] = None, eos_id: Optional[int] = None, parser: Optional[str] = 'en', pad_id: int = 0, shard_strategy: str = "scatter", shard_manifests: bool = False, global_rank: int = 0, world_size: int = 0, return_sample_id: bool = False, manifest_parse_func: Optional[Callable] = 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__( audio_tar_filepaths=audio_tar_filepaths, manifest_filepath=manifest_filepath, parser=parser, sample_rate=sample_rate, int_values=int_values, augmentor=augmentor, shuffle_n=shuffle_n, min_duration=min_duration, max_duration=max_duration, trim=trim, bos_id=bos_id, eos_id=eos_id, pad_id=pad_id, shard_strategy=shard_strategy, shard_manifests=shard_manifests, global_rank=global_rank, world_size=world_size, return_sample_id=return_sample_id, manifest_parse_func=manifest_parse_func, ) class TarredAudioToBPEDataset(_TarredAudioToTextDataset): """ A similar Dataset to the AudioToBPEDataset, but which loads tarred audio files. Accepts a single comma-separated JSON manifest file (in the same style as for the AudioToBPEDataset), as well as the path(s) to the tarball(s) containing the wav files. Each line of the manifest should contain the information for one audio file, including at least the transcript and name of the audio file within the tarball. Valid formats for the audio_tar_filepaths argument include: (1) a single string that can be brace-expanded, e.g. 'path/to/audio.tar' or 'path/to/audio_{1..100}.tar.gz', or (2) a list of file paths that will not be brace-expanded, e.g. ['audio_1.tar', 'audio_2.tar', ...]. See the WebDataset documentation for more information about accepted data and input formats. If using multiple workers the number of shards should be divisible by world_size to ensure an even split among workers. If it is not divisible, logging will give a warning but training will proceed. In addition, if using mutiprocessing, each shard MUST HAVE THE SAME NUMBER OF ENTRIES after filtering is applied. We currently do not check for this, but your program may hang if the shards are uneven! Notice that a few arguments are different from the AudioToBPEDataset; for example, shuffle (bool) has been replaced by shuffle_n (int). Additionally, please note that the len() of this DataLayer is assumed to be the length of the manifest after filtering. An incorrect manifest length may lead to some DataLoader issues down the line. Args: audio_tar_filepaths: Either a list of audio tarball filepaths, or a string (can be brace-expandable). manifest_filepath (str): Path to the manifest. tokenizer (TokenizerSpec): Either a Word Piece Encoding tokenizer (BERT), or a Sentence Piece Encoding tokenizer (BPE). The CTC blank symbol is automatically added later for models using ctc. 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 shuffle_n (int): How many samples to look ahead and load to be shuffled. See WebDataset documentation for more details. Defaults to 0. min_duration (float): Dataset parameter. All training files which have a duration less than min_duration are dropped. Note: Duration is read from the manifest JSON. Defaults to 0.1. max_duration (float): Dataset parameter. All training files which have a duration more than max_duration are dropped. Note: Duration is read from the manifest JSON. Defaults to None. trim (bool): Whether to use trim silence from beginning and end of audio signal using librosa.effects.trim(). Defaults to False. use_start_end_token: Boolean which dictates whether to add [BOS] and [EOS] tokens to beginning and ending of speech respectively. pad_id (id): Token used to pad when collating samples in batches. If this is None, pads using 0s. Defaults to None. shard_strategy (str): Tarred dataset shard distribution strategy chosen as a str value during ddp. - `scatter`: The default shard strategy applied by WebDataset, where each node gets a unique set of shards, which are permanently pre-allocated and never changed at runtime. - `replicate`: Optional shard strategy, where each node gets all of the set of shards available in the tarred dataset, which are permanently pre-allocated and never changed at runtime. The benefit of replication is that it allows each node to sample data points from the entire dataset independently of other nodes, and reduces dependence on value of `shuffle_n`. .. warning:: Replicated strategy allows every node to sample the entire set of available tarfiles, and therefore more than one node may sample the same tarfile, and even sample the same data points! As such, there is no assured guarantee that all samples in the dataset will be sampled at least once during 1 epoch. Scattered strategy, on the other hand, on specific occasions (when the number of shards is not divisible with ``world_size``), will not sample the entire dataset. For these reasons it is not advisable to use tarred datasets as validation or test datasets. global_rank (int): Worker rank, used for partitioning shards. Defaults to 0. world_size (int): Total number of processes, used for partitioning shards. Defaults to 0. return_sample_id (bool): whether to return the sample_id as a part of each sample manifest_parse_func: Optional function to parse manifest entries. Defaults to None. """ def __init__( self, audio_tar_filepaths: Union[str, List[str]], manifest_filepath: str, tokenizer: 'nemo.collections.common.tokenizers.TokenizerSpec', sample_rate: int, int_values: bool = False, augmentor: Optional['nemo.collections.asr.parts.perturb.AudioAugmentor'] = None, shuffle_n: int = 0, min_duration: Optional[float] = None, max_duration: Optional[float] = None, trim: bool = False, use_start_end_token: bool = True, shard_strategy: str = "scatter", shard_manifests: bool = False, global_rank: int = 0, world_size: int = 0, return_sample_id: bool = False, manifest_parse_func: Optional[Callable] = 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__( audio_tar_filepaths=audio_tar_filepaths, manifest_filepath=manifest_filepath, parser=TokenizerWrapper(tokenizer), sample_rate=sample_rate, int_values=int_values, augmentor=augmentor, shuffle_n=shuffle_n, min_duration=min_duration, max_duration=max_duration, trim=trim, bos_id=bos_id, eos_id=eos_id, pad_id=pad_id, shard_strategy=shard_strategy, shard_manifests=shard_manifests, global_rank=global_rank, world_size=world_size, return_sample_id=return_sample_id, manifest_parse_func=manifest_parse_func, ) class BucketingDataset(IterableDataset): """ A Dataset which wraps another IterableDataset and adopts it for bucketing Args: dataset (IterableDataset): The IterableDataset to get wrapped bucketing_batch_size (int): Number of samples to build a batch """ def __init__( self, dataset: IterableDataset, bucketing_batch_size: int, ): self.wrapped_dataset = dataset self.bucketing_batch_size = bucketing_batch_size super().__init__() def _collate_fn(self, batch): return _speech_collate_fn(batch[0], self.wrapped_dataset.pad_id) def __iter__(self): return BucketingIterator( wrapped_ds=self.wrapped_dataset._dataset, bucketing_batch_size=self.bucketing_batch_size ).__iter__() def __len__(self): return int(math.ceil(len(self.wrapped_dataset) / float(self.bucketing_batch_size))) class BucketingIterator: def __init__(self, wrapped_ds, bucketing_batch_size): self.wrapped_ds = wrapped_ds self.wrapped_iter = None self.bucketing_batch_size = bucketing_batch_size def __iter__(self): self.wrapped_iter = iter(self.wrapped_ds) return self def __next__(self): batches = [] for idx in range(self.bucketing_batch_size): try: sample = next(self.wrapped_iter) except StopIteration: break batches.append(sample) if len(batches) == 0: raise StopIteration return batches class RandomizedChainDataset(ChainDataset): def __init__(self, datasets: Iterable[Dataset], rnd_seed=0) -> None: super(RandomizedChainDataset, self).__init__(list(datasets)) self.rnd_gen = np.random.RandomState(rnd_seed) def __iter__(self): shuffled_order = self.rnd_gen.permutation(len(self.datasets)) for dataset_idx in shuffled_order: d = self.datasets[dataset_idx] assert isinstance(d, IterableDataset), "ChainDataset only supports IterableDataset" for idx, x in enumerate(d): yield x # in case d is an infinite dataset, we want to break the loop # so that the other datasets get a chance to yield too if idx >= len(d) - 1: break