Skip to content

Migrate editdistance metrics to rapidfuzz with empty-reference guard#3114

Merged
LauraGPT merged 3 commits into
mainfrom
codex/fix-pr-3105-empty-ref-metrics
Jul 7, 2026
Merged

Migrate editdistance metrics to rapidfuzz with empty-reference guard#3114
LauraGPT merged 3 commits into
mainfrom
codex/fix-pr-3105-empty-ref-metrics

Conversation

@LauraGPT

@LauraGPT LauraGPT commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Maintainer-owned replacement for #3105. Thanks to @excosy for starting the editdistance -> rapidfuzz migration.

This PR:

  • replaces archived editdistance with rapidfuzz.distance.Levenshtein
  • updates runtime and install dependencies to require rapidfuzz>=3.0.0
  • preserves CER/WER behavior for normal non-empty references
  • fixes the remaining empty-reference divide-by-zero case found while reviewing Migrate obsoleted editdistance to rapidfuzz #3105

Root Cause

The original migration guarded only on whether edit-distance entries existed. That still divides by zero when the input list is non-empty but every reference is empty or whitespace-only:

calculate_cer(["abc"], ["   "])
calculate_wer(["hello world"], ["   "])

This PR calculates total reference length explicitly and returns None when it is zero, matching the existing CTC metric no-valid-reference behavior.

Testing

python -m pytest tests/test_metrics_common_rapidfuzz.py -q
python -m py_compile funasr/metrics/common.py tests/test_metrics_common_rapidfuzz.py setup.py
git diff --check origin/main...HEAD

Additional validation on ind-gpu8:

  • red before fix: tests/test_metrics_common_rapidfuzz.py failed with ZeroDivisionError
  • green after fix: 2 passed
  • temporary real rapidfuzz>=3.0.0 install verified string and token-list Levenshtein.distance() semantics

@LauraGPT LauraGPT merged commit cd15a47 into main Jul 7, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request replaces the editdistance dependency with rapidfuzz in funasr/metrics/common.py and the setup configuration. It also handles division-by-zero scenarios in CER and WER calculations when the reference length is zero. A new test file is introduced to verify these changes. The reviewer recommends simplifying the test file by removing the complex mocking of rapidfuzz and dynamic module loading, and instead using standard imports and the real library.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1 to +72
import importlib.util
import sys
import types
from pathlib import Path


def _install_fake_rapidfuzz():
rapidfuzz = types.ModuleType("rapidfuzz")
distance = types.ModuleType("rapidfuzz.distance")

class Levenshtein:
@staticmethod
def distance(left, right):
left = list(left)
right = list(right)
dp = list(range(len(right) + 1))
for i, lval in enumerate(left, 1):
prev = dp[0]
dp[0] = i
for j, rval in enumerate(right, 1):
old = dp[j]
dp[j] = min(
dp[j] + 1,
dp[j - 1] + 1,
prev + (0 if lval == rval else 1),
)
prev = old
return dp[-1]

distance.Levenshtein = Levenshtein
rapidfuzz.distance = distance
sys.modules.setdefault("rapidfuzz", rapidfuzz)
sys.modules.setdefault("rapidfuzz.distance", distance)


def _load_metrics_common():
_install_fake_rapidfuzz()
module_path = Path(__file__).resolve().parents[1] / "funasr" / "metrics" / "common.py"
spec = importlib.util.spec_from_file_location("metrics_common_probe", module_path)
module = importlib.util.module_from_spec(spec)
sys.modules["metrics_common_probe"] = module
assert spec.loader is not None
spec.loader.exec_module(module)
return module


def test_cer_and_wer_return_none_when_reference_length_is_zero():
common = _load_metrics_common()
calc = common.ErrorCalculator(
char_list=["<blank>", "a", "b", "c", "h", "e", "l", "o", "w", "r", "d", " "],
sym_space=" ",
sym_blank="<blank>",
report_cer=True,
report_wer=True,
)

assert calc.calculate_cer(["abc"], [" "]) is None
assert calc.calculate_wer(["hello world"], [" "]) is None


def test_cer_and_wer_keep_normal_levenshtein_semantics():
common = _load_metrics_common()
calc = common.ErrorCalculator(
char_list=["<blank>", "a", "b", "c", "x", "y", " "],
sym_space=" ",
sym_blank="<blank>",
report_cer=True,
report_wer=True,
)

assert calc.calculate_cer(["abc"], ["axc"]) == 1 / 3
assert calc.calculate_wer(["foo bar baz"], ["foo qux baz"]) == 1 / 3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Mocking rapidfuzz with a custom pure-Python Levenshtein implementation and using dynamic module loading (importlib.util) is an anti-pattern here. Since rapidfuzz is a required dependency in setup.py, it is guaranteed to be installed in any standard test environment. Mocking it defeats the purpose of testing the actual integration with the library, hides potential import/compatibility issues, and adds unnecessary complexity to the test suite. Simplifying the test file to use standard imports and the real rapidfuzz library makes the tests cleaner, more maintainable, and more reliable.

from funasr.metrics.common import ErrorCalculator


def test_cer_and_wer_return_none_when_reference_length_is_zero():
    calc = ErrorCalculator(
        char_list=["<blank>", "a", "b", "c", "h", "e", "l", "o", "w", "r", "d", " "],
        sym_space=" ",
        sym_blank="<blank>",
        report_cer=True,
        report_wer=True,
    )

    assert calc.calculate_cer(["abc"], ["   "]) is None
    assert calc.calculate_wer(["hello world"], ["   "]) is None


def test_cer_and_wer_keep_normal_levenshtein_semantics():
    calc = ErrorCalculator(
        char_list=["<blank>", "a", "b", "c", "x", "y", " "],
        sym_space=" ",
        sym_blank="<blank>",
        report_cer=True,
        report_wer=True,
    )

    assert calc.calculate_cer(["abc"], ["axc"]) == 1 / 3
    assert calc.calculate_wer(["foo bar baz"], ["foo qux baz"]) == 1 / 3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants