Spaces:
Running on Zero
Running on Zero
| # Copyright (c) Meta Platforms, Inc. and affiliates. | |
| # All rights reserved. | |
| # | |
| # This source code is licensed under the license found in the | |
| # LICENSE file in the root directory of this source tree. | |
| import argparse | |
| import copy | |
| from concurrent.futures import ThreadPoolExecutor, Future | |
| from dataclasses import dataclass, fields | |
| from contextlib import ExitStack | |
| import gzip | |
| import json | |
| import logging | |
| import os | |
| from pathlib import Path | |
| import random | |
| import sys | |
| import typing as tp | |
| import torch | |
| import torch.nn.functional as F | |
| from .audio import audio_read, audio_info | |
| from .audio_utils import convert_audio | |
| from .zip import PathInZip | |
| try: | |
| import dora | |
| except ImportError: | |
| dora = None # type: ignore | |
| class BaseInfo: | |
| def _dict2fields(cls, dictionary: dict): | |
| return { | |
| field.name: dictionary[field.name] | |
| for field in fields(cls) if field.name in dictionary | |
| } | |
| def from_dict(cls, dictionary: dict): | |
| _dictionary = cls._dict2fields(dictionary) | |
| return cls(**_dictionary) | |
| def to_dict(self): | |
| return { | |
| field.name: self.__getattribute__(field.name) | |
| for field in fields(self) | |
| } | |
| class AudioMeta(BaseInfo): | |
| path: str | |
| duration: float | |
| sample_rate: int | |
| amplitude: tp.Optional[float] = None | |
| weight: tp.Optional[float] = None | |
| # info_path is used to load additional information about the audio file that is stored in zip files. | |
| info_path: tp.Optional[PathInZip] = None | |
| def from_dict(cls, dictionary: dict): | |
| base = cls._dict2fields(dictionary) | |
| if 'info_path' in base and base['info_path'] is not None: | |
| base['info_path'] = PathInZip(base['info_path']) | |
| return cls(**base) | |
| def to_dict(self): | |
| d = super().to_dict() | |
| if d['info_path'] is not None: | |
| d['info_path'] = str(d['info_path']) | |
| return d | |
| class SegmentInfo(BaseInfo): | |
| meta: AudioMeta | |
| seek_time: float | |
| n_frames: int # actual number of frames without padding | |
| total_frames: int # total number of frames, padding included | |
| sample_rate: int # actual sample rate | |
| DEFAULT_EXTS = ['.wav', '.mp3', '.flac', '.ogg', '.m4a'] | |
| logger = logging.getLogger(__name__) | |
| def _get_audio_meta(file_path: str, minimal: bool = True) -> AudioMeta: | |
| """AudioMeta from a path to an audio file. | |
| Args: | |
| file_path (str): Resolved path of valid audio file. | |
| minimal (bool): Whether to only load the minimal set of metadata (takes longer if not). | |
| Returns: | |
| AudioMeta: Audio file path and its metadata. | |
| """ | |
| info = audio_info(file_path) | |
| amplitude: tp.Optional[float] = None | |
| if not minimal: | |
| wav, sr = audio_read(file_path) | |
| amplitude = wav.abs().max().item() | |
| return AudioMeta(file_path, info.duration, info.sample_rate, amplitude) | |
| def _resolve_audio_meta(m: AudioMeta, fast: bool = True) -> AudioMeta: | |
| """If Dora is available as a dependency, try to resolve potential relative paths | |
| in list of AudioMeta. This method is expected to be used when loading meta from file. | |
| Args: | |
| m (AudioMeta): Audio meta to resolve. | |
| fast (bool): If True, uses a really fast check for determining if a file is already absolute or not. | |
| Only valid on Linux/Mac. | |
| Returns: | |
| AudioMeta: Audio meta with resolved path. | |
| """ | |
| def is_abs(m): | |
| if fast: | |
| return str(m)[0] == '/' | |
| else: | |
| os.path.isabs(str(m)) | |
| if not dora: | |
| return m | |
| if not is_abs(m.path): | |
| m.path = dora.git_save.to_absolute_path(m.path) | |
| if m.info_path is not None and not is_abs(m.info_path.zip_path): | |
| m.info_path.zip_path = dora.git_save.to_absolute_path(m.path) | |
| return m | |
| def find_audio_files(path: tp.Union[Path, str], | |
| exts: tp.List[str] = DEFAULT_EXTS, | |
| resolve: bool = True, | |
| minimal: bool = True, | |
| progress: bool = False, | |
| workers: int = 0) -> tp.List[AudioMeta]: | |
| """Build a list of AudioMeta from a given path, | |
| collecting relevant audio files and fetching meta info. | |
| Args: | |
| path (str or Path): Path to folder containing audio files. | |
| exts (list of str): List of file extensions to consider for audio files. | |
| minimal (bool): Whether to only load the minimal set of metadata (takes longer if not). | |
| progress (bool): Whether to log progress on audio files collection. | |
| workers (int): number of parallel workers, if 0, use only the current thread. | |
| Returns: | |
| List[AudioMeta]: List of audio file path and its metadata. | |
| """ | |
| audio_files = [] | |
| futures: tp.List[Future] = [] | |
| pool: tp.Optional[ThreadPoolExecutor] = None | |
| with ExitStack() as stack: | |
| if workers > 0: | |
| pool = ThreadPoolExecutor(workers) | |
| stack.enter_context(pool) | |
| if progress: | |
| print("Finding audio files...") | |
| for root, folders, files in os.walk(path, followlinks=True): | |
| for file in files: | |
| full_path = Path(root) / file | |
| if full_path.suffix.lower() in exts: | |
| audio_files.append(full_path) | |
| if pool is not None: | |
| futures.append(pool.submit(_get_audio_meta, str(audio_files[-1]), minimal)) | |
| if progress: | |
| print(format(len(audio_files), " 8d"), end='\r', file=sys.stderr) | |
| if progress: | |
| print("Getting audio metadata...") | |
| meta: tp.List[AudioMeta] = [] | |
| for idx, file_path in enumerate(audio_files): | |
| try: | |
| if pool is None: | |
| m = _get_audio_meta(str(file_path), minimal) | |
| else: | |
| m = futures[idx].result() | |
| if resolve: | |
| m = _resolve_audio_meta(m) | |
| except Exception as err: | |
| print("Error with", str(file_path), err, file=sys.stderr) | |
| continue | |
| meta.append(m) | |
| if progress: | |
| print(format((1 + idx) / len(audio_files), " 3.1%"), end='\r', file=sys.stderr) | |
| meta.sort() | |
| return meta | |
| def load_audio_meta(path: tp.Union[str, Path], | |
| resolve: bool = True, fast: bool = True) -> tp.List[AudioMeta]: | |
| """Load list of AudioMeta from an optionally compressed json file. | |
| Args: | |
| path (str or Path): Path to JSON file. | |
| resolve (bool): Whether to resolve the path from AudioMeta (default=True). | |
| fast (bool): activates some tricks to make things faster. | |
| Returns: | |
| List[AudioMeta]: List of audio file path and its total duration. | |
| """ | |
| open_fn = gzip.open if str(path).lower().endswith('.gz') else open | |
| with open_fn(path, 'rb') as fp: # type: ignore | |
| lines = fp.readlines() | |
| meta = [] | |
| for line in lines: | |
| d = json.loads(line) | |
| m = AudioMeta.from_dict(d) | |
| if resolve: | |
| m = _resolve_audio_meta(m, fast=fast) | |
| meta.append(m) | |
| return meta | |
| def save_audio_meta(path: tp.Union[str, Path], meta: tp.List[AudioMeta]): | |
| """Save the audio metadata to the file pointer as json. | |
| Args: | |
| path (str or Path): Path to JSON file. | |
| metadata (list of BaseAudioMeta): List of audio meta to save. | |
| """ | |
| Path(path).parent.mkdir(exist_ok=True, parents=True) | |
| open_fn = gzip.open if str(path).lower().endswith('.gz') else open | |
| with open_fn(path, 'wb') as fp: # type: ignore | |
| for m in meta: | |
| json_str = json.dumps(m.to_dict()) + '\n' | |
| json_bytes = json_str.encode('utf-8') | |
| fp.write(json_bytes) | |
| def main(): | |
| logging.basicConfig(stream=sys.stderr, level=logging.INFO) | |
| parser = argparse.ArgumentParser( | |
| prog='audio_dataset', | |
| description='Generate .jsonl files by scanning a folder.') | |
| parser.add_argument('root', help='Root folder with all the audio files') | |
| parser.add_argument('output_meta_file', | |
| help='Output file to store the metadata, ') | |
| parser.add_argument('--complete', | |
| action='store_false', dest='minimal', default=True, | |
| help='Retrieve all metadata, even the one that are expansive ' | |
| 'to compute (e.g. normalization).') | |
| parser.add_argument('--resolve', | |
| action='store_true', default=False, | |
| help='Resolve the paths to be absolute and with no symlinks.') | |
| parser.add_argument('--workers', | |
| default=10, type=int, | |
| help='Number of workers.') | |
| args = parser.parse_args() | |
| meta = find_audio_files(args.root, DEFAULT_EXTS, progress=True, | |
| resolve=args.resolve, minimal=args.minimal, workers=args.workers) | |
| save_audio_meta(args.output_meta_file, meta) | |
| if __name__ == '__main__': | |
| main() | |