All posts

· Casimiro Ferreira· 9 min read

Measuring Speech Quality Without a Listening Panel

  • speechonnxmetrics
  • TTS
  • ONNX
  • evaluation

A denoising model ships a new checkpoint. A TTS voice gets retrained on more data. A voice-cloning pipeline swaps its vocoder. In each case, someone has to answer: is the output better or worse than before?

“It sounds better to me” does not scale. It breaks down the moment the language isn’t one you speak, the moment there are twenty checkpoints to compare instead of two, or the moment the change needs to be checked on every commit instead of once by hand.

The rigorous answer to “does it sound better” is a Mean Opinion Score (MOS): put the audio in front of a panel of listeners, ask each to rate it 1 to 5, and average the scores. MOS is the standard speech-quality metric because it asks the question that matters, would a human find this acceptable, instead of a proxy for it.

It is also expensive. Recruiting a panel, running it consistently, and repeating it for every language, every recording condition and every model version a small team ships is not something a listening panel can keep up with.

speechonnxmetrics is a library for approximating that judgement without a panel, on every build. It groups its metrics into three families, and picking the right family for the situation matters more than any individual number.

Three families, three questions

No-reference MOS estimators are neural networks trained to predict what a listening panel would say, from the audio alone. They need no clean original, only the output you want to judge.

Use this family when there is no ground truth to compare against: scoring a TTS system’s output, or checking a real, already-degraded recording after denoising.

Intrusive metrics need a matching clean reference and measure the distance between it and the degraded signal. Use this family when you manufactured the degradation yourself and still hold the clean original: you fed a known-good recording through a codec, a bandwidth-extension model, or a voice converter, and want to know how far the output drifted from the source.

ASR-based text metrics transcribe the output with a speech recognizer and diff the transcript against the expected text. This catches something the other two families cannot: audio that sounds perfectly natural and clean but says the wrong words.

A no-reference MOS predictor rates naturalness, not correctness, so a fluent mispronunciation scores fine. An intrusive metric needs a reference waveform, not a reference sentence. Only comparing text catches a wrong word.

The library exposes these through one function:

import speechonnxmetrics as s

# No-reference: only the output needed, no ground truth exists
s.score("output.wav", ["utmos"])

# Intrusive: needs a clean reference, ref= is required
s.score("degraded.wav", ["stoi", "mcd", "si_sdr"], ref="clean.wav")

Text metrics live in a separate module, speechonnxmetrics.asr, because they compare strings, not audio. s.score() only dispatches metrics that take a waveform.

No-reference MOS: reading the numbers

Four MOS estimators ship, each returning values on a 1–5 scale where higher is better, the same scale a human panel uses:

metricdimensionstrained oncommercial use
utmosone naturalness scoresynthesized speech (VoiceMOS Challenge)yes
dnsmossig / bak / ovrl (speech quality / background noise / overall)ITU-T P.835yes
dnsmos_p808one crowdsourced-listening MOSITU-T P.808yes
sigmos7 dimensions (col, disc, loud, noise, reverb, sig, ovrl)ITU-T P.804yes
nisqamos plus noi/dis/col/loud breakdownNISQA-v2no — CC BY-NC-SA 4.0

nisqa is the one metric in the whole library with non-commercial weights. The other four are MIT-licensed. speechonnxmetrics does not filter this for you. It states the licence and leaves the choice to the caller.

Here is what real audio scores. Running the library’s own bundled fixtures (a clean recording, source.wav, and a neural-codec resynthesis of the same clip, facodec_aria.wav) through UTMOS:

>>> s.score("source.wav", ["utmos"])
{'utmos': 4.41}
>>> s.score("facodec_aria.wav", ["utmos"])
{'utmos': 3.21}

The clean recording lands near the top of the scale, as it should. It is real human speech, not synthesized. The codec resynthesis drops over a full point. That gap, more than either number alone, is the useful signal: it tells you the codec introduces audible degradation, and it gives you a number to track as the codec is tuned.

DNSMOS on the same clean recording:

>>> s.score("source.wav", ["dnsmos"])
{'dnsmos.sig': 3.45, 'dnsmos.bak': 3.60, 'dnsmos.ovrl': 2.93}

Three numbers, not one, and they diverge: ovrl sits noticeably below both sig and bak. That divergence is informative rather than a bug. ovrl is P.835’s rating of the overall listening experience, and it tends to punish a recording harder than either component score alone would suggest, especially for a real-world recording rather than a studio one.

When bak is low, look for background noise. When sig is low, look for voice-level artifacts: clipping, dropouts, robotic timbre. Report more than one predictor for the same clip. They are trained on different data and disagree in informative ways, and a wide gap between two independent predictors on the same clip is a cue to go and listen.

Intrusive metrics: reading the numbers

Eleven reference-based metrics measure distance from a clean original. The ones worth knowing first:

metricrangedirectionmeasures
stoi / estoi0–1higher is bettershort-time objective intelligibility — how much of the content survives, independent of how natural it sounds
si_sdr / sdr / snrdB, unboundedhigher is bettersignal-to-distortion / signal-to-noise ratio
mcddB, unboundedlower is bettermel-cepstral distortion — spectral-envelope distance, a classic TTS/VC quality metric
log_f0_rmseunboundedlower is betterpitch-contour error
lsd / msddBlower is betterlog-spectral / mel-spectral distance

Note the direction flips: STOI and the SDR family go up when quality is better, while MCD, pitch error and spectral distance go down. Mixing them up when reading a table is an easy mistake.

Scoring the same codec resynthesis against its clean source:

>>> s.score("facodec_aria.wav", ["stoi", "mcd", "si_sdr"], ref="source.wav")
{'stoi': 0.662, 'mcd': 10.46, 'si_sdr': -26.94}

STOI of 0.66 on a 0–1 scale where 1.0 is a perfect match says intelligibility took a real hit. This is well below what a lightly-processed recording would score.

An SI-SDR of roughly −27 dB confirms it. SI-SDR is negative whenever the distortion energy outweighs the signal, and a large negative number means heavy structural change, not just added noise. MCD of 10.46 dB is high. Published TTS systems that sound clearly synthetic but still speaker-consistent typically land in single digits, so 10+ points to substantial spectral-envelope drift between the resynthesis and the original.

ASR-based metrics: reading the numbers

Five text metrics come from one Levenshtein alignment between a reference transcript and a hypothesis (what the audio was actually transcribed as):

metricrangedirectionmeaning
wer≥ 0 (usually 0–1, can exceed 1)lower is betterword error rate: substitutions + deletions + insertions, divided by reference word count
cer0–1lower is bettersame idea at the character level — more forgiving of minor spelling/tokenization mismatches
mer0–1lower is bettermatch error rate
wil0–1lower is betterword information lost
wip0–1higher is betterword information preserved (1 − wil)

A worked example: reference “the quick brown fox jumps over the lazy dog” against hypothesis “the quick brown fox jumped over a lazy dog” (one substitution, “jumps” → “jumped”, one deletion of “the”):

>>> from speechonnxmetrics import asr
>>> from speechonnxmetrics.asr import BASIC
>>> asr.wer(reference, hypothesis, normalizer=BASIC)
0.222
>>> asr.cer(reference, hypothesis, normalizer=BASIC)
0.116

A WER of 0.22 means roughly one word in five is wrong: noticeable, worth listening to. CER is lower on the same pair because character-level scoring treats a one-word substitution as a handful of character edits inside a much longer character string, not a whole missing token.

CER and WER answer different questions and are not directly comparable to each other. WER above roughly 0.3–0.4 on natural speech usually means the ASR system, or the audio it’s transcribing, has a real problem, not a rounding error.

speechonnxmetrics never normalizes text on your behalf. A raw comparison counts case and punctuation as errors, which is rarely what you want when scoring pronunciation rather than exact transcription formatting. Pass a normalizer explicitly: BASIC lowercases and collapses whitespace, STRICT also expands contractions and strips diacritics, punctuation and filler words.

The caveat that matters most

Every no-reference MOS number in this library is a prediction from a model, not a measurement of a fact. UTMOS, DNSMOS, SIGMOS and NISQA were each trained on a specific set of listening-test data, in specific languages and recording conditions.

A predictor trained mostly on English studio recordings can misjudge a language it never saw in training, an accent its training panel never rated, or a recording condition (telephone audio, a noisy room, a low-resource microphone) outside its training distribution. The model is not lying. It is extrapolating, and extrapolation from unfamiliar inputs is where neural predictors are least reliable.

Treat a predicted MOS as evidence, not as ground truth. It is trustworthy for what it is good at: catching large regressions, ranking several candidates against each other, and flagging a run that needs a human to actually listen.

It is not a substitute for a real listening panel when a decision is high-stakes, and it should not be the last word on a language or condition the underlying model wasn’t trained to judge. The practical mitigation is to report several predictors together and treat disagreement between them as a prompt to listen rather than noise to average away.

What this makes possible

None of this is useful in isolation. It becomes useful the moment several engines need to be compared on the same footing: which TTS engine, which STT engine, which enhancement model to default to.

The pure-ONNX speech libraries that speechonnxmetrics was built to evaluate (TTS, ASR, denoising, voice cloning) publish per-engine comparisons produced with exactly the metrics above: no-reference MOS for systems with no ground truth, intrusive metrics where a clean reference exists, WER/CER wherever correctness of the transcript is in question. That is what turns “we picked this engine” into a number someone else can check.

If your project needs a language, an engine or a recording condition evaluated this way and it isn’t covered yet, get in touch or look at our services.