montreal_forced_aligner/corpus/classes.py
class UtteranceCollection(Collection):
    """
    Utility class for storing collections of speakers
    """

    CLASS_TYPE = Utterance

    def add_utterance(self, utterance: Utterance) -> None:
        """
        Add utterance to the collection

        Parameters
        ----------
        speaker: :class:`~montreal_forced_aligner.corpus.classes.Utterance`
            Utterance to be added
        """
        self[utterance.name] = utterance

    def __iter__(self) -> Generator[Utterance]:
        """Iterator over the collection"""
        for v in self._data.values():
            if v.ignored:
                continue
            yield v

    def __repr__(self) -> str:
        """Object representation"""
        return f"<UtteranceCollection of {self._data}>"
montreal_forced_aligner/corpus/classes.py
class FileCollection(Collection):
    """
    Utility class for storing collections of speakers
    """

    CLASS_TYPE = File

    def __init__(self):
        super(FileCollection, self).__init__()
        self.lab_count = 0
        self.textgrid_count = 0
        self.sound_file_count = 0

    def add_file(self, file: File) -> None:
        """
        Add file to the collection

        Parameters
        ----------
        speaker: :class:`~montreal_forced_aligner.corpus.classes.File`
            File to be added
        """
        self[file.name] = file
        if file.text_type == TextFileType.TEXTGRID:
            self.textgrid_count += 1
        elif file.text_type == TextFileType.LAB:
            self.lab_count += 1
        if file.sound_type != SoundFileType.NONE:
            self.sound_file_count += 1

    def __repr__(self) -> str:
        """Object representation"""
        return f"<FileCollection of {self._data}>"
montreal_forced_aligner/data.py
class SoundFileInformation:
    """
    Data class for sound file information with format, duration, number of channels, bit depth, and
        sox_string for use in Kaldi feature extraction if necessary

    Parameters
    ----------
    format: str
        Format of the sound file
    sample_rate: int
        Sample rate
    duration: float
        Duration
    sample_rate: int
        Sample rate
    bit_depth: int
        Bit depth
    sox_string: str
        String to use for loading with sox
    """

    __slots__ = ["format", "sample_rate", "duration", "num_channels", "bit_depth", "sox_string"]
    format: str
    sample_rate: int
    duration: float
    num_channels: int
    bit_depth: int
    sox_string: str

    @property
    def meta(self) -> typing.Dict[str, typing.Any]:
        return dataclasses.asdict(self)
montreal_forced_aligner/exceptions.py
class CorpusError(MFAError):
    """
    Class for errors in creating Corpus objects
    """

    pass
montreal_forced_aligner/corpus/classes.py
class SpeakerCollection(Collection):
    """
    Utility class for storing collections of speakers
    """

    CLASS_TYPE = Speaker

    def add_speaker(self, speaker: Speaker) -> None:
        """
        Add speaker to the collection

        Parameters
        ----------
        speaker: :class:`~montreal_forced_aligner.corpus.classes.Speaker`
            Speaker to be added
        """
        self[speaker.name] = speaker

    def __repr__(self) -> str:
        """Object representation"""
        return f"<SpeakerCollection of {self._data}>"
montreal_forced_aligner/utils.py
class Stopped(object):
    """
    Multiprocessing class for detecting whether processes should stop processing and exit ASAP

    Attributes
    ----------
    val: :func:`~multiprocessing.Value`
        0 if not stopped, 1 if stopped
    lock: :class:`~multiprocessing.Lock`
        Lock for process safety
    _source: multiprocessing.Value
        1 if it was a Ctrl+C event that stopped it, 0 otherwise
    """

    def __init__(self, initval: Union[bool, int] = False):
        self.val = mp.Value("i", initval)
        self.lock = mp.Lock()
        self._source = mp.Value("i", 0)

    def stop(self) -> None:
        """Signal that work should stop asap"""
        with self.lock:
            self.val.value = True

    def stop_check(self) -> int:
        """Check whether a process should stop"""
        with self.lock:
            return self.val.value

    def set_sigint_source(self) -> None:
        """Set the source as a ctrl+c"""
        with self.lock:
            self._source.value = True

    def source(self) -> int:
        """Get the source value"""
        with self.lock:
            return self._source.value
montreal_forced_aligner/corpus/classes.py
class Utterance(MfaCorpusClass):
    """
    Class for information about specific utterances

    Parameters
    ----------
    speaker: :class:`~montreal_forced_aligner.corpus.classes.Speaker`
        Speaker of the utterance
    file: :class:`~montreal_forced_aligner.corpus.classes.File`
        File that the utterance belongs to
    begin: float, optional
        Start time of the utterance,
        if None, then the utterance is assumed to start at 0
    end: float, optional
        End time of the utterance,
        if None, then the utterance is assumed to end at the end of the File
    channel: int, optional
        Channel in the file, if None, then assumed to be the first/only channel
    text: str, optional
        Text transcription of the utterance

    Attributes
    ----------
    file_name: str
        Saved File.name property for reconstructing objects following serialization
    speaker_name: str
        Saved Speaker.name property for reconstructing objects following serialization
    transcription_text: str, optional
        Output of transcription is saved here
    ignored: bool
        The ignored flag is set if feature generation does not work for this utterance, or it is too short to
        be processed by Kaldi
    features: str, optional
        Feature string reference to the computed features archive
    phone_labels: list[:class:`~montreal_forced_aligner.data.CtmInterval`], optional
        Saved aligned phone labels
    word_labels: list[:class:`~montreal_forced_aligner.data.CtmInterval`], optional
        Saved aligned word labels
    oovs: list[str]
        Words not found in the dictionary for this utterance
    """

    def __init__(
        self,
        speaker: Speaker,
        file: File,
        begin: Optional[float] = None,
        end: Optional[float] = None,
        channel: Optional[int] = 0,
        text: Optional[str] = None,
    ):
        self.speaker = speaker
        self.file = file
        self.file_name = file.name
        self.speaker_name: str = speaker.name
        if begin is None:
            begin = 0
        if end is None:
            end = self.file.duration
        self.begin = begin
        self.end = end
        self.channel = channel
        self.text = text
        self.transcription_text = None
        self.ignored = False
        self.features = None
        self.phone_labels: Optional[List[CtmInterval]] = None
        self.word_labels: Optional[List[CtmInterval]] = None
        self.reference_phone_labels: Optional[List[CtmInterval]] = []
        self.oovs = set()
        self.normalized_text = []
        self.text_int = []
        self.alignment_log_likelihood = None
        self.word_error_rate = None
        self.character_error_rate = None
        self.phone_error_rate = None
        self.alignment_score = None

    def parse_transcription(self, sanitize_function=Optional[MultispeakerSanitizationFunction]):
        """
        Parse an orthographic transcription given punctuation and clitic markers

        Parameters
        ----------
        sanitize_function: :class:`~montreal_forced_aligner.dictionary.multispeaker.MultispeakerSanitizationFunction`, optional
            Function to sanitize words and strip punctuation

        """
        self.normalized_text = []
        if sanitize_function is not None:
            try:
                sanitize, split = sanitize_function.get_functions_for_speaker(self.speaker_name)
            except AttributeError:
                sanitize = sanitize_function
                split = None
            words = [
                sanitize(w)
                for w in self.text.split()
                if w and w not in sanitize.clitic_markers + sanitize.compound_markers
            ]
            self.text = " ".join(words)
            if split is not None:
                for w in words:
                    for new_w in split(w):
                        if not new_w:
                            continue
                        if split.word_set is not None and new_w not in split.word_set:
                            self.oovs.add(new_w)
                        self.normalized_text.append(new_w)

    @property
    def multiprocessing_data(self):
        return UtteranceData(
            self.speaker_name,
            self.file_name,
            self.begin,
            self.end,
            self.channel,
            self.text,
            self.normalized_text,
            self.oovs,
        )

    @classmethod
    def load_from_mp_data(cls, data: UtteranceData, file: File) -> Utterance:
        utterance = Utterance(
            speaker=Speaker(data.speaker_name),
            file=file,
            begin=data.begin,
            end=data.end,
            channel=data.channel,
            text=data.text,
        )
        if data.normalized_text:
            utterance.normalized_text = data.normalized_text
            utterance.oovs = data.oovs
        return utterance

    def __str__(self) -> str:
        """String representation"""
        return self.name

    def __repr__(self) -> str:
        """Object representation"""
        return f'<Utterance "{self.name}">'

    def __eq__(self, other: Union[Utterance, str]) -> bool:
        """Check if an Utterance is equal to another Utterance"""
        if isinstance(other, Utterance):
            return other.name == self.name
        if isinstance(other, str):
            return self.name == other
        raise TypeError("Utterances can only be compared to other utterances and strings.")

    def __lt__(self, other: Union[Utterance, str]) -> bool:
        """Check if an Utterance is less than another Utterance"""
        if isinstance(other, Utterance):
            return other.name < self.name
        if isinstance(other, str):
            return self.name < other
        raise TypeError("Utterances can only be compared to other utterances and strings.")

    def __gt__(self, other: Union[Utterance, str]) -> bool:
        """Check if an Utterance is greater than another Utterance"""
        if isinstance(other, Utterance):
            return other.name > self.name
        if isinstance(other, str):
            return self.name > other
        raise TypeError("Utterances can only be compared to other utterances and strings.")

    def __hash__(self) -> hash:
        """Compute the hash of this function"""
        return hash(self.name)

    @property
    def duration(self) -> float:
        """Duration of the utterance"""
        if self.begin is not None and self.end is not None:
            return self.end - self.begin
        return self.file.duration

    @property
    def meta(self) -> Dict[str, Any]:
        """Metadata dictionary for the utterance"""
        return {
            "speaker": self.speaker.name,
            "file": self.file.name,
            "begin": self.begin,
            "end": self.end,
            "channel": self.channel,
            "text": self.text,
            "ignored": self.ignored,
            "features": self.features,
            "normalized_text": self.normalized_text,
            "oovs": self.oovs,
            "transcription_text": self.transcription_text,
            "reference_phone_labels": self.reference_phone_labels,
            "phone_labels": self.phone_labels,
            "word_labels": self.word_labels,
            "word_error_rate": self.word_error_rate,
            "character_error_rate": self.character_error_rate,
            "phone_error_rate": self.phone_error_rate,
            "alignment_score": self.alignment_score,
            "alignment_log_likelihood": self.alignment_log_likelihood,
        }

    def set_speaker(self, speaker: Speaker) -> None:
        """
        Set the speaker of the utterance and updates other objects

        Parameters
        ----------
        speaker: :class:`~montreal_forced_aligner.corpus.classes.Speaker`
            New speaker
        """
        self.speaker = speaker
        self.speaker.add_utterance(self)
        self.file.add_utterance(self)

    @property
    def is_segment(self) -> bool:
        """Check if this utterance is a segment of a longer file"""
        return (
            self.begin is not None
            and self.end is not None
            and self.begin != 0
            and self.end != self.file.duration
        )

    def add_word_intervals(self, intervals: Union[CtmInterval, List[CtmInterval]]) -> None:
        """
        Add aligned word intervals for the utterance

        Parameters
        ----------
        intervals: Union[CtmInterval, list[CtmInterval]]
            Intervals to add
        """
        if not isinstance(intervals, list):
            intervals = [intervals]
        if self.word_labels is None:
            self.word_labels = []
        if self.is_segment:
            for interval in intervals:
                interval.shift_times(self.begin)
        self.word_labels = intervals

    def add_phone_intervals(self, intervals: Union[CtmInterval, List[CtmInterval]]) -> None:
        """
        Add aligned phone intervals for the utterance

        Parameters
        ----------
        intervals: Union[CtmInterval, list[CtmInterval]]
            Intervals to add
        """
        if not isinstance(intervals, list):
            intervals = [intervals]
        if self.phone_labels is None:
            self.phone_labels = []
        if self.is_segment:
            for interval in intervals:
                interval.shift_times(self.begin)
        self.phone_labels = intervals

    def text_for_scp(self) -> List[str]:
        """
        Generate the text for exporting to Kaldi's text scp

        Returns
        -------
        list[str]
            List of words
        """
        return self.text.split()

    def text_int_for_scp(self) -> Optional[List[int]]:
        """
        Generate the text for exporting to Kaldi's text int scp

        Returns
        -------
        list[int]
            List of word IDs, or None if the utterance's speaker doesn't have an associated dictionary
        """
        if self.speaker.dictionary is None:
            return
        if self.text_int:
            return self.text_int
        if self.normalized_text:
            normalized = True
            text = self.normalized_text
        else:
            normalized = False
            text = self.text_for_scp()
        self.text_int = []
        for i, t in enumerate(text):
            lookup = self.speaker.dictionary.to_int(t, normalized)
            if self.speaker.dictionary.oov_int in lookup:
                self.oovs.add(text[i])
            self.text_int.extend(lookup)
        return self.text_int

    def segment_for_scp(self) -> List[Any]:
        """
        Generate data for Kaldi's segments scp file

        Returns
        -------
        list[Any]
            Segment data
        """
        return [self.file_name.replace(" ", "_MFASPACE_"), self.begin, self.end, self.channel]

    @property
    def name(self) -> str:
        """The name of the utterance"""
        base = f"{self.file_name}"
        base = base.replace(" ", "-space-").replace(".", "-").replace("_", "-")
        if not base.startswith(f"{self.speaker_name}-"):
            base = f"{self.speaker_name}-" + base
        if self.is_segment:
            base = f"{base}-{self.begin}-{self.end}"
        return base.replace(" ", "-space-").replace(".", "-").replace("_", "-")
montreal_forced_aligner/corpus/multiprocessing.py
class Job:
    """
    Class representing information about corpus jobs that will be run in parallel.
    Jobs have a set of speakers that they will process, along with all files and utterances associated with that speaker.
    As such, Jobs also have a set of dictionaries that the speakers use, and argument outputs are largely dependent on
    the pronunciation dictionaries in use.

    Parameters
    ----------
    name: int
        Job number is the job's identifier

    Attributes
    ----------
    speakers: list[:class:`~montreal_forced_aligner.corpus.classes.Speaker`]
        List of speakers associated with this job
    dictionaries: set[:class:`~montreal_forced_aligner.dictionary.PronunciationDictionary`]
        Set of dictionaries that the job's speakers use
    subset_utts: set[:class:`~montreal_forced_aligner.corpus.classes.Utterance`]
        When trainers are just using a subset of the corpus, the subset of utterances on each job will be set and used to
        filter the job's utterances
    subset_speakers: set[:class:`~montreal_forced_aligner.corpus.classes.Speaker`]
        When subset_utts is set, this property will be calculated as the subset of speakers that the utterances correspond to
    subset_dictionaries: set[:class:`~montreal_forced_aligner.dictionary.PronunciationDictionary`]
        Subset of dictionaries that the subset of speakers use

    """

    name: int
    speakers: SpeakerCollection
    dictionaries: Set[PronunciationDictionaryMixin]

    def __init__(self, name: int):
        self.name = name
        self.speakers = SpeakerCollection()
        self.files = FileCollection()
        self.dictionaries = set()

        self.subset_utts = set()
        self.subset_files = set()
        self.subset_speakers = set()
        self.subset_dictionaries = set()

    def add_speaker(self, speaker: Speaker) -> None:
        """
        Add a speaker to a job

        Parameters
        ----------
        speaker: :class:`~montreal_forced_aligner.corpus.classes.Speaker`
            Speaker to add
        """
        self.speakers.add_speaker(speaker)
        for u in speaker.utterances:
            self.files.add_file(u.file)
        self.dictionaries.add(speaker.dictionary)

    def set_subset(self, subset_utts: Optional[UtteranceCollection]) -> None:
        """
        Set the current subset for the trainer

        Parameters
        ----------
        subset_utts: Collection[:class:`~montreal_forced_aligner.corpus.classes.Utterance`], optional
            Subset of utterances for this job to use
        """
        self.subset_utts = set()
        self.subset_files = set()
        self.subset_speakers = set()
        self.subset_dictionaries = set()
        if subset_utts:
            for u in subset_utts:
                if u.speaker not in self.speakers:
                    continue
                self.subset_utts.add(u.name)
                self.subset_speakers.add(u.speaker_name)
                self.subset_files.add(u.file_name)
                dict_name = self.speakers[u.speaker_name].dictionary_name
                if dict_name is not None:
                    self.subset_dictionaries.add(dict_name)

    def text_scp_data(self) -> Dict[str, Dict[str, str]]:
        """
        Generate the job's data for Kaldi's text scp files

        Returns
        -------
        dict[str, dict[str, str]]
            Text for each utterance, per dictionary name
        """
        data = {x: {} for x in self.current_dictionary_names}
        for utt in self.current_utterances:
            dict_name = utt.speaker.dictionary_name
            if not utt.text:
                continue
            data[dict_name][utt.name] = utt.text
        return data

    def text_int_scp_data(self) -> Dict[str, Dict[str, str]]:
        """
        Generate the job's data for Kaldi's text int scp files

        Returns
        -------
        dict[str, dict[str, str]]
            Text converted to integer IDs for each utterance, per dictionary name
        """
        data = {x: {} for x in self.current_dictionary_names}
        for utt in self.current_utterances:
            dict_name = utt.speaker.dictionary_name
            if dict_name is None:
                continue
            if not utt.text:
                continue
            data[dict_name][utt.name] = " ".join(map(str, utt.text_int_for_scp()))
            utt.speaker.dictionary.oovs_found.update(utt.oovs)
        return data

    def wav_scp_data(self) -> Dict[str, str]:
        """
        Generate the job's data for Kaldi's wav scp files

        Returns
        -------
        dict[str, str]
            Wav scp strings for each file, per dictionary name
        """
        data = {}
        for file in self.files:
            if any(u.is_segment for u in file.utterances):  # Segmented file
                data[file.name] = file.for_wav_scp()
            else:
                for utt in file.utterances:
                    data[utt.name] = file.for_wav_scp()
        return data

    def utt2spk_scp_data(self) -> Dict[str, Dict[str, str]]:
        """
        Generate the job's data for Kaldi's utt2spk scp files

        Returns
        -------
        dict[str, dict[str, str]]
            Utterance to speaker mapping, per dictionary name
        """
        data = {x: {} for x in self.current_dictionary_names}
        for utt in self.current_utterances:
            dict_name = utt.speaker.dictionary_name
            data[dict_name][utt.name] = utt.speaker_name
        return data

    def feat_scp_data(self) -> Dict[str, Dict[str, str]]:
        """
        Generate the job's data for Kaldi's feature scp files

        Returns
        -------
        dict[str, dict[str, str]]
            Utterance to feature archive ID mapping, per dictionary name
        """
        data = {x: {} for x in self.current_dictionary_names}
        for utt in self.current_utterances:
            if utt.features is None:
                continue
            dict_name = utt.speaker.dictionary_name
            data[dict_name][utt.name] = utt.features
        return data

    def spk2utt_scp_data(self) -> Dict[str, Dict[str, List[str]]]:
        """
        Generate the job's data for Kaldi's spk2utt scp files

        Returns
        -------
        dict[str, dict[str, list[str]]]
            Speaker to utterance mapping, per dictionary name
        """
        data = {x: {} for x in self.current_dictionary_names}
        for utt in self.current_utterances:
            dict_name = utt.speaker.dictionary_name
            if utt.speaker_name not in data[dict_name]:
                data[dict_name][utt.speaker_name] = []
            data[dict_name][utt.speaker_name].append(str(utt))
        for k, v in data.items():
            for s, utts in v.items():
                data[k][s] = sorted(utts)
        return data

    def cmvn_scp_data(self) -> Dict[str, Dict[str, str]]:
        """
        Generate the job's data for Kaldi's CMVN scp files

        Returns
        -------
        dict[str, dict[str, str]]
            Speaker to CMVN mapping, per dictionary name
        """
        data = {x: {} for x in self.current_dictionary_names}
        for s in self.current_speakers:
            if s.dictionary is None:
                key = None
            else:
                key = s.dictionary_name
            if s.cmvn:
                data[key][s.name] = s.cmvn
        return data

    def segments_scp_data(self) -> Dict[str, List[Any]]:
        """
        Generate the job's data for Kaldi's segments scp files

        Returns
        -------
        dict[str, list[Any]]
            Utterance to segment mapping, per dictionary name
        """
        data = {}
        for utt in self.current_utterances:
            if not utt.is_segment:
                continue
            data[utt.name] = utt.segment_for_scp()
        return data

    def construct_path_dictionary(
        self, directory: str, identifier: str, extension: str
    ) -> Dict[str, str]:
        """
        Helper function for constructing dictionary-dependent paths for the Job

        Parameters
        ----------
        directory: str
            Directory to use as the root
        identifier: str
            Identifier for the path name, like ali or acc
        extension: str
            Extension of the path, like .scp or .ark

        Returns
        -------
        dict[str, str]
            Path for each dictionary
        """
        output = {}
        for dict_name in self.current_dictionary_names:
            output[dict_name] = os.path.join(
                directory, f"{identifier}.{dict_name}.{self.name}.{extension}"
            )
        return output

    def construct_path(self, directory: str, identifier: str, extension: str) -> str:
        """
        Helper function for constructing dictionary-dependent paths for the Job

        Parameters
        ----------
        directory: str
            Directory to use as the root
        identifier: str
            Identifier for the path name, like ali or acc
        extension: str
            Extension of the path, like .scp or .ark

        Returns
        -------
        str
            Path
        """
        return os.path.join(directory, f"{identifier}.{self.name}.{extension}")

    def construct_dictionary_dependent_paths(
        self, directory: str, identifier: str, extension: str
    ) -> Dict[str, str]:
        """
        Helper function for constructing paths that depend only on the dictionaries of the job, and not the job name itself.
        These paths should be merged with all other jobs to get a full set of dictionary paths.

        Parameters
        ----------
        directory: str
            Directory to use as the root
        identifier: str
            Identifier for the path name, like ali or acc
        extension: str
            Extension of the path, like .scp or .ark

        Returns
        -------
        dict[str, str]
            Path for each dictionary
        """
        output = {}
        for dict_name in self.current_dictionary_names:
            output[dict_name] = os.path.join(directory, f"{identifier}.{dict_name}.{extension}")
        return output

    @property
    def dictionary_count(self):
        """Number of dictionaries currently used"""
        if self.subset_dictionaries:
            return len(self.subset_dictionaries)
        return len(self.dictionaries)

    @property
    def current_dictionaries(self) -> Collection[PronunciationDictionaryMixin]:
        """Current dictionaries depending on whether a subset is being used"""
        if self.subset_dictionaries:
            return [x for x in self.dictionaries if x.name in self.subset_dictionaries]
        return self.dictionaries

    @property
    def current_dictionary_names(self) -> List[Optional[str]]:
        """Current dictionary names depending on whether a subset is being used"""
        if self.subset_dictionaries:
            return sorted(self.subset_dictionaries)
        if self.dictionaries == {None}:
            return [None]
        return sorted(x.name for x in self.dictionaries)

    @property
    def current_speakers(self) -> Generator[Speaker]:
        """Current dictionary names depending on whether a subset is being used"""

        return self.speakers.subset(self.subset_speakers)

    @property
    def current_utterances(self) -> Generator[Utterance]:
        """Current dictionary names depending on whether a subset is being used"""
        for s in self.current_speakers:
            for u in s.utterances.subset(self.subset_utts):
                yield u

    def word_boundary_int_files(self) -> Dict[str, str]:
        """
        Generate mapping for dictionaries to word boundary int files

        Returns
        -------
        dict[str, str]
            Per dictionary word boundary int files
        """
        data = {}
        for dictionary in self.current_dictionaries:
            data[dictionary.name] = os.path.join(dictionary.phones_dir, "word_boundary.int")
        return data

    def output_for_features(self, split_directory: str) -> None:
        """
        Output the necessary files for Kaldi to generate features

        Parameters
        ----------
        split_directory: str
            Split directory for the corpus
        """
        wav_scp_path = self.construct_path(split_directory, "wav", "scp")
        if not os.path.exists(wav_scp_path):
            wav_scp = self.wav_scp_data()
            output_mapping(wav_scp, wav_scp_path, skip_safe=True)

        segments_scp_path = self.construct_path(split_directory, "segments", "scp")
        if not os.path.exists(segments_scp_path):
            segments = self.segments_scp_data()
            output_mapping(segments, segments_scp_path)

    def output_to_directory(self, split_directory: str) -> None:
        """
        Output job information to a directory

        Parameters
        ----------
        split_directory: str
            Directory to output to
        """

        spk2utt = self.spk2utt_scp_data()
        for dict_name, scp in spk2utt.items():
            spk2utt_scp_path = os.path.join(
                split_directory, f"spk2utt.{dict_name}.{self.name}.scp"
            )

            output_mapping(scp, spk2utt_scp_path)

        feats = self.feat_scp_data()
        for dict_name, scp in feats.items():
            feats_scp_path = os.path.join(split_directory, f"feats.{dict_name}.{self.name}.scp")
            output_mapping(scp, feats_scp_path)

        cmvn = self.cmvn_scp_data()
        for dict_name, scp in cmvn.items():
            cmvn_scp_path = os.path.join(split_directory, f"cmvn.{dict_name}.{self.name}.scp")
            output_mapping(scp, cmvn_scp_path)

        utt2spk = self.utt2spk_scp_data()
        for dict_name, scp in utt2spk.items():
            utt2spk_scp_path = os.path.join(
                split_directory, f"utt2spk.{dict_name}.{self.name}.scp"
            )
            output_mapping(scp, utt2spk_scp_path)

        text_scp = self.text_scp_data()
        for dict_name, scp in text_scp.items():
            if not scp:
                continue
            text_scp_path = os.path.join(split_directory, f"text.{dict_name}.{self.name}.scp")
            output_mapping(scp, text_scp_path, skip_safe=True)

        text_int = self.text_int_scp_data()
        for dict_name, scp in text_int.items():
            if dict_name is None:
                continue
            if not scp:
                continue
            text_int_scp_path = os.path.join(
                split_directory, f"text.{dict_name}.{self.name}.int.scp"
            )
            output_mapping(scp, text_int_scp_path, skip_safe=True)
montreal_forced_aligner/corpus/classes.py
class File(MfaCorpusClass):
    """
    File class for representing metadata and associations of Files

    Parameters
    ----------
    wav_path: str, optional
        Sound file path
    text_path: str, optional
        Transcription file path
    relative_path: str, optional
        Relative path to the corpus root

    Attributes
    ----------
    utterances: :class:`~montreal_forced_aligner.corpus.classes.UtteranceCollection`
        Utterances in the file
    speaker_ordering: list[Speaker]
        Ordering of speakers in the transcription file
    wav_info: :class:`~montreal_forced_aligner.data.SoundFileInformation`
        Information about sound file
    waveform: numpy.array
        Audio samples
    aligned: bool
        Flag for whether a file has alignments

    Raises
    ------
    :class:`~montreal_forced_aligner.exceptions.CorpusError`
        If both wav_path and text_path are None
    """

    textgrid_regex = re.compile(r"\.textgrid$", flags=re.IGNORECASE)
    wav_regex = re.compile(r"\.wav$", flags=re.IGNORECASE)

    def __init__(
        self,
        wav_path: Optional[str] = None,
        text_path: Optional[str] = None,
        relative_path: Optional[str] = None,
        name: Optional[str] = None,
    ):
        self.wav_path = wav_path
        self.text_path = text_path
        wav_check = self.wav_path is not None
        text_check = self.text_path is not None
        self._name = name
        if not self._name:
            if wav_check:
                self._name = os.path.splitext(os.path.basename(self.wav_path))[0]
            elif text_check:
                self._name = os.path.splitext(os.path.basename(self.text_path))[0]
            else:
                raise CorpusError("File objects must have either a wav_path or text_path")
        self.relative_path = relative_path
        self.wav_info: Optional[SoundFileInformation] = None
        self.waveform = None
        self.speaker_ordering: List[Speaker] = []
        self.utterances = UtteranceCollection()
        self.aligned = False
        if text_check:
            if self.text_path.lower().endswith(".textgrid"):
                self.text_type = TextFileType.TEXTGRID
            else:
                self.text_type = TextFileType.LAB
        else:
            self.text_type = TextFileType.NONE
        if wav_check:

            if self.wav_path.lower().endswith(".wav"):
                self.sound_type = SoundFileType.WAV
            else:
                self.sound_type = SoundFileType.SOX
        else:
            self.sound_type = SoundFileType.NONE

    @property
    def multiprocessing_data(self) -> FileData:
        """
        Data object for the file
        """
        return FileData(
            self._name,
            self.wav_path,
            self.text_path,
            self.relative_path,
            self.wav_info,
            [s.name for s in self.speaker_ordering],
            [u.multiprocessing_data for u in self.utterances],
        )

    @classmethod
    def load_from_mp_data(cls, file_data: FileData) -> File:
        """
        Construct a File from a multiprocessing file data class

        Parameters
        ----------
        file_data: :class:`~montreal_forced_aligner.data.FileData`
            Data for the loaded file

        Returns
        -------
        :class:`~montreal_forced_aligner.corpus.classes.File`
            Loaded file
        """
        file = File(
            file_data.wav_path,
            file_data.text_path,
            relative_path=file_data.relative_path,
            name=file_data.name,
        )
        file.wav_info = file_data.wav_info
        for s in file_data.speaker_ordering:
            file.add_speaker(Speaker(s))
        for u in file_data.utterances:
            u = Utterance.load_from_mp_data(u, file)
            file.utterances.add_utterance(u)
        return file

    @classmethod
    def parse_file(
        cls,
        file_name: str,
        wav_path: Optional[str],
        text_path: Optional[str],
        relative_path: str,
        speaker_characters: Union[int, str],
        sanitize_function: Optional[MultispeakerSanitizationFunction] = None,
    ):
        """
        Parse a collection of sound file and transcription file into a File

        Parameters
        ----------
        file_name: str
            File identifier
        wav_path: str
            Full sound file path
        text_path: str
            Full transcription path
        relative_path: str
            Relative path from the corpus directory root
        speaker_characters: int, optional
            Number of characters in the file name to specify the speaker
        sanitize_function: Callable, optional
            Function to sanitize words and strip punctuation

        Returns
        -------
        :class:`~montreal_forced_aligner.corpus.classes.File`
            Parsed file
        """
        file = File(wav_path, text_path, relative_path=relative_path, name=file_name)
        if file.has_sound_file:
            root = os.path.dirname(wav_path)
            file.wav_info = get_wav_info(wav_path)
        else:
            root = os.path.dirname(text_path)
        if not speaker_characters:
            speaker_name = os.path.basename(root)
        elif isinstance(speaker_characters, int):
            speaker_name = file_name[:speaker_characters]
        elif speaker_characters == "prosodylab":
            speaker_name = file_name.split("_")[1]
        else:
            speaker_name = file_name
        root_speaker = None
        if speaker_characters or file.text_type != TextFileType.TEXTGRID:
            root_speaker = Speaker(speaker_name)
        file.load_text(
            root_speaker=root_speaker,
            sanitize_function=sanitize_function,
        )
        return file

    def __eq__(self, other: Union[File, str]) -> bool:
        """Check if a File is equal to another File"""
        if isinstance(other, File):
            return other.name == self.name
        if isinstance(other, str):
            return self.name == other
        raise TypeError("Files can only be compared to other files and strings.")

    def __lt__(self, other: Union[File, str]) -> bool:
        """Check if a File is less than another File"""
        if isinstance(other, File):
            return other.name < self.name
        if isinstance(other, str):
            return self.name < other
        raise TypeError("Files can only be compared to other files and strings.")

    def __gt__(self, other: Union[File, str]) -> bool:
        """Check if a File is greater than another File"""
        if isinstance(other, File):
            return other.name > self.name
        if isinstance(other, str):
            return self.name > other
        raise TypeError("Files can only be compared to other files and strings.")

    def __hash__(self) -> hash:
        """Get the hash of the file"""
        return hash(self.name)

    @property
    def name(self) -> str:
        """Name of the file"""
        name = self._name
        if self.relative_path:
            prefix = self.relative_path.replace("/", "").replace("\\", "")
            if not name.startswith(prefix):
                name = f"{prefix}_{name}"
        return name

    @property
    def is_fully_aligned(self) -> bool:
        """
        Check if all utterances have been aligned
        """
        for u in self.utterances:
            if u.ignored:
                continue
            if u.word_labels is None:
                return False
            if u.phone_labels is None:
                return False
        return True

    def __repr__(self) -> str:
        """Representation of File objects"""
        return f'<File {self.name} Sound path="{self.wav_path}" Text path="{self.text_path}">'

    def save(
        self,
        output_directory: Optional[str] = None,
        text_type: Optional[TextFileType] = None,
        save_transcription: bool = False,
    ) -> None:
        """
        Output File to TextGrid or lab.  If ``text_type`` is not specified, the original file type will be used,
        but if there was no text file for the file, it will guess lab format if there is only one utterance, otherwise
        it will output a TextGrid file.

        Parameters
        ----------
        output_directory: str, optional
            Directory to output file, if None, then it will overwrite the original file
        backup_output_directory: str, optional
            If specified, then it will check whether it would overwrite an existing file and
            instead use this directory
        text_type: TextFileType, optional
            Text type to save as, if not provided, it will use either the original file type or guess the file type
        save_transcription: bool
            Flag for whether the hypothesized transcription text should be saved instead of the default text
        """
        utterance_count = len(self.utterances)
        if text_type is None:
            text_type = self.text_type
            if text_type == TextFileType.NONE:
                if utterance_count == 1:
                    text_type = TextFileType.LAB
                else:
                    text_type = TextFileType.TEXTGRID
        if text_type == TextFileType.LAB:
            if utterance_count == 0 and os.path.exists(self.text_path) and not save_transcription:
                os.remove(self.text_path)
                return
            elif utterance_count == 0:
                return
            output_path = self.construct_output_path(output_directory, enforce_lab=True)
            with open(output_path, "w", encoding="utf8") as f:
                for u in self.utterances:
                    if save_transcription:
                        f.write(u.transcription_text if u.transcription_text else "")
                    else:
                        f.write(u.text)
            return
        elif text_type == TextFileType.TEXTGRID:
            output_path = self.construct_output_path(output_directory)
            max_time = self.duration
            tiers = {}
            for speaker in self.speaker_ordering:
                if speaker is None:
                    tiers["speech"] = textgrid.IntervalTier("speech", [], minT=0, maxT=max_time)
                else:
                    tiers[speaker] = textgrid.IntervalTier(speaker.name, [], minT=0, maxT=max_time)

            tg = textgrid.Textgrid()
            tg.maxTimestamp = max_time
            for utterance in self.utterances:

                if utterance.speaker is None:
                    speaker = "speech"
                else:
                    speaker = utterance.speaker
                if not self.aligned:

                    if save_transcription:
                        tiers[speaker].entryList.append(
                            Interval(
                                start=utterance.begin,
                                end=utterance.end,
                                label=utterance.transcription_text
                                if utterance.transcription_text
                                else "",
                            )
                        )
                    else:
                        tiers[speaker].entryList.append(
                            Interval(
                                start=utterance.begin, end=utterance.end, label=utterance.text
                            )
                        )
            for t in tiers.values():
                tg.addTier(t)
            tg.save(output_path, includeBlankSpaces=True, format="long_textgrid")

    @property
    def meta(self) -> Dict[str, Any]:
        """Metadata for the File"""
        return {
            "wav_path": self.wav_path,
            "text_path": self.text_path,
            "name": self._name,
            "relative_path": self.relative_path,
            "wav_info": self.wav_info.meta,
            "speaker_ordering": [x.name for x in self.speaker_ordering],
        }

    @property
    def has_sound_file(self) -> bool:
        """Flag for whether the File has a sound file"""
        return self.sound_type != SoundFileType.NONE

    @property
    def has_text_file(self) -> bool:
        """Flag for whether the File has a text file"""
        return self.text_type != TextFileType.NONE

    @property
    def aligned_data(self) -> Dict[str, Dict[str, List[CtmInterval]]]:
        """
        Word and phone alignments for the file

        Returns
        -------
        dict[str, dict[str, list[CtmInterval]]]
            Dictionary of word and phone intervals for each speaker in the file
        """
        data = {}
        for s in self.speaker_ordering:
            if s.name not in data:
                data[s.name] = {"words": [], "phones": []}
        for u in self.utterances:
            if u.word_labels is None:
                continue
            data[u.speaker_name]["words"].extend(u.word_labels)
            data[u.speaker_name]["phones"].extend(u.phone_labels)
        return data

    def clean_up(self) -> None:
        """
        Recombine words that were split up as part of initial text processing
        """
        for u in self.utterances:
            if not u.word_labels:
                continue
            cur_ind = 0
            actual_labels = []
            dictionary = u.speaker.dictionary
            for word in u.text.split():
                splits = dictionary.lookup(word)
                b = 1000000
                e = -1
                for w in splits:
                    cur = u.word_labels[cur_ind]
                    if w == cur.label or cur.label == dictionary.oov_word:
                        if cur.begin < b:
                            b = cur.begin
                        if cur.end > e:
                            e = cur.end
                    cur_ind += 1
                lab = CtmInterval(b, e, word, u.name)
                actual_labels.append(lab)
            u.word_labels = actual_labels
            u.phone_labels = [
                x for x in u.phone_labels if x.label != dictionary.optional_silence_phone
            ]

    def construct_output_path(
        self,
        output_directory: Optional[str] = None,
        enforce_lab: bool = False,
    ) -> str:
        """
        Construct the output path for the File

        Parameters
        ----------
        output_directory: str, optional
            Directory to output to, if None, it will overwrite the original file
        enforce_lab: bool
            Flag for whether to enforce generating a lab file over a TextGrid

        Returns
        -------
        str
            Output path
        """
        if enforce_lab:
            extension = ".lab"
        else:
            extension = ".TextGrid"
        if output_directory is None:
            if self.text_path is None:
                return os.path.splitext(self.wav_path)[0] + extension
            return self.text_path
        if self.relative_path:
            relative = os.path.join(output_directory, self.relative_path)
        else:
            relative = output_directory
        tg_path = os.path.join(relative, self._name + extension)
        os.makedirs(os.path.dirname(tg_path), exist_ok=True)
        return tg_path

    def load_text(
        self,
        root_speaker: Optional[Speaker] = None,
        sanitize_function: Optional[MultispeakerSanitizationFunction] = None,
    ) -> None:
        """
        Load the transcription text from the text_file of the object

        Parameters
        ----------
        root_speaker: :class:`~montreal_forced_aligner.corpus.classes.Speaker`, optional
            Speaker derived from the root directory, ignored for TextGrids
        sanitize_function: :class:`~montreal_forced_aligner.dictionary.mixins.SanitizeFunction`, optional
            Function to sanitize words and strip punctuation
        """
        if self.text_type == TextFileType.LAB:
            try:
                text = load_text(self.text_path)
            except UnicodeDecodeError:
                raise TextParseError(self.text_path)
            utterance = Utterance(speaker=root_speaker, file=self, text=text)
            utterance.parse_transcription(sanitize_function)
            self.add_utterance(utterance)
        elif self.text_type == TextFileType.TEXTGRID:
            try:
                tg = textgrid.openTextgrid(self.text_path, includeEmptyIntervals=False)
            except Exception:
                exc_type, exc_value, exc_traceback = sys.exc_info()
                raise TextGridParseError(
                    self.text_path,
                    "\n".join(traceback.format_exception(exc_type, exc_value, exc_traceback)),
                )

            num_tiers = len(tg.tierNameList)
            if num_tiers == 0:
                raise TextGridParseError(self.text_path, "Number of tiers parsed was zero")
            if self.num_channels > 2:
                raise (Exception("More than two channels"))
            for tier_name in tg.tierNameList:
                ti = tg.tierDict[tier_name]
                if tier_name.lower() == "notes":
                    continue
                if not isinstance(ti, textgrid.IntervalTier):
                    continue
                if not root_speaker:
                    speaker_name = tier_name.strip()
                    speaker = Speaker(speaker_name)
                    self.add_speaker(speaker)
                else:
                    speaker = root_speaker
                for begin, end, text in ti.entryList:
                    text = text.lower().strip()
                    if not text:
                        continue
                    begin, end = round(begin, 4), round(end, 4)
                    end = min(end, self.duration)
                    utt = Utterance(speaker=speaker, file=self, begin=begin, end=end, text=text)
                    utt.parse_transcription(sanitize_function)
                    if not utt.text:
                        continue
                    self.add_utterance(utt)
        else:
            utterance = Utterance(speaker=root_speaker, file=self)
            self.add_utterance(utterance)

    def add_speaker(self, speaker: Speaker) -> None:
        """
        Add a speaker to a file

        Parameters
        ----------
        speaker: :class:`~montreal_forced_aligner.corpus.classes.Speaker`
            Speaker to add
        """
        if speaker not in self.speaker_ordering:
            self.speaker_ordering.append(speaker)

    def add_utterance(self, utterance: Utterance) -> None:
        """
        Add an utterance to a file

        Parameters
        ----------
        utterance: :class:`~montreal_forced_aligner.corpus.classes.Utterance`
            Utterance to add
        """
        self.utterances.add_utterance(utterance)
        self.add_speaker(utterance.speaker)

    def delete_utterance(self, utterance: Utterance) -> None:
        """
        Delete an utterance from the file

        Parameters
        ----------
        utterance: :class:`~montreal_forced_aligner.corpus.classes.Utterance`
            Utterance to remove
        """
        identifier = utterance.name
        del self.utterances[identifier]

    def load_info(self) -> None:
        """
        Load sound file info if it hasn't been already
        """
        if self.wav_path is not None:
            self.wav_info = get_wav_info(self.wav_path)

    @property
    def duration(self) -> float:
        """Get the duration of the sound file"""
        if self.wav_path is None:
            return 0
        if not self.wav_info:
            self.load_info()
        return self.wav_info.duration

    @property
    def num_channels(self) -> int:
        """Get the number of channels of the sound file"""
        if self.wav_path is None:
            return 0
        if not self.wav_info:
            self.load_info()
        return self.wav_info.num_channels

    @property
    def num_utterances(self) -> int:
        """Get the number of utterances for the sound file"""
        return len(self.utterances)

    @property
    def num_speakers(self) -> int:
        """Get the number of speakers in the sound file"""
        return len(self.speaker_ordering)

    @property
    def sample_rate(self) -> int:
        """Get the sample rate of the sound file"""
        if self.wav_path is None:
            return 0
        if not self.wav_info:
            self.load_info()
        return self.wav_info.sample_rate

    @property
    def format(self) -> str:
        """Get the sound file format"""
        if not self.wav_info:
            self.load_info()
        return self.wav_info.format

    @property
    def sox_string(self) -> str:
        """String used for converting sound file via SoX within Kaldi"""
        if not self.wav_info:
            self.load_info()
        return self.wav_info.sox_string

    def load_wav_data(self) -> None:
        """
        Load the sound file into memory as a numpy array
        """
        self.waveform, _ = librosa.load(self.wav_path, sr=None, mono=False)

    def normalized_waveform(
        self, begin: float = 0, end: Optional[float] = None
    ) -> Tuple[np.array, np.array]:
        if self.waveform is None:
            self.load_wav_data()
        if end is None or end > self.duration:
            end = self.duration

        begin_sample = int(begin * self.sample_rate)
        end_sample = int(end * self.sample_rate)
        if len(self.waveform.shape) > 1 and self.waveform.shape[0] == 2:
            y = self.waveform[:, begin_sample:end_sample] / np.max(
                np.abs(self.waveform[:, begin_sample:end_sample]), axis=0
            )
            y[np.isnan(y)] = 0
        else:
            y = self.waveform[begin_sample:end_sample] / np.max(
                np.abs(self.waveform[begin_sample:end_sample]), axis=0
            )
        x = np.arange(start=begin_sample, stop=end_sample) / self.sample_rate
        return x, y

    def for_wav_scp(self) -> str:
        """
        Generate the string to use in feature generation

        Returns
        -------
        str
            SoX string if necessary, the sound file path otherwise
        """
        if self.sox_string:
            return self.sox_string
        return self.wav_path
montreal_forced_aligner/data.py
class CtmInterval:
    """
    Data class for intervals derived from CTM files

    Parameters
    ----------
    begin: float
        Start time of interval
    end: float
        End time of interval
    label: str
        Text of interval
    utterance: str
        Utterance ID that the interval belongs to
    """

    __slots__ = ["begin", "end", "label", "utterance"]

    begin: float
    end: float
    label: str
    utterance: str

    def __lt__(self, other: CtmInterval):
        return self.begin < other.begin

    def __post_init__(self):
        """
        Check on data validity

        Raises
        ------
        :class:`~montreal_forced_aligner.exceptions.CtmError`
            If begin or end are not valid
        """
        if self.end < -1 or self.begin == 1000000:
            raise CtmError(self)

    def __add__(self, o: str):
        return self.label + o

    def shift_times(self, offset: float):
        """
        Shift times of the interval based on some offset (i.e., segments in Kaldi)

        Parameters
        ----------
        offset: float
            Offset to add to the interval's begin and end
        """
        self.begin += offset
        self.end += offset

    def to_tg_interval(self) -> Interval:
        """
        Converts the CTMInterval to `PraatIO's Interval class <http://timmahrt.github.io/praatIO/praatio/utilities/constants.html#Interval>`_

        Returns
        -------
        :class:`praatio.utilities.constants.Interval`
            Derived PraatIO Interval
        """
        if self.end < -1 or self.begin == 1000000:
            raise CtmError(self)
        return Interval(round(self.begin, 4), round(self.end, 4), self.label)
montreal_forced_aligner/corpus/classes.py
class Speaker(MfaCorpusClass):
    """
    Class representing information about a speaker

    Parameters
    ----------
    name: str
        Identifier of the speaker

    Attributes
    ----------
    utterances: :class:`~montreal_forced_aligner.corpus.classes.UtteranceCollection`
        Utterances that the speaker is associated with
    cmvn: str, optional
        String pointing to any CMVN that has been calculated for this speaker
    dictionary: :class:`~montreal_forced_aligner.dictionary.PronunciationDictionary`, optional
        Pronunciation dictionary that the speaker is associated with
    """

    def __init__(self, name):
        self._name = name
        self.utterances = UtteranceCollection()
        self.cmvn = None
        self.dictionary: Optional[PronunciationDictionaryMixin] = None
        self.dictionary_name: Optional[str] = None
        self.word_counts = Counter()

    @property
    def name(self) -> str:
        """Speaker name"""
        return self._name

    def __str__(self) -> str:
        """Return Speaker's name"""
        return self.name

    def __eq__(self, other: Union[Speaker, str]) -> bool:
        """Check if a Speaker is equal to another Speaker"""
        if isinstance(other, Speaker):
            return other.name == self.name
        if isinstance(other, str):
            return self.name == other
        raise TypeError("Speakers can only be compared to other speakers and strings.")

    def __lt__(self, other: Union[Speaker, str]) -> bool:
        """Check if a Speaker is less than another Speaker"""
        if isinstance(other, Speaker):
            return other.name < self.name
        if isinstance(other, str):
            return self.name < other
        raise TypeError("Speakers can only be compared to other speakers and strings.")

    def __gt__(self, other: Union[Speaker, str]) -> bool:
        """Check if a Speaker is greater than another Speaker"""
        if isinstance(other, Speaker):
            return other.name > self.name
        if isinstance(other, str):
            return self.name > other
        raise TypeError("Speakers can only be compared to other speakers and strings.")

    def __hash__(self) -> hash:
        """Get the hash of the speaker"""
        return hash(self.name)

    @property
    def num_utterances(self) -> int:
        """Get the number of utterances for the speaker"""
        return len(self.utterances)

    def add_utterance(self, utterance: Utterance) -> None:
        """
        Associate an utterance with a speaker

        Parameters
        ----------
        utterance: :class:`~montreal_forced_aligner.corpus.classes.Utterance`
            Utterance to be added
        """
        self.utterances.add_utterance(utterance)

    def delete_utterance(self, utterance: Utterance) -> None:
        """
        Delete an utterance associated with a speaker

        Parameters
        ----------
        utterance: :class:`~montreal_forced_aligner.corpus.classes.Utterance`
            Utterance to be deleted
        """
        identifier = utterance.name
        del self.utterances[identifier]

    def set_dictionary(self, dictionary: PronunciationDictionaryMixin) -> None:
        """
        Set the dictionary for the speaker

        Parameters
        ----------
        dictionary: :class:`~montreal_forced_aligner.dictionary.PronunciationDictionary`
            Pronunciation dictionary to associate with the speaker
        """
        self.dictionary = dictionary
        self.dictionary_name = dictionary.name

    @property
    def meta(self) -> Dict[str, str]:
        """Metadata for the speaker"""
        data = {
            "name": self.name,
            "cmvn": self.cmvn,
        }
        if self.dictionary is not None:
            data["dictionary"] = self.dictionary.name
        return data
montreal_forced_aligner/helper.py
def jsonl_encoder(obj):
    return json.dumps(obj, cls=EnhancedJSONEncoder)
montreal_forced_aligner/abc.py
class MfaWorker(metaclass=abc.ABCMeta):
    """
    Abstract class for MFA workers

    Parameters
    ----------
    use_mp: bool
        Flag to run in multiprocessing mode, defaults to True
    debug: bool
        Flag to run in debug mode, defaults to False
    verbose: bool
        Flag to run in verbose mode, defaults to False

    Attributes
    ----------
    dirty: bool
        Flag for whether an error was encountered in processing
    """

    def __init__(
        self,
        use_mp: bool = True,
        debug: bool = False,
        verbose: bool = False,
        **kwargs,
    ):
        super().__init__(**kwargs)
        self.debug = debug
        self.verbose = verbose
        self.use_mp = use_mp
        self.dirty = False

    def log_debug(self, message: str) -> None:
        """
        Print a debug message

        Parameters
        ----------
        message: str
            Debug message to log
        """
        print(message)

    def log_error(self, message: str) -> None:
        """
        Print an error message

        Parameters
        ----------
        message: str
            Error message to log
        """
        print(message)

    def log_info(self, message: str) -> None:
        """
        Print an info message

        Parameters
        ----------
        message: str
            Info message to log
        """
        print(message)

    def log_warning(self, message: str) -> None:
        """
        Print a warning message

        Parameters
        ----------
        message: str
            Warning message to log
        """
        print(message)

    @classmethod
    def extract_relevant_parameters(cls, config: MetaDict) -> Tuple[MetaDict, List[str]]:
        """
        Filter a configuration dictionary to just the relevant parameters for the current worker

        Parameters
        ----------
        config: dict[str, Any]
            Configuration dictionary

        Returns
        -------
        dict[str, Any]
            Filtered configuration dictionary
        list[str]
            Skipped keys
        """
        skipped = []
        new_config = {}
        for k, v in config.items():
            if k in cls.get_configuration_parameters():
                new_config[k] = v
            else:
                skipped.append(k)
        return new_config, skipped

    @classmethod
    def get_configuration_parameters(cls) -> Dict[str, Type]:
        """
        Get the types of parameters available to be configured

        Returns
        -------
        dict[str, Type]
            Dictionary of parameter names and their types
        """
        mapping = {Dict: dict, Tuple: tuple, List: list, Set: set}
        configuration_params = {}
        for t, ty in get_type_hints(cls.__init__).items():
            configuration_params[t] = ty
            try:
                if ty.__origin__ == Union:
                    configuration_params[t] = ty.__args__[0]
            except AttributeError:
                pass

        for c in cls.mro():
            try:
                for t, ty in get_type_hints(c.__init__).items():
                    configuration_params[t] = ty
                    try:
                        if ty.__origin__ == Union:
                            configuration_params[t] = ty.__args__[0]
                    except AttributeError:
                        pass
            except AttributeError:
                pass
        for t, ty in configuration_params.items():
            for v in mapping.values():
                try:
                    if ty.__origin__ == v:
                        configuration_params[t] = v
                        break
                except AttributeError:
                    break
        return configuration_params

    @property
    def configuration(self) -> MetaDict:
        """Configuration parameters"""
        return {
            "debug": self.debug,
            "verbose": self.verbose,
            "use_mp": self.use_mp,
            "dirty": self.dirty,
        }

    @property
    @abc.abstractmethod
    def working_directory(self) -> str:
        """Current working directory"""
        ...

    @property
    def working_log_directory(self) -> str:
        """Current working log directory"""
        return os.path.join(self.working_directory, "log")

    @property
    @abc.abstractmethod
    def data_directory(self) -> str:
        """Data directory"""
        ...
montreal_forced_aligner/helper.py
def output_mapping(mapping: CorpusMappingType, path: str, skip_safe: bool = False) -> None:
    """
    Helper function to save mapping information (i.e., utt2spk) in Kaldi scp format

    CorpusMappingType is either a dictionary of key to value for
    one-to-one mapping case and a dictionary of key to list of values for one-to-many case.

    See Also
    --------
    :func:`~montreal_forced_aligner.helper.save_scp`
        For another function that saves SCPs from lists

    Parameters
    ----------
    mapping: CorpusMappingType
        Mapping to output
    path: str
        Path to save mapping
    skip_safe: bool, optional
        Flag for whether to skip over making a string safe
    """
    if not mapping:
        return
    with open(path, "w", encoding="utf8") as f:
        for k in sorted(mapping.keys()):
            v = mapping[k]
            if isinstance(v, (list, set, tuple)):
                v = " ".join(map(str, v))
            elif not skip_safe:
                v = make_scp_safe(v)
            f.write(f"{make_scp_safe(k)} {v}\n")
montreal_forced_aligner/abc.py
class TemporaryDirectoryMixin(metaclass=abc.ABCMeta):
    """
    Abstract mixin class for MFA temporary directories

    Parameters
    ----------
    temporary_directory: str, optional
        Path to store temporary files
    """

    def __init__(
        self,
        temporary_directory: str = None,
        **kwargs,
    ):
        super().__init__(**kwargs)
        if not temporary_directory:
            from .config import get_temporary_directory

            temporary_directory = get_temporary_directory()
        self.temporary_directory = temporary_directory
        self._corpus_output_directory = None
        self._dictionary_output_directory = None
        self._language_model_output_directory = None

    @property
    @abc.abstractmethod
    def identifier(self) -> str:
        """Identifier to use in creating the temporary directory"""
        ...

    @property
    @abc.abstractmethod
    def data_source_identifier(self) -> str:
        """Identifier for the data source (generally the corpus being used)"""
        ...

    @property
    @abc.abstractmethod
    def output_directory(self) -> str:
        """Root temporary directory"""
        ...

    @property
    def corpus_output_directory(self) -> str:
        """Temporary directory containing all corpus information"""
        if self._corpus_output_directory:
            return self._corpus_output_directory
        return os.path.join(self.output_directory, f"{self.data_source_identifier}")

    @corpus_output_directory.setter
    def corpus_output_directory(self, directory: str) -> None:
        self._corpus_output_director = directory

    @property
    def dictionary_output_directory(self) -> str:
        """Temporary directory containing all dictionary information"""
        if self._dictionary_output_directory:
            return self._dictionary_output_directory
        return os.path.join(self.output_directory, "dictionary")

    @dictionary_output_directory.setter
    def dictionary_output_directory(self, directory: str) -> None:
        self._dictionary_output_directory = directory

    @property
    def language_model_output_directory(self) -> str:
        """Temporary directory containing all dictionary information"""
        if self._language_model_output_directory:
            return self._language_model_output_directory
        return os.path.join(self.output_directory, "language_model")

    @language_model_output_directory.setter
    def language_model_output_directory(self, directory: str) -> None:
        self._language_model_output_directory = directory


Question: import json
import os
import random
import time
import jsonlines
import yaml
from abc import ABCMeta, abstractmethod
from collections import Counter
from typing import Dict, List, Optional, Union
from montreal_forced_aligner.abc import MfaWorker, TemporaryDirectoryMixin
from montreal_forced_aligner.corpus.classes import (
    File,
    FileCollection,
    Speaker,
    SpeakerCollection,
    Utterance,
    UtteranceCollection,
)
from montreal_forced_aligner.corpus.multiprocessing import Job
from montreal_forced_aligner.data import CtmInterval, SoundFileInformation
from montreal_forced_aligner.exceptions import CorpusError
from montreal_forced_aligner.helper import jsonl_encoder, output_mapping
from montreal_forced_aligner.utils import Stopped
        pass

    @property
    def corpus_meta(self):
        return {}

    @property
    def features_directory(self) -> str:
        """Feature directory of the corpus"""
        return os.path.join(self.corpus_output_directory, "features")

    @property
    def features_log_directory(self) -> str:
        """Feature log directory"""
        return os.path.join(self.split_directory, "log")

    @property
    def split_directory(self) -> str:
        """Directory used to store information split by job"""
        return os.path.join(self.corpus_output_directory, f"split{self.num_jobs}")

    def write_corpus_information(self) -> None:
        """
        Output information to the temporary directory for later loading
        """
        os.makedirs(self.split_directory, exist_ok=True)
        self._write_speakers()
        self._write_files()
        self._write_utterances()
        self._write_spk2utt()
        self._write_corpus_info()

    def _write_spk2utt(self):
        """Write spk2utt scp file for Kaldi"""
        data = {
            speaker.name: sorted(u.name for u in speaker.utterances) for speaker in self.speakers
        }
        output_mapping(data, os.path.join(self.corpus_output_directory, "spk2utt.scp"))

    def write_utt2spk(self):
        """Write utt2spk scp file for Kaldi"""
        data = {u.name: u.speaker.name for u in self.utterances}
        output_mapping(data, os.path.join(self.corpus_output_directory, "utt2spk.scp"))

    def _write_corpus_info(self):
        """Write speaker information for speeding up future runs"""
        with open(
            os.path.join(self.corpus_output_directory, "corpus.json"), "w", encoding="utf8"
        ) as f:
            json.dump(self.corpus_meta, f)

    def _write_speakers(self):
        """Write speaker information for speeding up future runs"""
        with open(
            os.path.join(self.corpus_output_directory, "speakers.jsonl"), "w", encoding="utf8"
        ) as f:
            writer = jsonlines.Writer(f, dumps=jsonl_encoder)
            for speaker in self.speakers:
                writer.write(speaker.meta)

    def _write_files(self):
        """Write file information for speeding up future runs"""
        with open(
            os.path.join(self.corpus_output_directory, "files.jsonl"), "w", encoding="utf8"
        ) as f:
            writer = jsonlines.Writer(f, dumps=jsonl_encoder)
            for file in self.files:
                writer.write(file.meta)

    def _write_utterances(self):
        """Write utterance information for speeding up future runs"""
        with open(
            os.path.join(self.corpus_output_directory, "utterances.jsonl"), "w", encoding="utf8"
        ) as f:
            writer = jsonlines.Writer(f, dumps=jsonl_encoder)
            for utterance in self.utterances:
                writer.write(utterance.meta)

    def create_corpus_split(self) -> None:
        """Create split directory and output information from Jobs"""
        split_dir = self.split_directory
        os.makedirs(os.path.join(split_dir, "log"), exist_ok=True)
        for job in self.jobs:
            job.output_to_directory(split_dir)

    @property
    def file_speaker_mapping(self) -> Dict[str, List[str]]:
        """Speaker ordering for each file"""
        return {file.name: file.speaker_ordering for file in self.files}

    def get_word_frequency(self) -> Dict[str, float]:
        """
        Calculate the relative word frequency across all the texts in the corpus

        Returns
        -------
        dict[str, float]
            Dictionary of words and their relative frequencies
        """
        word_counts = Counter()
        for u in self.utterances:
            text = u.text
            speaker = u.speaker
            d = speaker.dictionary
            new_text = []
            text = text.split()
            for t in text:

                lookup = d.split_clitics(t)
                if lookup is None:
                    continue
                new_text.extend(x for x in lookup if x != "")
            word_counts.update(new_text)
        return {k: v / sum(word_counts.values()) for k, v in word_counts.items()}

    @property
    def corpus_word_set(self) -> List[str]:
        """Set of words used in the corpus"""
        return sorted(self.word_counts)


Answer:     def add_utterance(self, utterance: Utterance) -> None: