-
Notifications
You must be signed in to change notification settings - Fork 196
[WIP]Add CS-FLEURS code-switched ASR benchmark with Mixed Error Rate scoring #1494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Slyne
wants to merge
12
commits into
NVIDIA-NeMo:main
Choose a base branch
from
Slyne:cs-fleurs-eval
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 9 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
0edd45b
Add CS-FLEURS massively-multilingual code-switched ASR benchmark
Slyne 318dc09
Skip HuggingFace download when CS-FLEURS subset already complete
Slyne c7cf8ef
Add CS-FLEURS tests: exclude from GPU auto-prepare + unit-test scoring
Slyne 7a8cd4a
Register cs-fleurs in test_datasets manifest
Slyne 9159169
Add Mixed Error Rate (MER) scoring for CS-FLEURS code-switching
Slyne 4c8c571
Address PR review feedback for CS-FLEURS
Slyne e30739d
docs: add Whisper large-v3 baseline results for cs-fleurs.read
Slyne 87cd1b8
docs: render cs-fleurs baseline as house-style Example output block
Slyne c51df8c
Add paper-comparable CER column for CS-FLEURS
Slyne 38c25b1
Preserve combining marks in MER normalization for CS-FLEURS
Slyne 66912e6
docs: update CS-FLEURS baseline to mark-preserving MER + paper CER
Slyne 8d8d1ed
Make CS-FLEURS CER space-insensitive to match the paper
Slyne File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """CS-FLEURS: massively multilingual code-switched ASR evaluation. | ||
|
|
||
| Code-switched speech recognition across the four CS-FLEURS test sets, each a | ||
| sub-benchmark scored with per-language CER/WER (CER for scriptio-continua matrix | ||
| languages, WER otherwise): | ||
|
|
||
| - ``cs-fleurs.read`` 14 X-English pairs, human-read speech (the paper's | ||
| intended, human-validated benchmarking set) | ||
| - ``cs-fleurs.mms`` 45 X-English pairs, concatenative MMS-TTS speech | ||
| - ``cs-fleurs.xtts-test1`` 16 X-English pairs, generative XTTS-v2 speech | ||
| - ``cs-fleurs.xtts-test2`` 60 language pairs, generative XTTS-v2 speech | ||
|
|
||
| Dataset: https://huggingface.co/datasets/byan/cs-fleurs (CC-BY-NC-4.0) | ||
| Paper: CS-FLEURS: A Massively Multilingual and Code-Switched Speech Dataset | ||
| (https://arxiv.org/abs/2509.14161) | ||
| """ | ||
|
|
||
| REQUIRES_DATA_DIR = True | ||
| IS_BENCHMARK_GROUP = True | ||
| SCORE_MODULE = "nemo_skills.dataset.cs-fleurs.audio_score" | ||
|
|
||
| BENCHMARKS = { | ||
| "cs-fleurs.read": {}, | ||
| "cs-fleurs.mms": {}, | ||
| "cs-fleurs.xtts-test1": {}, | ||
| "cs-fleurs.xtts-test2": {}, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| # Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """Group-level aggregation for the CS-FLEURS code-switched ASR benchmark. | ||
|
|
||
| Each sub-benchmark (read / mms / xtts-test1 / xtts-test2) already reports a | ||
| per-language-pair WER/CER breakdown via ``subset_for_metrics``. This module adds | ||
| a per-test-set headline number (entry-weighted WER/CER, where CER is stored | ||
| under the ``wer`` key for consistency with corpus-level aggregation) plus an | ||
| overall entry-weighted figure across the prepared test sets. | ||
|
|
||
| Note: the test sets differ in nature — ``read`` is human-validated while the | ||
| others are synthetic (XTTS / MMS) — so the per-test-set numbers are the | ||
| meaningful comparison; the overall figure is a convenience aggregate only. | ||
| """ | ||
|
|
||
| SUBSET_NAMES = ["read", "mms", "xtts-test1", "xtts-test2"] | ||
|
|
||
|
|
||
| def compute_score(combined_metrics: dict) -> dict: | ||
| """Aggregate CS-FLEURS sub-benchmark metrics into per-test-set + overall scores. | ||
|
|
||
| ``combined_metrics`` maps each sub-benchmark name (e.g. ``cs-fleurs.read``) | ||
| to a dict of eval-mode -> metrics dict (as emitted by the audio metrics | ||
| computation). Returns ``{eval_mode: {subset_name: {...}, "overall": {...}}}``. | ||
| """ | ||
| benchmarks = {k: v for k, v in combined_metrics.items() if k.split(".")[-1] in SUBSET_NAMES} | ||
| if not benchmarks: | ||
| return {} | ||
|
|
||
| weighted_metrics = ["wer", "wer_macro"] | ||
| summed_metrics = ["substitutions", "insertions", "deletions", "ref_words"] | ||
| # Generation/timing metrics that are entry-weighted like WER but only emitted | ||
| # when present: pure-ASR runs omit success_rate / no_answer, so we must not | ||
| # fabricate them as 0. | ||
| optional_weighted = ["success_rate", "avg_tokens", "no_answer"] | ||
|
|
||
| first_benchmark = next(iter(benchmarks.values())) | ||
| eval_modes = list(first_benchmark.keys()) | ||
|
|
||
| def _summarize(metrics_list: list[dict]) -> dict | None: | ||
| total_entries = 0 | ||
| total_gen_seconds = 0 | ||
| gen_seconds_seen = False | ||
| weighted_sums = {m: 0.0 for m in weighted_metrics + optional_weighted} | ||
| weighted_counts = {m: 0 for m in weighted_metrics + optional_weighted} | ||
| sums = {m: 0 for m in summed_metrics} | ||
| # Track presence separately so a legitimately-zero metric (e.g. 0 | ||
| # substitutions on a perfect subset) is still emitted, rather than | ||
| # dropped by a truthiness check on the sum. | ||
| seen = {m: False for m in summed_metrics} | ||
|
|
||
| for metrics in metrics_list: | ||
| num_entries = metrics["num_entries"] # required; fail loudly if absent | ||
| if num_entries == 0: | ||
| continue | ||
| total_entries += num_entries | ||
| if "gen_seconds" in metrics: | ||
| total_gen_seconds += metrics["gen_seconds"] | ||
| gen_seconds_seen = True | ||
| for m in weighted_metrics + optional_weighted: | ||
| if m in metrics: | ||
| weighted_sums[m] += metrics[m] * num_entries | ||
| weighted_counts[m] += num_entries | ||
| for m in summed_metrics: | ||
| if m in metrics: | ||
| sums[m] += metrics[m] | ||
| seen[m] = True | ||
|
|
||
| if total_entries == 0: | ||
| return None | ||
|
|
||
| agg = {"num_entries": total_entries} | ||
| if gen_seconds_seen: | ||
| agg["gen_seconds"] = total_gen_seconds | ||
| for m in weighted_metrics: | ||
| if weighted_counts[m] > 0: | ||
| agg[m] = round(weighted_sums[m] / weighted_counts[m], 2) | ||
| for m in optional_weighted: | ||
| if weighted_counts[m] > 0: | ||
| value = weighted_sums[m] / weighted_counts[m] | ||
| agg[m] = int(value) if m == "avg_tokens" else value | ||
| for m in summed_metrics: | ||
| if seen[m]: | ||
| agg[m] = sums[m] | ||
| return agg | ||
|
|
||
| aggregated: dict[str, dict] = {} | ||
| for eval_mode in eval_modes: | ||
| per_subset = {} | ||
| for name, benchmark_data in benchmarks.items(): | ||
| if eval_mode not in benchmark_data: | ||
| continue | ||
| summary = _summarize([benchmark_data[eval_mode]]) | ||
| if summary is not None: | ||
| per_subset[name.split(".")[-1]] = summary | ||
|
|
||
| if not per_subset: | ||
| continue | ||
|
|
||
| overall = _summarize(list(per_subset.values())) | ||
| if overall is not None: | ||
| per_subset["overall"] = overall | ||
| aggregated[eval_mode] = per_subset | ||
|
|
||
| return aggregated |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| # Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """Static CS-FLEURS language metadata. | ||
|
|
||
| CS-FLEURS metadata identifies languages by ISO 639-3 codes (e.g. ``ara``, | ||
| ``cmn``, ``deu``) inside a ``"<matrix>-<embedded>"`` ``language`` field, which | ||
| differs from the BCP-47 locales used by the plain ``fleurs`` benchmark. This | ||
| table maps each code to a display name and an ISO 639-1 code; the latter is | ||
| passed to the audio evaluator as ``src_lang`` so that its number-normalization | ||
| (``num2words``) and multilingual normalizer behave correctly. Codes without a | ||
| ISO 639-1 form map to ``None`` (number normalization is then skipped, which the | ||
| evaluator handles gracefully). | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| # ISO 639-3 code -> (display name, ISO 639-1 code or None for num2words/normalization) | ||
| CS_FLEURS_LANGUAGES: dict[str, tuple[str, str | None]] = { | ||
| "ara": ("Arabic", "ar"), | ||
| "aze": ("Azerbaijani", "az"), | ||
| "ben": ("Bengali", "bn"), | ||
| "bul": ("Bulgarian", "bg"), | ||
| "cat": ("Catalan", "ca"), | ||
| "ceb": ("Cebuano", None), | ||
| "ces": ("Czech", "cs"), | ||
| "cmn": ("Mandarin Chinese", "zh"), | ||
| "cym": ("Welsh", "cy"), | ||
| "deu": ("German", "de"), | ||
| "ell": ("Greek", "el"), | ||
| "eng": ("English", "en"), | ||
| "fin": ("Finnish", "fi"), | ||
| "fra": ("French", "fr"), | ||
| "guj": ("Gujarati", "gu"), | ||
| "heb": ("Hebrew", "he"), | ||
| "hin": ("Hindi", "hi"), | ||
| "hun": ("Hungarian", "hu"), | ||
| "ind": ("Indonesian", "id"), | ||
| "isl": ("Icelandic", "is"), | ||
| "ita": ("Italian", "it"), | ||
| "jav": ("Javanese", "jv"), | ||
| "jpn": ("Japanese", "ja"), | ||
| "kan": ("Kannada", "kn"), | ||
| "kaz": ("Kazakh", "kk"), | ||
| "khm": ("Khmer", "km"), | ||
| "kir": ("Kyrgyz", "ky"), | ||
| "kor": ("Korean", "ko"), | ||
| "lao": ("Lao", "lo"), | ||
| "lav": ("Latvian", "lv"), | ||
| "lug": ("Ganda", "lg"), | ||
| "mal": ("Malayalam", "ml"), | ||
| "mar": ("Marathi", "mr"), | ||
| "mya": ("Burmese", "my"), | ||
| "nld": ("Dutch", "nl"), | ||
| "pan": ("Punjabi", "pa"), | ||
| "pol": ("Polish", "pl"), | ||
| "por": ("Portuguese", "pt"), | ||
| "ron": ("Romanian", "ro"), | ||
| "rus": ("Russian", "ru"), | ||
| "slk": ("Slovak", "sk"), | ||
| "spa": ("Spanish", "es"), | ||
| "swe": ("Swedish", "sv"), | ||
| "swh": ("Swahili", "sw"), | ||
| "tam": ("Tamil", "ta"), | ||
| "tel": ("Telugu", "te"), | ||
| "tgk": ("Tajik", "tg"), | ||
| "tgl": ("Tagalog", "tl"), | ||
| "tha": ("Thai", "th"), | ||
| "tur": ("Turkish", "tr"), | ||
| "ukr": ("Ukrainian", "uk"), | ||
| "urd": ("Urdu", "ur"), | ||
| "uzb": ("Uzbek", "uz"), | ||
| "vie": ("Vietnamese", "vi"), | ||
| "yor": ("Yoruba", "yo"), | ||
| "yue": ("Cantonese Chinese", None), | ||
| "zlm": ("Malay", "ms"), | ||
| } | ||
|
|
||
| # Matrix languages scored with Character Error Rate instead of Word Error Rate | ||
| # (scriptio-continua: no explicit word boundaries). Mirrors the fleurs benchmark | ||
| # CER_LOCALES, expressed in ISO 639-3. | ||
| CER_LANGS: frozenset[str] = frozenset( | ||
| { | ||
| "cmn", # Mandarin Chinese | ||
| "yue", # Cantonese Chinese | ||
| "jpn", # Japanese | ||
| "kor", # Korean | ||
| "tha", # Thai | ||
| "lao", # Lao | ||
| "mya", # Burmese | ||
| "khm", # Khmer | ||
| "vie", # Vietnamese | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def split_pair(language: str) -> tuple[str, str]: | ||
| """Split a CS-FLEURS ``language`` field (``"<matrix>-<embedded>"``). | ||
|
|
||
| Returns ``(matrix_code, embedded_code)``. If no separator is present the | ||
| whole string is treated as the matrix language with an empty embedded code. | ||
| """ | ||
| parts = language.replace("_", "-").split("-") | ||
| matrix = parts[0] | ||
| embedded = parts[1] if len(parts) > 1 else "" | ||
| return matrix, embedded | ||
|
|
||
|
|
||
| def get_lang_name(code: str) -> str: | ||
| """Display name for an ISO 639-3 code. | ||
|
|
||
| Raises KeyError on an unknown code: every CS-FLEURS language is expected in | ||
| the table, so an unknown code signals a dataset/table mismatch we want to | ||
| fail on loudly rather than paper over. | ||
| """ | ||
| return CS_FLEURS_LANGUAGES[code][0] | ||
|
|
||
|
|
||
| def get_iso1(code: str) -> str | None: | ||
| """ISO 639-1 code for a (known) ISO 639-3 code. | ||
|
|
||
| Returns None for languages that have no ISO 639-1 form (e.g. ``ceb``, | ||
| ``yue``); number normalization is then skipped, which the evaluator handles. | ||
| Raises KeyError on an unknown code (see ``get_lang_name``). | ||
| """ | ||
| return CS_FLEURS_LANGUAGES[code][1] | ||
|
|
||
|
|
||
| def uses_cer(matrix_code: str) -> bool: | ||
| """Whether a code-switched pair with this matrix language is scored with CER.""" | ||
| return matrix_code in CER_LANGS | ||
|
|
||
|
|
||
| # Scripts whose characters carry spacing/non-spacing combining marks (vowel | ||
| # signs, tone marks). For these, Mixed Error Rate must segment by grapheme | ||
| # cluster, since per-codepoint splitting would separate a base consonant from | ||
| # its marks. Han / kana / Hangul are precomposed (no combining marks), so | ||
| # codepoint-level segmentation is already correct and they are intentionally | ||
| # excluded here. | ||
| GRAPHEME_CLUSTER_LANGS: frozenset[str] = frozenset( | ||
| { | ||
| "tha", # Thai | ||
| "lao", # Lao | ||
| "mya", # Burmese | ||
| "khm", # Khmer | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def uses_grapheme(code: str) -> bool: | ||
| """Whether this language's script needs grapheme-cluster MER segmentation.""" | ||
| return code in GRAPHEME_CLUSTER_LANGS |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.