Repository: MontrealCorpusTools/Montreal-Forced-Aligner

Issue:
[BUG] mfa diarize will fail with error missing attribute
**Debugging checklist**

[x] Have you updated to latest MFA version?  Yes, v 2.2.9
[x] Have you tried rerunning the command with the `--clean` flag? Yes running from fresh docker. 

**Describe the issue**
When trying to use `mfa diarize` on corpus, it fails with AttributeError. 

Command used:
```
 mfa diarize train_data_diarize /home/mfauser/Documents/MFA/pretrained_models/ivector/multilingual_mfa.zip train_data_diarize_out
```
Console output:
```
server started
 WARNING  Skipped the following configuration keys: ivector_dimension
 INFO     Setting up corpus information...
 INFO     Loading corpus from source files...
 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 21,109/100  [ 0:00:00 < 0:00:00 , 2,421 it/s ]
 INFO     Found 1 speaker across 21128 files, average number of utterances per speaker: 21128.0
 INFO     Initializing multiprocessing jobs...
 WARNING  Number of jobs was specified as 3, but due to only having 1 speakers, MFA will only use 1 jobs. Use the --single_speaker flag if you would like to split utterances across jobs regardless of their speaker.
 INFO     Creating corpus split for feature generation...
  88% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━ 37,382/42,256  [ 0:00:02 < 0:00:01 , 50,431 it/s ]
 INFO     Generating MFCCs...
  67% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14,243/21,128  [ 0:06:13 < 0:03:04 , 37 it/s ]
  88% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━━━━━━ 18,667/21,128  [ 0:08:13 < 0:01:05 , 38 it/s ]
 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 21,127/21,128  [ 0:09:18 < 0:00:01 , 39 it/s ]
 INFO     Calculating CMVN...
 INFO     Generating final features...
 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 21,116/21,128  [ 0:00:36 < 0:00:01 , 600 it/s ]
 INFO     Creating corpus split with features...
  78% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16,568/21,128  [ 0:00:01 < 0:00:01 , 55,230 it/s ]
 INFO     Extracting ivectors...
  71% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14,968/21,128  [ 0:03:08 < 0:01:09 , 90 it/s ]
  74% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 15,553/21,128  [ 0:03:14 < 0:01:01 , 92 it/s ]
 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 21,126/21,128  [ 0:04:23 < 0:00:01 , 86 it/s ]
 INFO     Collecting ivectors...
  96% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━ 20,326/21,128  [ 0:00:05 < 0:00:01 , 13,541 it/s ]
 INFO     Computing speaker ivectors...
 INFO     Collecting speaker ivectors...
   0% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0/1  [ 0:00:00 < -:--:-- , ? it/s ]
 INFO     Generating initial speaker labels...
 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 21,127/21,128  [ 0:09:01 < 0:00:01 , 40 it/s ]
 INFO     Breaking up large speakers...
   0% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0/1  [ 0:00:14 < -:--:-- , ? it/s ]
   0% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0/1  [ 0:00:16 < -:--:-- , ? it/s ]
 ERROR    There was an error in the run, please see the log.
 INFO     Stopping the global MFA database server...
Exception ignored in atexit callback: <bound method ExitHooks.history_save_handler of <montreal_forced_aligner.command_line.mfa.ExitHooks object at 0x7f650776ebf0>>
Traceback (most recent call last):
  File "/env/lib/python3.10/site-packages/montreal_forced_aligner/command_line/mfa.py", line 97, in history_save_handler
    raise self.exception
  File "/env/bin/mfa", line 10, in <module>
    sys.exit(mfa_cli())
  File "/env/lib/python3.10/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/env/lib/python3.10/site-packages/rich_click/rich_group.py", line 21, in main
    rv = super().main(*args, standalone_mode=False, **kwargs)
  File "/env/lib/python3.10/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/env/lib/python3.10/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/env/lib/python3.10/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params) 
  File "/env/lib/python3.10/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/env/lib/python3.10/site-packages/click/decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/env/lib/python3.10/site-packages/montreal_forced_aligner/command_line/diarize_speakers.py", line 115, in diarize_speakers_cli
    classifier.cluster_utterances()
  File "/env/lib/python3.10/site-packages/montreal_forced_aligner/diarization/speaker_diarizer.py", line 1079, in cluster_utterances
    self.cluster_utterances_mfa()
  File "/env/lib/python3.10/site-packages/montreal_forced_aligner/diarization/speaker_diarizer.py", line 1002, in cluster_utterances_mfa
    self.initialize_mfa_clustering()
  File "/env/lib/python3.10/site-packages/montreal_forced_aligner/diarization/speaker_diarizer.py", line 749, in initialize_mfa_clustering
    self.breakup_large_clusters()
  File "/env/lib/python3.10/site-packages/montreal_forced_aligner/diarization/speaker_diarizer.py", line 938, in breakup_large_clusters
    label = self._unknown_speaker_break_up_count   
AttributeError: 'SpeakerDiarizer' object has no attribute '_unknown_speaker_break_up_count'

AttributeError: 'SpeakerDiarizer' object has no attribute '_unknown_speaker_break_up_count'
```

**For Reproducing your issue**
Please fill out the following:

1. Corpus structure
   *  Czech 
   *  Unknown number of speakers, try to diarize corpus https://mfa-models.readthedocs.io/en/latest/corpus/Czech/VoxPopuli%20Czech.html#VoxPopuli%20Czech
   * Using lab files. 
2. Dictionary
   * N/A
   * N/A
3. Acoustic model
   *using pretrained ivectors  https://mfa-models.readthedocs.io/en/latest/ivector/Multilingual/Multilingual%20MFA%20ivector%20extractor%20v2_1_0.html#Multilingual%20MFA%20ivector%20extractor%20v2_1_0

**Log file**
Please attach the log file for the run that encountered an error (by default these will be stored in `~/Documents/MFA`).

**Desktop (please complete the following information):**
 - Linux, Docker



Discussion:

Patch:
diff --git a/docs/source/changelog/changelog_2.2.rst b/docs/source/changelog/changelog_2.2.rst
index d7e762e..6a52400 100644
--- a/docs/source/changelog/changelog_2.2.rst
+++ b/docs/source/changelog/changelog_2.2.rst
@@ -5,6 +5,12 @@
 2.2 Changelog
 *************
 
+2.2.10
+======
+
+- Fix crash in speaker diarization
+- Update alignment evaluation to export confusion counts
+
 2.2.9
 =====
 - Fixed a bug in pronunciation probability training that was causing all probabilities of following silence to be 0
diff --git a/docs/source/installation.rst b/docs/source/installation.rst
index 2d6d831..593001e 100644
--- a/docs/source/installation.rst
+++ b/docs/source/installation.rst
@@ -43,7 +43,7 @@ To use the Docker image of MFA:
 
 1. Run :code:`docker image pull mmcauliffe/montreal-forced-aligner:latest`
 2. Enter the interactive docker shell via :code:`docker run -it -v /path/to/data/directory:/data mmcauliffe/montreal-forced-aligner:latest`
-3. Once you are in the shell, you can run MFA commands as normal (i.e., :code:`mfa align ...`).  You may need to download any pretrained models you want to use each session (i.e., :code:`mfa download acoustic english_mfa`)
+3. Once you are in the shell, you can run MFA commands as normal (i.e., :code:`mfa align ...`).  You may need to download any pretrained models you want to use each session (i.e., :code:`mfa model download acoustic english_mfa`)
 
 .. important::
 
diff --git a/montreal_forced_aligner/alignment/base.py b/montreal_forced_aligner/alignment/base.py
index 65bfa77..a0901e3 100644
--- a/montreal_forced_aligner/alignment/base.py
+++ b/montreal_forced_aligner/alignment/base.py
@@ -1209,7 +1209,11 @@ class CorpusAligner(AcousticCorpusPronunciationMixin, AlignMixin, FileExporterMi
                 f"Check {os.path.join(self.export_output_directory, 'output_errors.txt')} "
                 f"for more details"
             )
-        output_textgrid_writing_errors(self.export_output_directory, error_dict)
+            output_textgrid_writing_errors(self.export_output_directory, error_dict)
+            if GLOBAL_CONFIG.current_profile.debug:
+                for k, v in error_dict.items():
+                    print(k)
+                    raise v
         logger.info(f"Finished exporting TextGrids to {self.export_output_directory}!")
         logger.debug(f"Exported TextGrids in a total of {time.time() - begin:.3f} seconds")
 
@@ -1279,6 +1283,10 @@ class CorpusAligner(AcousticCorpusPronunciationMixin, AlignMixin, FileExporterMi
                 output_directory,
                 f"{comparison_source.name}_{reference_source.name}_evaluation.csv",
             )
+            confusion_path = os.path.join(
+                output_directory,
+                f"{comparison_source.name}_{reference_source.name}_confusions.csv",
+            )
         else:
             self._current_workflow = "evaluation"
             os.makedirs(self.working_log_directory, exist_ok=True)
@@ -1286,6 +1294,10 @@ class CorpusAligner(AcousticCorpusPronunciationMixin, AlignMixin, FileExporterMi
                 self.working_log_directory,
                 f"{comparison_source.name}_{reference_source.name}_evaluation.csv",
             )
+            confusion_path = os.path.join(
+                self.working_log_directory,
+                f"{comparison_source.name}_{reference_source.name}_confusions.csv",
+            )
         csv_header = [
             "file",
             "begin",
@@ -1306,6 +1318,7 @@ class CorpusAligner(AcousticCorpusPronunciationMixin, AlignMixin, FileExporterMi
         score_sum = 0
         phone_edit_sum = 0
         phone_length_sum = 0
+        phone_confusions = collections.Counter()
         with self.session() as session:
             # Set up
             logger.info("Evaluating alignments...")
@@ -1370,10 +1383,11 @@ class CorpusAligner(AcousticCorpusPronunciationMixin, AlignMixin, FileExporterMi
                 to_comp.append((reference_phones, comparison_phones))
             with mp.Pool(GLOBAL_CONFIG.num_jobs) as pool:
                 gen = pool.starmap(score_func, to_comp)
-                for i, (score, phone_error_rate) in enumerate(gen):
+                for i, (score, phone_error_rate, errors) in enumerate(gen):
                     if score is None:
                         continue
                     u = indices[i]
+                    phone_confusions.update(errors)
                     reference_phone_count = reference_phone_counts[u.id]
                     update_mappings.append(
                         {
@@ -1443,7 +1457,10 @@ class CorpusAligner(AcousticCorpusPronunciationMixin, AlignMixin, FileExporterMi
                         score_count += 1
                         score_sum += alignment_score
                     writer.writerow(data)
-
+        with mfa_open(confusion_path, "w") as f:
+            f.write("reference,hypothesis,count\n")
+            for k, v in sorted(phone_confusions.items(), key=lambda x: -x[1]):
+                f.write(f"{k[0]},{k[1]},{v}\n")
         logger.info(f"Average overlap score: {score_sum/score_count}")
         logger.info(f"Average phone error rate: {phone_edit_sum/phone_length_sum}")
         logger.debug(f"Alignment evaluation took {time.time()-begin} seconds")
diff --git a/montreal_forced_aligner/command_line/utils.py b/montreal_forced_aligner/command_line/utils.py
index f760d1c..0b6a2c8 100644
--- a/montreal_forced_aligner/command_line/utils.py
+++ b/montreal_forced_aligner/command_line/utils.py
@@ -433,14 +433,14 @@ def start_server() -> None:
     logger.info(f"{GLOBAL_CONFIG.current_profile_name} MFA database server started!")
 
 
-def stop_server(mode: str = "fast") -> None:
+def stop_server(mode: str = "smart") -> None:
     """
     Stop the MFA server for the current profile.
 
     Parameters
     ----------
     mode: str, optional
-        Mode to to be passed to `pg_ctl`, defaults to "fast"
+        Mode to to be passed to `pg_ctl`, defaults to "smart"
     """
     logger = logging.getLogger("mfa")
     GLOBAL_CONFIG.load()
diff --git a/montreal_forced_aligner/data.py b/montreal_forced_aligner/data.py
index d27551b..99cf96b 100644
--- a/montreal_forced_aligner/data.py
+++ b/montreal_forced_aligner/data.py
@@ -1751,8 +1751,10 @@ class CtmInterval:
         if self.end < -1 or self.begin == 1000000:
             raise CtmError(self)
         end = round(self.end, 6)
+        begin = round(self.begin, 6)
         if file_duration is not None and end > file_duration:
             end = round(file_duration, 6)
+        assert begin < end
         return Interval(round(self.begin, 6), end, self.label)
 
 
diff --git a/montreal_forced_aligner/diarization/speaker_diarizer.py b/montreal_forced_aligner/diarization/speaker_diarizer.py
index 3f0e37a..f3abf80 100644
--- a/montreal_forced_aligner/diarization/speaker_diarizer.py
+++ b/montreal_forced_aligner/diarization/speaker_diarizer.py
@@ -182,6 +182,7 @@ class SpeakerDiarizer(IvectorCorpusMixin, TopLevelMfaWorker, FileExporterMixin):
         self.ground_truth_utt2spk = {}
         self.ground_truth_speakers = {}
         self.single_clusters = set()
+        self._unknown_speaker_break_up_count = 0
 
     @classmethod
     def parse_parameters(
@@ -674,6 +675,7 @@ class SpeakerDiarizer(IvectorCorpusMixin, TopLevelMfaWorker, FileExporterMixin):
             session.commit()
 
     def initialize_mfa_clustering(self):
+        self._unknown_speaker_break_up_count = 0
 
         with self.session() as session:
             next_speaker_id = self.get_next_primary_key(Speaker)
diff --git a/montreal_forced_aligner/helper.py b/montreal_forced_aligner/helper.py
index c936d73..303bdc5 100644
--- a/montreal_forced_aligner/helper.py
+++ b/montreal_forced_aligner/helper.py
@@ -5,6 +5,7 @@ Helper functions
 """
 from __future__ import annotations
 
+import collections
 import functools
 import itertools
 import json
@@ -619,7 +620,7 @@ def align_phones(
     ignored_phones: typing.Set[str] = None,
     custom_mapping: Optional[Dict[str, str]] = None,
     debug: bool = False,
-) -> Tuple[float, float]:
+) -> Tuple[float, float, Dict[Tuple[str, str], int]]:
     """
     Align phones based on how much they overlap and their phone label, with the ability to specify a custom mapping for
     different phone labels to be scored as if they're the same phone
@@ -634,6 +635,8 @@ def align_phones(
         Silence phone (these are ignored in the final calculation)
     custom_mapping: dict[str, str], optional
         Mapping of phones to treat as matches even if they have different symbols
+    debug: bool, optional
+        Flag for logging extra information about alignments
 
     Returns
     -------
@@ -641,6 +644,8 @@ def align_phones(
         Score based on the average amount of overlap in phone intervals
     float
         Phone error rate
+    dict[tuple[str, str], int]
+        Dictionary of error pairs with their counts
     """
 
     if ignored_phones is None:
@@ -653,23 +658,26 @@ def align_phones(
         )
 
     alignments = pairwise2.align.globalcs(
-        ref, test, score_func, -5, -5, gap_char=["-"], one_alignment_only=True
+        ref, test, score_func, -2, -2, gap_char=["-"], one_alignment_only=True
     )
     overlap_count = 0
     overlap_sum = 0
     num_insertions = 0
     num_deletions = 0
     num_substitutions = 0
+    errors = collections.Counter()
     for a in alignments:
         for i, sa in enumerate(a.seqA):
             sb = a.seqB[i]
             if sa == "-":
                 if sb.label not in ignored_phones:
+                    errors[(sa, sb.label)] += 1
                     num_insertions += 1
                 else:
                     continue
             elif sb == "-":
                 if sa.label not in ignored_phones:
+                    errors[(sa.label, sb)] += 1
                     num_deletions += 1
                 else:
                     continue
@@ -680,6 +688,7 @@ def align_phones(
                 overlap_count += 1
                 if compare_labels(sa.label, sb.label, silence_phone, mapping=custom_mapping) > 0:
                     num_substitutions += 1
+                    errors[(sa.label, sb.label)] += 1
     if debug:
         import logging
 
@@ -690,7 +699,7 @@ def align_phones(
     else:
         score = None
     phone_error_rate = (num_insertions + num_deletions + (2 * num_substitutions)) / len(ref)
-    return score, phone_error_rate
+    return score, phone_error_rate, errors
 
 
 def format_probability(probability_value: float) -> float:


Test patch:
diff --git a/tests/test_helper.py b/tests/test_helper.py
index 1077bd0..8292fb3 100644
--- a/tests/test_helper.py
+++ b/tests/test_helper.py
@@ -89,7 +89,7 @@ def test_align_phones(basic_corpus_dir, basic_dict_path, temp_dir, eval_mapping_
         "ɹ",
     ]
     comparison_sequence = [CtmInterval(i, i + 1, x) for i, x in enumerate(comparison_sequence)]
-    score, phone_errors = align_phones(
+    score, phone_errors, error_counts = align_phones(
         reference_sequence,
         comparison_sequence,
         silence_phone="sil",