Skip to content

Commit ba2cd63

Browse files
authored
Eval/tts multilingual parakeet metrics (#15826)
* [TTS] Add Japanese Katakana CER metric to MagpieTTS eval Adds a reading-based CER for Japanese, computed on the Katakana reading (via pyopenjtalk g2p) of both reference and ASR hypothesis. Robust to kanji/kana spelling differences that inflate raw character CER. - text_to_katakana(): lazy-imports pyopenjtalk, returns '' if unavailable (graceful no-op for non-ja or environments without the dep). - katakana_cer / gt_katakana / pred_katakana computed only when language=='ja', saved per-utterance in filewise metrics. - katakana_cer_filewise_avg + katakana_cer_cumulative aggregated globally (only emitted for ja datasets), added to the results CSV header/rows. Signed-off-by: quanpham <youngkwan199@gmail.com> * feat(tts): add multilingual eval text processors Signed-off-by: quanpham <youngkwan199@gmail.com> * refactor(tts): reuse shared eval text processors Signed-off-by: quanpham <youngkwan199@gmail.com> * Address Japanese text processor review comments Signed-off-by: quanpham <youngkwan199@gmail.com> * Fix MagpieTTS evaluation formatting Signed-off-by: quanpham <youngkwan199@gmail.com> --------- Signed-off-by: quanpham <youngkwan199@gmail.com>
1 parent 01f05b6 commit ba2cd63

3 files changed

Lines changed: 90 additions & 2 deletions

File tree

examples/tts/magpietts_inference.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ def append_metrics_to_csv(csv_path: str, checkpoint_name: str, dataset: str, met
134134
metrics.get('eou_silence_rate', ''),
135135
metrics.get('eou_noise_rate', ''),
136136
metrics.get('eou_error_rate', ''),
137+
metrics.get('katakana_cer_filewise_avg', ''),
138+
metrics.get('katakana_cer_cumulative', ''),
137139
]
138140
with open(csv_path, "a") as f:
139141
f.write(",".join(str(v) for v in values) + "\n")
@@ -230,7 +232,8 @@ def run_inference_and_evaluation(
230232
"ssim_pred_gt_avg_alternate,ssim_pred_context_avg_alternate,"
231233
"ssim_gt_context_avg_alternate,cer_gt_audio_cumulative,wer_gt_audio_cumulative,"
232234
"utmosv2_avg,total_gen_audio_seconds,frechet_codec_distance,"
233-
"eou_cutoff_rate,eou_silence_rate,eou_noise_rate,eou_error_rate"
235+
"eou_cutoff_rate,eou_silence_rate,eou_noise_rate,eou_error_rate,"
236+
"katakana_cer_filewise_avg,katakana_cer_cumulative"
234237
)
235238

236239
for dataset in datasets:

nemo/collections/tts/modules/magpietts_inference/evaluate_generated_audio.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from nemo.collections.tts.metrics.eou_classifier import EoUClassification, EoUClassifier, EoUType
3737
from nemo.collections.tts.metrics.frechet_codec_distance import FrechetCodecDistance
3838
from nemo.collections.tts.parts.utils.tts_dataset_utils import (
39+
JapaneseTextProcessor,
3940
NemoTranscriber,
4041
NemoTranscriberWithPrompt,
4142
WhisperTranscriber,
@@ -57,11 +58,18 @@
5758
)
5859

5960

61+
KATAKANA_METRICS_TO_SAVE = [
62+
'katakana_cer',
63+
'gt_katakana',
64+
'pred_katakana',
65+
]
66+
6067
FILEWISE_METRICS_TO_SAVE = [
6168
'cer',
6269
'wer',
6370
'pred_context_ssim',
6471
'pred_text',
72+
'gt_audio_text',
6573
'gt_text',
6674
'gt_audio_filepath',
6775
'pred_audio_filepath',
@@ -408,6 +416,17 @@ def evaluate_dir(
408416
detailed_cer = word_error_rate_detail(hypotheses=[pred_text], references=[gt_text], use_cer=True)
409417
detailed_wer = word_error_rate_detail(hypotheses=[pred_text], references=[gt_text], use_cer=False)
410418

419+
# Japanese: additional reading-based CER on Katakana (pyopenjtalk g2p), robust to
420+
# kanji/kana spelling differences between reference and ASR hypothesis.
421+
gt_katakana = pred_katakana = None
422+
katakana_cer = None
423+
if isinstance(text_processor, JapaneseTextProcessor):
424+
gt_katakana = text_processor.text_to_katakana(gt_text)
425+
pred_katakana = text_processor.text_to_katakana(pred_text)
426+
katakana_cer = word_error_rate_detail(hypotheses=[pred_katakana], references=[gt_katakana], use_cer=True)[
427+
0
428+
]
429+
411430
logging.info(f"{ridx} GT Text: {gt_text}")
412431
logging.info(f"{ridx} Pr Text: {pred_text}")
413432
# Format cer and wer to 2 decimal places
@@ -502,6 +521,9 @@ def evaluate_dir(
502521
'detailed_wer': detailed_wer,
503522
'cer': detailed_cer[0],
504523
'wer': detailed_wer[0],
524+
'katakana_cer': katakana_cer,
525+
'gt_katakana': gt_katakana,
526+
'pred_katakana': pred_katakana,
505527
'pred_gt_ssim': pred_gt_ssim,
506528
'pred_context_ssim': pred_context_ssim,
507529
'gt_context_ssim': gt_context_ssim,
@@ -593,7 +615,11 @@ def evaluate(
593615
elapsed = time.time() - start_time
594616
logging.info(f"evaluate() completed in {elapsed:.1f}s ({elapsed / 60:.1f} min)")
595617

596-
filtered_filewise = [{k: m[k] for k in FILEWISE_METRICS_TO_SAVE if k in m} for m in filewise_metrics]
618+
filewise_metrics_to_save = list(FILEWISE_METRICS_TO_SAVE)
619+
if (language or "").replace("_", "-").lower().split("-")[0] == "ja":
620+
filewise_metrics_to_save[2:2] = KATAKANA_METRICS_TO_SAVE
621+
622+
filtered_filewise = [{k: m[k] for k in filewise_metrics_to_save if k in m} for m in filewise_metrics]
597623
return avg_metrics, filtered_filewise
598624

599625

@@ -651,6 +677,15 @@ def compute_global_metrics(
651677
avg_metrics['wer_cumulative'] = word_error_rate_detail(hypotheses=pred_texts, references=gt_texts, use_cer=False)[
652678
0
653679
]
680+
# Japanese reading-based CER (Katakana via pyopenjtalk); only present for ja datasets.
681+
kata = [m for m in filewise_metrics if m.get('katakana_cer') is not None]
682+
if kata:
683+
avg_metrics['katakana_cer_filewise_avg'] = sum(m['katakana_cer'] for m in kata) / len(kata)
684+
avg_metrics['katakana_cer_cumulative'] = word_error_rate_detail(
685+
hypotheses=[m['pred_katakana'] for m in kata],
686+
references=[m['gt_katakana'] for m in kata],
687+
use_cer=True,
688+
)[0]
654689
avg_metrics['ssim_pred_gt_avg'] = sum(m['pred_gt_ssim'] for m in filewise_metrics) / n
655690
avg_metrics['ssim_pred_context_avg'] = sum(m['pred_context_ssim'] for m in filewise_metrics) / n
656691
avg_metrics['ssim_gt_context_avg'] = sum(m['gt_context_ssim'] for m in filewise_metrics) / n

nemo/collections/tts/parts/utils/tts_dataset_utils.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import functools
16+
import importlib
1617
import logging
1718
import os
1819
import random
@@ -944,6 +945,50 @@ def process_text_for_wer(self, text: str) -> str:
944945
return text
945946

946947

948+
class NoSpaceTextProcessor(TextProcessor):
949+
"""WER text processor for languages where ASR/tokenization spaces should be ignored."""
950+
951+
def __init__(self):
952+
super().__init__()
953+
self.default_processor = DefaultTextProcessor()
954+
955+
def normalize_text(self, text: str) -> str:
956+
return text
957+
958+
def process_text_for_wer(self, text: str) -> str:
959+
text = self.default_processor.process_text_for_wer(text)
960+
text = text.replace(" ", "")
961+
return text
962+
963+
964+
class JapaneseTextProcessor(NoSpaceTextProcessor):
965+
"""Japanese WER text processor with Katakana reading conversion."""
966+
967+
def __init__(self):
968+
super().__init__()
969+
try:
970+
self.pyopenjtalk = importlib.import_module("pyopenjtalk")
971+
except ImportError as e:
972+
raise ImportError(
973+
"JapaneseTextProcessor requires pyopenjtalk for Katakana CER computation. "
974+
"Install pyopenjtalk or do not request Japanese evaluation text processing."
975+
) from e
976+
977+
def text_to_katakana(self, text: str) -> str:
978+
"""Convert Japanese text to its Katakana reading via pyopenjtalk.
979+
980+
Used for an additional, reading-based Japanese CER metric that is robust to
981+
kanji/kana spelling variation between the reference and the ASR hypothesis.
982+
"""
983+
if not text:
984+
return ""
985+
try:
986+
return self.pyopenjtalk.g2p(text, kana=True).strip()
987+
except Exception as e: # noqa: BLE001
988+
logging.warning(f"pyopenjtalk failed for '{text[:40]}': {e}")
989+
return ""
990+
991+
947992
class EnglishTextProcessor(TextProcessor):
948993
"""English text processing, which catches some edge cases not covered by normal text normalization.
949994
@@ -980,8 +1025,13 @@ def process_text_for_wer(self, text: str) -> str:
9801025

9811026

9821027
def get_text_processor(language: str) -> TextProcessor:
1028+
language = (language or "").replace("_", "-").lower().split("-")[0]
9831029
if language == "en":
9841030
return EnglishTextProcessor()
1031+
if language == "ja":
1032+
return JapaneseTextProcessor()
1033+
elif language == "zh":
1034+
return NoSpaceTextProcessor()
9851035
else:
9861036
logging.info(f"Text processing not implemented for language {language}; using default processor")
9871037
return DefaultTextProcessor()

0 commit comments

Comments
 (0)