| """ |
| Copyright 2024 Johns Hopkins University (Author: Jesus Villalba) |
| Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0) |
| """ |
|
|
| import glob |
| import logging |
| from pathlib import Path |
| from typing import Optional |
|
|
| import numpy as np |
| import pandas as pd |
| from jsonargparse import ArgumentParser |
|
|
| from ..utils import ClassInfo, HyperDataset, RecordingSet, SegmentSet |
| from ..utils.misc import PathLike |
| from ..utils.scp_list import SCPList |
| from .data_prep import DataPrep |
|
|
|
|
| class LibriSpeechDataPrep(DataPrep): |
| """ |
| Prepares the LibriSpeech dataset into structured tables. |
| |
| This class processes a specified subset of the LibriSpeech corpus, including |
| speaker and chapter metadata, transcripts, and audio durations. It produces |
| a dataset suitable for training or evaluating speech and speaker recognition systems. |
| |
| Attributes: |
| corpus_dir (PathLike): Path to the base LibriSpeech directory. |
| subset (str): Subset to process (e.g., "train-clean-100"). |
| output_dir (PathLike): Output directory for the prepared dataset. |
| use_kaldi_ids (bool): Whether to prepend speaker ID to segment ID. |
| target_sample_freq (Optional[int]): Optional resampling target frequency. |
| num_threads (int): Number of threads to use for audio duration extraction. |
| """ |
|
|
| def __init__( |
| self, |
| corpus_dir: PathLike, |
| subset: str, |
| output_dir: PathLike, |
| use_kaldi_ids: bool = False, |
| target_sample_freq: Optional[int] = None, |
| num_threads: int = 10, |
| ): |
| """Initializes the LibriSpeech data preparation object.""" |
| super().__init__(corpus_dir, output_dir, False, target_sample_freq, num_threads) |
|
|
| self.subset = subset |
|
|
| @staticmethod |
| def dataset_name() -> str: |
| """Returns the dataset identifier name.""" |
| return "librispeech" |
|
|
| @staticmethod |
| def add_class_args(parser: ArgumentParser) -> None: |
| """Adds LibriSpeech-specific arguments to an argument parser.""" |
| DataPrep.add_class_args(parser) |
| parser.add_argument( |
| "--subset", |
| choices=[ |
| "train-clean-100", |
| "train-clean-360", |
| "train-other", |
| "dev-clean", |
| "dev-other", |
| "test-clean", |
| "test-other", |
| ], |
| help="""if we prepare the data for ["train-clean-100", "train-clean-360", "train-other", "dev-clean", "dev-other", "test-clean", "test-other"]""", |
| required=True, |
| ) |
|
|
| def _get_docs_dir(self) -> Path: |
| """ |
| Resolves the location of the metadata documentation directory. |
| |
| Returns: |
| Path: Path to the directory containing documentation files. |
| """ |
| docs_dir = self.corpus_dir / "docs" |
| if docs_dir.is_dir(): |
| return docs_dir |
| else: |
| return self.corpus_dir |
|
|
| def _get_spks_metadata(self) -> pd.DataFrame: |
| """ |
| Loads speaker metadata from SPEAKERS.TXT. |
| |
| Returns: |
| pd.DataFrame: DataFrame with speaker ID and gender. |
| """ |
|
|
| docs_dir = self._get_docs_dir() |
| file_path = docs_dir / "SPEAKERS.TXT" |
| |
| df = pd.read_csv( |
| file_path, |
| sep="\s*\|\s*", |
| header=None, |
| names=["id", "gender", "subset", "min", "name"], |
| skiprows=12, |
| engine="python", |
| dtype={"id": str}, |
| on_bad_lines="skip", |
| ) |
|
|
| df2 = pd.read_csv( |
| file_path, |
| sep="\s*\|\s*", |
| header=None, |
| names=["id", "gender", "subset", "min", "name", "d1", "d2"], |
| skiprows=44, |
| engine="python", |
| dtype={"id": str}, |
| nrows=1, |
| ) |
| df2.drop(columns=["d1", "d2"], inplace=True) |
| df = pd.concat([df, df2], ignore_index=True) |
| df["id"] = df["id"].apply(lambda x: f"libri-{x}") |
| df = df.loc[df["subset"] == self.subset] |
| df["gender"] = df["gender"].apply(lambda x: x.lower()) |
| df.drop(columns=["subset", "min", "name"], inplace=True) |
| return df |
|
|
| def _get_chapters_metadata(self) -> tuple[pd.DataFrame, pd.DataFrame]: |
| """ |
| Loads metadata for books and chapters. |
| |
| Returns: |
| Tuple[pd.DataFrame, pd.DataFrame]: Book and chapter metadata tables. |
| """ |
| docs_dir = self._get_docs_dir() |
| file_path = docs_dir / "BOOKS.TXT" |
| df_books = pd.read_csv( |
| file_path, |
| sep="\s*\|\s*", |
| header=None, |
| names=["book", "book_title", "dummy"], |
| engine="python", |
| dtype={"book": str}, |
| ) |
| df_books.drop(columns=["dummy"], inplace=True) |
|
|
| file_path = docs_dir / "CHAPTERS.TXT" |
| df_chapters = pd.read_csv( |
| file_path, |
| sep="\s*\|\s*", |
| header=None, |
| names=[ |
| "chapter", |
| "speaker", |
| "mins", |
| "subset", |
| "proj", |
| "book", |
| "chapter_title", |
| "proj_title", |
| ], |
| skiprows=14, |
| engine="python", |
| dtype={"chapter": str, "book": str, "speaker": str}, |
| ) |
| df_chapters = df_chapters.merge(df_books, how="left", on="book") |
| df_chapters["speaker"] = df_chapters["speaker"].apply(lambda x: f"libri-{x}") |
| df_chapters.drop(columns=["proj", "mins", "proj_title"], inplace=True) |
|
|
| return df_books, df_chapters |
|
|
| def _get_transcripts(self, rec_dir: Path) -> pd.DataFrame: |
| """ |
| Loads transcript files and returns a mapping of ID to transcript text. |
| |
| Args: |
| rec_dir (Path): Directory where audio and transcript files are located. |
| |
| Returns: |
| pd.DataFrame: DataFrame with columns ['id', 'transcript']. |
| """ |
| logging.info("searching transcript files in %s", str(rec_dir)) |
| trans_files = list(rec_dir.glob("**/*.trans.txt")) |
| if not trans_files: |
| |
| trans_files = [ |
| Path(f) for f in glob.iglob(f"{rec_dir}/**/.*trans.txt", recursive=True) |
| ] |
|
|
| assert len(trans_files) > 0, "transcript files not found" |
| dfs = [] |
| for trans_file in trans_files: |
| scp = SCPList.load(trans_file) |
| df_i = pd.DataFrame({"id": scp.key, "transcript": scp.file_path}) |
| dfs.append(df_i) |
|
|
| df_trans = pd.concat(dfs) |
| df_trans["id"] = df_trans["id"].apply(lambda x: f"librispeech-{x}") |
| return df_trans |
|
|
| def prepare(self) -> None: |
| """ |
| Executes the full LibriSpeech preparation pipeline: |
| - Loads audio files and metadata (speakers, chapters, books, transcripts) |
| - Extracts audio durations |
| - Builds segment, recording, and class info tables |
| - Writes the final dataset to output_dir |
| """ |
| logging.info( |
| "Peparing Libri %s corpus_dir:%s -> data_dir:%s", |
| self.subset, |
| self.corpus_dir, |
| self.output_dir, |
| ) |
|
|
| df_spks = self._get_spks_metadata() |
| df_books, df_chapters = self._get_chapters_metadata() |
|
|
| rec_dir = self.corpus_dir / self.subset |
| logging.info("searching audio files in %s", str(rec_dir)) |
| rec_files = list(rec_dir.glob("**/*.flac")) |
| if not rec_files: |
| |
| rec_files = [ |
| Path(f) for f in glob.iglob(f"{rec_dir}/**/*.flac", recursive=True) |
| ] |
|
|
| assert len(rec_files) > 0, "recording files not found" |
|
|
| df_trans = self._get_transcripts(rec_dir) |
|
|
| speakers = ["libri-" + f.parent.parent.name for f in rec_files] |
| chapters = [f.parent.name for f in rec_files] |
| rec_ids = ["librispeech-" + f.with_suffix("").name for f in rec_files] |
| if self.use_kaldi_ids: |
| rec_ids = [f"{s}-{f}" for f, s in zip(rec_ids, speakers)] |
|
|
| file_paths = [str(r) for r in rec_files] |
| logging.info("making RecordingSet") |
| recs = pd.DataFrame({"id": rec_ids, "storage_path": file_paths}) |
| recs = RecordingSet(recs) |
| recs.sort() |
|
|
| logging.info("getting recording durations") |
| recs.get_durations(self.num_threads) |
| if self.target_sample_freq: |
| recs["target_sample_freq"] = self.target_sample_freq |
|
|
| logging.info("making SegmentsSet") |
| df_segs = pd.DataFrame( |
| {"id": rec_ids, "speaker": speakers, "chapter": chapters} |
| ) |
| df_segs = df_segs.merge( |
| df_spks, how="left", left_on="speaker", right_on="id", suffixes=(None, "_y") |
| ) |
| df_segs.drop(columns=["id_y"], inplace=True) |
| df_segs = df_segs.merge(df_trans, how="left", on="id") |
| df_segs["duration"] = recs.loc[df_segs["id"], "duration"].values |
| df_segs = df_segs.merge(df_chapters, how="left", on="chapter") |
| df_segs.rename(columns={"speaker_x": "speaker"}, inplace=True) |
| df_segs.drop(columns=["speaker_y"], inplace=True) |
| df_segs["language"] = "eng" |
| df_segs["corpusid"] = "librivox" |
| df_segs["dataset"] = "librispeech" |
| df_segs["original_bandwidth"] = 8000 |
| segments = SegmentSet(df_segs) |
| segments.sort() |
|
|
| logging.info("making speaker info file") |
| df_spks = df_spks.loc[df_spks["id"].isin(df_segs["speaker"].values)] |
| speakers = ClassInfo(df_spks) |
|
|
| logging.info("making book info file") |
| df_books.rename(columns={"book": "id"}, inplace=True) |
| df_books = df_books.loc[df_books["id"].isin(df_segs["book"].values)] |
| books = ClassInfo(df_books) |
|
|
| logging.info("making chapter info file") |
| df_chapters.rename(columns={"chapter": "id"}, inplace=True) |
| df_chapters = df_chapters.loc[df_chapters["id"].isin(df_segs["chapter"].values)] |
| chapters = ClassInfo(df_chapters) |
|
|
| logging.info("making language info file") |
| languages = ClassInfo(pd.DataFrame({"id": ["eng"]})) |
| logging.info("making gender info file") |
| genders = ClassInfo(pd.DataFrame({"id": ["m", "f"]})) |
|
|
| classes = { |
| "speaker": speakers, |
| "book": books, |
| "chapter": chapters, |
| "language": languages, |
| "gender": genders, |
| } |
|
|
| logging.info("making dataset") |
| dataset = HyperDataset( |
| segments, |
| classes=classes, |
| recordings=recs, |
| ) |
| logging.info("saving dataset at %s", self.output_dir) |
| dataset.save(self.output_dir) |
| dataset.describe() |
|
|