nvidia-nemo--speech
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
310 行
12 KiB
Python
310 行
12 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.
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
from collections import defaultdict
|
|
from os.path import expanduser
|
|
from typing import Any, Callable, Dict, Iterator, List, Optional, Union
|
|
|
|
from nemo.utils import logging
|
|
from nemo.utils.data_utils import DataStoreObject, get_datastore_object, is_datastore_path
|
|
from nemo.utils.nemo_logging import LogMode
|
|
|
|
|
|
class ManifestBase:
|
|
"""Deprecated manifest base class."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
raise ValueError(
|
|
"This class is deprecated, look at https://github.com/NVIDIA/NeMo/pull/284 for correct behaviour."
|
|
)
|
|
|
|
|
|
class ManifestEN:
|
|
"""Deprecated English manifest class."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
raise ValueError(
|
|
"This class is deprecated, look at https://github.com/NVIDIA/NeMo/pull/284 for correct behaviour."
|
|
)
|
|
|
|
|
|
def item_iter(
|
|
manifests_files: Union[str, List[str]], parse_func: Callable[[str, Optional[str]], Dict[str, Any]] = None
|
|
) -> Iterator[Dict[str, Any]]:
|
|
"""Iterate through json lines of provided manifests.
|
|
|
|
NeMo ASR pipelines often assume certain manifest files structure. In
|
|
particular, each manifest file should consist of line-per-sample files with
|
|
each line being correct json dict. Each such json dict should have a field
|
|
for audio file string, a field for duration float and a field for text
|
|
string. Offset also could be additional field and is set to None by
|
|
default.
|
|
|
|
Args:
|
|
manifests_files: Either single string file or list of such -
|
|
manifests to yield items from.
|
|
|
|
parse_func: A callable function which accepts as input a single line
|
|
of a manifest and optionally the manifest file itself,
|
|
and parses it, returning a dictionary mapping from str -> Any.
|
|
|
|
Yields:
|
|
Parsed key to value item dicts.
|
|
|
|
Raises:
|
|
ValueError: If met invalid json line structure.
|
|
"""
|
|
|
|
if isinstance(manifests_files, str):
|
|
manifests_files = [manifests_files]
|
|
|
|
if parse_func is None:
|
|
parse_func = __parse_item
|
|
|
|
errors = defaultdict(list)
|
|
k = -1
|
|
logging.debug('Manifest files: %s', str(manifests_files))
|
|
for manifest_file in manifests_files:
|
|
logging.debug('Using manifest file: %s', str(manifest_file))
|
|
cached_manifest_file = DataStoreObject(manifest_file).get()
|
|
logging.debug('Cached at: %s', str(cached_manifest_file))
|
|
with open(expanduser(cached_manifest_file), 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
k += 1
|
|
try:
|
|
item = parse_func(line, manifest_file)
|
|
except json.JSONDecodeError:
|
|
errors[str(manifest_file)].append(line)
|
|
continue
|
|
item['id'] = k
|
|
|
|
yield item
|
|
|
|
if len(errors) > 0:
|
|
for filename, lines in errors.items():
|
|
logging.error("=============================================")
|
|
logging.error(f"Failed to parse {len(lines)} lines from manifest file: {filename}")
|
|
for line in lines:
|
|
logging.error(f"-- Failed to parse line: `{line}`")
|
|
raise RuntimeError("Failed to parse some lines from manifest files. See logs for more details.")
|
|
|
|
|
|
def __parse_item(line: str, manifest_file: str) -> Dict[str, Any]:
|
|
item = json.loads(line)
|
|
|
|
# Audio file
|
|
if 'audio_filename' in item:
|
|
item['audio_file'] = item.pop('audio_filename')
|
|
elif 'audio_filepath' in item:
|
|
item['audio_file'] = item.pop('audio_filepath')
|
|
elif 'context' in item:
|
|
item['audio_file'] = item['context']
|
|
|
|
# Video File
|
|
if 'video_filename' in item:
|
|
item['video_file'] = item.pop('video_filename')
|
|
elif 'video_filepath' in item:
|
|
item['video_file'] = item.pop('video_filepath')
|
|
|
|
if 'video_file' not in item and 'audio_file' not in item:
|
|
raise ValueError(
|
|
f"Manifest file {manifest_file} has invalid json line structure: "
|
|
f"{line} without proper audio/video file key."
|
|
)
|
|
|
|
# If the audio/video path is a relative path and does not exist,
|
|
# try to attach the parent directory of manifest to the audio path.
|
|
# Revert to the original path if the new path still doesn't exist.
|
|
# Assume that the audio path is like "wavs/xxxxxx.wav".
|
|
if 'audio_file' in item:
|
|
item['audio_file'] = get_full_path(audio_file=item['audio_file'], manifest_file=manifest_file)
|
|
if 'video_file' in item:
|
|
item['video_file'] = get_full_path(audio_file=item['video_file'], manifest_file=manifest_file)
|
|
|
|
# Duration.
|
|
if 'context_duration' in item and 'duration' not in item:
|
|
item['duration'] = item['context_duration']
|
|
elif 'duration' not in item:
|
|
raise ValueError(
|
|
f"Manifest file {manifest_file} has invalid json line structure: {line} without proper duration key."
|
|
)
|
|
|
|
# Text.
|
|
if 'text' in item:
|
|
pass
|
|
elif 'text_filepath' in item:
|
|
with open(item.pop('text_filepath'), 'r') as f:
|
|
item['text'] = f.read().replace('\n', '')
|
|
elif 'normalized_text' in item:
|
|
item['text'] = item['normalized_text']
|
|
else:
|
|
item['text'] = ""
|
|
|
|
# Optional RTTM file
|
|
if 'rttm_file' in item:
|
|
pass
|
|
elif 'rttm_filename' in item:
|
|
item['rttm_file'] = item.pop('rttm_filename')
|
|
elif 'rttm_filepath' in item:
|
|
item['rttm_file'] = item.pop('rttm_filepath')
|
|
else:
|
|
item['rttm_file'] = None
|
|
if item['rttm_file'] is not None:
|
|
item['rttm_file'] = get_full_path(audio_file=item['rttm_file'], manifest_file=manifest_file)
|
|
|
|
# Optional audio feature file
|
|
if 'feature_file' in item:
|
|
pass
|
|
elif 'feature_filename' in item:
|
|
item['feature_file'] = item.pop('feature_filename')
|
|
elif 'feature_filepath' in item:
|
|
item['feature_file'] = item.pop('feature_filepath')
|
|
else:
|
|
item['feature_file'] = None
|
|
if item['feature_file'] is not None:
|
|
item['feature_file'] = get_full_path(audio_file=item['feature_file'], manifest_file=manifest_file)
|
|
|
|
item = dict(
|
|
audio_file=item.get('audio_file', None),
|
|
video_file=item.get('video_file', None),
|
|
duration=item['duration'],
|
|
text=item['text'],
|
|
rttm_file=item['rttm_file'],
|
|
feature_file=item['feature_file'],
|
|
offset=item.get('offset', None),
|
|
speaker=item.get('speaker', None),
|
|
orig_sr=item.get('orig_sample_rate', None),
|
|
token_labels=item.get('token_labels', None),
|
|
lang=item.get('lang', None),
|
|
context=item.get('context', None),
|
|
context_type=item.get('context_type', None),
|
|
context_duration=item.get('context_duration', None),
|
|
answer=item.get('answer', None),
|
|
answer_type=item.get('answer_type', None),
|
|
answer_duration=item.get('answer_duration', None),
|
|
question=item.get('question', None),
|
|
question_type=item.get('question_type', None),
|
|
task=item.get('task', None),
|
|
)
|
|
return item
|
|
|
|
|
|
def is_tarred_dataset(audio_file: str, manifest_file: Optional[str] = None) -> bool:
|
|
"""Check if an audio file belongs to a tarred dataset based on path heuristics."""
|
|
if "/" in audio_file or manifest_file is None:
|
|
# audio files in a tarred dataset don't have `/` in their paths
|
|
return False
|
|
if os.path.basename(manifest_file) == "tarred_audio_manifest.json":
|
|
# the manifest file is a tarred manifest
|
|
return True
|
|
if "/sharded_manifests/" in manifest_file and re.match(r'^manifest_(\d+)\.json$', os.path.basename(manifest_file)):
|
|
# the manifest file is a sharded manifest
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_full_path(
|
|
audio_file: Union[str, List[str]],
|
|
manifest_file: Optional[str] = None,
|
|
data_dir: Optional[str] = None,
|
|
audio_file_len_limit: int = 255,
|
|
force_cache: bool = True,
|
|
) -> Union[str, List[str]]:
|
|
"""Get full path to audio_file.
|
|
|
|
If the audio_file is a relative path and does not exist,
|
|
try to attach the parent directory of manifest to the audio path.
|
|
Revert to the original path if the new path still doesn't exist.
|
|
Assume that the audio path is like "wavs/xxxxxx.wav".
|
|
|
|
Args:
|
|
audio_file: path to an audio file, either absolute or assumed relative
|
|
to the manifest directory or data directory.
|
|
Alternatively, a list of paths may be provided.
|
|
manifest_file: path to a manifest file
|
|
data_dir: path to a directory containing data, use only if a manifest file is not provided
|
|
audio_file_len_limit: limit for length of audio_file when using relative paths
|
|
force_cache: cache and provide local cached path of the audio if audio_file is from a data object store
|
|
|
|
Returns:
|
|
Full path to audio_file or a list of paths.
|
|
"""
|
|
if isinstance(audio_file, list):
|
|
# If input is a list, return a list of full paths
|
|
return [
|
|
get_full_path(
|
|
audio_file=a_file,
|
|
manifest_file=manifest_file,
|
|
data_dir=data_dir,
|
|
audio_file_len_limit=audio_file_len_limit,
|
|
force_cache=force_cache,
|
|
)
|
|
for a_file in audio_file
|
|
]
|
|
elif isinstance(audio_file, str):
|
|
# If input is a string, get the corresponding full path
|
|
if is_datastore_path(audio_file):
|
|
return audio_file
|
|
if is_tarred_dataset(audio_file=audio_file, manifest_file=manifest_file):
|
|
logging.warning(
|
|
f"Manifest file `{manifest_file}` seems to be part of a tarred dataset, skip checking"
|
|
f" for relative paths. If this is not intended, please avoid having `/sharded_manifests/`"
|
|
f" and `tarred_audio_manifest.json` in manifest_filepath.",
|
|
mode=LogMode.ONCE,
|
|
)
|
|
return audio_file
|
|
if (
|
|
(len(audio_file) < audio_file_len_limit)
|
|
and not os.path.isabs(audio_file)
|
|
# and not os.path.isfile(audio_file) # Commented out because it slows down dataloading
|
|
):
|
|
# If audio_file is not available and the path is not absolute, the full path is assumed
|
|
# to be relative to the manifest file parent directory or data directory.
|
|
if manifest_file is None and data_dir is None:
|
|
raise ValueError('Use either manifest_file or data_dir to specify the data directory.')
|
|
elif manifest_file is not None and data_dir is not None:
|
|
raise ValueError(
|
|
f'Parameters manifest_file and data_dir cannot be used simultaneously. '
|
|
f'Currently manifest_file is {manifest_file} and data_dir is {data_dir}.'
|
|
)
|
|
|
|
# resolve the data directory
|
|
if data_dir is None:
|
|
data_dir = os.path.dirname(manifest_file)
|
|
|
|
# assume audio_file path is relative to data_dir
|
|
audio_file_path = os.path.join(data_dir, audio_file)
|
|
|
|
if is_datastore_path(audio_file_path):
|
|
# If audio was originally on an object store, use locally-cached path.
|
|
# If the file could not be found in the local cache, it will be downloaded.
|
|
audio_file_path = get_datastore_object(audio_file_path) if force_cache else audio_file_path
|
|
return audio_file_path
|
|
|
|
if os.path.isfile(audio_file_path):
|
|
audio_file = os.path.abspath(audio_file_path)
|
|
else:
|
|
audio_file = expanduser(audio_file)
|
|
else:
|
|
audio_file = expanduser(audio_file)
|
|
return audio_file
|
|
else:
|
|
raise ValueError(f'Unexpected audio_file type {type(audio_file)}, audio_file {audio_file}.')
|