Skip to main content

speech-quality

Measure whether a voice recording is clean enough to transcribe or publish, and say in plain words what is wrong with it when it is not.

Install

pip install speech-quality

The only dependency is numpy. WAV files are read with the standard library wave module, so there is no audio library to install, no codec to find and nothing is ever downloaded.

Quickstart

import numpy as np
from speech_quality import assess

t = np.arange(3 * 16000) / 16000.0                          # three seconds at 16 kHz
hiss = np.random.default_rng(0).standard_normal(t.size + 4)
voice = np.convolve(hiss, np.ones(5) / 5, "valid")          # roll off the top
voice *= 0.35 * (0.5 + 0.5 * np.sin(2 * np.pi * 3.5 * t))   # syllable-rate envelope
print(assess(voice, sample_rate=16000).summary())
recording: usable (score 96.9 / 100, grade A)
  3.00 s, 16000 Hz, mono
measures:
  ok   level       98.7  -20.27 dBFS    level is healthy at -20.3 dBFS RMS, peaks at -3.6 dBFS, 3.6 dB of headroom left
  ok   clipping   100.0  0.0%           no samples reach full scale, so nothing is clipped
  ok   noise       85.7  25.12 dB       the voice stands 25.1 dB above a -41.7 dBFS noise floor, clean enough to transcribe
  ok   silence     99.5  3.2%           3% silence overall, 0.00 s at the start and 0.00 s at the end, which is normal for speech
  ok   speech     100.0  96.8%          97% of the recording carries speech, centred on 1300 Hz with the level swinging 9.0 dB between syllables
  ok   dynamics   100.0  16.52 dB       a 16.5 dB crest factor over the parts that carry sound, the natural rise and fall of unprocessed speech
  ok   bandwidth  100.0  8000 Hz        energy reaches 8000 Hz, 100% of the way to the 8000 Hz ceiling, wide enough for clear consonants
issues: none

On a real file it is one line:

report = assess("interview.wav")
if not report.usable:
    print(report.issues[0])

What it checks

Seven measures, each reporting the raw number it found, a 0-100 score, whether that passes, and a sentence you can act on.

  • level - RMS and peak in dBFS, and how far the level sits from the -20 dBFS a healthy speech recording holds. Catches material recorded too quiet to survive noise reduction and material recorded too hot to survive anything.
  • clipping - the share of samples pinned at or near full scale, how many separate clipped runs there are and how long the worst one lasts. Clipping is destroyed waveform; no processing brings it back.
  • noise - the noise floor taken from the quietest decile of frames, and the signal-to-noise ratio between that and the loudest decile. No voice-activity decision is involved, so none can go wrong.
  • silence - leading silence, trailing silence and the total silent share, so dead air is trimmed before anyone pays to transcribe it.
  • speech - the share of the recording carrying speech-like energy, judged on band energy, spectral centroid and whether the level actually swings at syllable rate. A steady tone passes every spectral test and is still not speech.
  • dynamics - crest factor, and whether the life has been compressed or limited out of the recording.
  • bandwidth - the highest frequency still carrying real energy. This is what exposes telephone-band audio, and audio upsampled from a narrower original: the file says 48 kHz, the sound stops at 3.4 kHz, and every consonant that lived above that is already gone.

Four of those faults are damage rather than a chore. Clipped samples are a destroyed waveform, noise is already mixed in under the voice, a recording that does not behave like speech has nothing in it to transcribe, and a band that stops at 3.4 kHz has lost its consonants. Nothing you do later brings any of them back, so any one of clipping, noise, speech or bandwidth failing makes the recording unusable, however well the other measures average out; report.blocking names the ones that did and the CLI exits 1. A level 6 dB off, dead air at the ends and heavy compression are all things a later pass fixes, so they pull the score down without condemning the take.

Input can be a path to a .wav file (PCM 8, 16, 24 or 32 bit, 8 kHz to 48 kHz and beyond), a numpy array of samples, or a (samples, sample_rate) pair. Multi-channel audio is mixed down to mono and the mixdown is recorded in report.notes. Your array is never written to. The same input always gives the same answer.

These are signal measurements, not a perceptual model. speech-quality is not PESQ, POLQA or ViSQOL and does not estimate a mean opinion score. It tells you that a recording clips, hisses, sits 8 dB too quiet or stops dead at 3.4 kHz. It does not tell you how a listener would rate it, and it has no opinion at all about accent, delivery or content.

API

from speech_quality import assess, assess_batch, signal_to_noise, estimate_noise_floor

assess(audio, *, sample_rate=None, thresholds=None, source=None) -> AudioReport Assess one recording.

assess_batch(items, *, sample_rate=None, thresholds=None) -> BatchReport Assess many, keeping going when one cannot be read. An item may be a (label, recording) pair, which is how arrays get names.

signal_to_noise(audio, **kw) -> float the ratio in dB. estimate_noise_floor(audio, **kw) -> float the floor in dBFS.

AudioReport

attribute meaning
.score 0-100 overall, the weighted average of the measures
.grade "A" (best) through "F"
.usable good enough to transcribe or publish: the score clears usable_score, enough of the measures could be taken, and none of the four decisive ones failed
.blocking the decisive measures that failed, worst fault first; empty when none did
.metrics dict[name -> Metric], one per measure
.issues what is wrong, worst first, one sentence each
.notes decisions taken while loading, such as a stereo mixdown
.coverage share of the measures that had anything to work with
.duration, .sample_rate, .channels, .digital_silence what was read
.summary() the whole report as plain text
.to_dict() the whole report as JSON-safe data

Metric carries .value (the raw number), .unit, .score (0-100), .ok, .measured and .message. .measured is False when the recording could not support that measurement - a spectrum of four samples, a noise floor of a signal whose level never drops - in which case the score is a neutral placeholder and not a judgement, and .ok is None rather than True, because nothing was measured and nothing passed. A recording most of the measures could not touch is never called usable, however well the placeholders average out.

BatchReport carries .results, .failures, .usable, .unusable, .mean_score, .worst(n=5), .summary() and .to_dict().

Thresholds holds every limit the verdict uses, and any of them can be moved:

from speech_quality import assess

report = assess("podcast.wav", thresholds={"min_snr_db": 25, "usable_score": 70})

CLI

speech-quality interview.wav                       # the report as text
speech-quality takes/*.wav --worst 3               # a batch, worst offenders first
speech-quality interview.wav --json                # the report as JSON
speech-quality interview.wav --output report.txt   # written as UTF-8
speech-quality interview.wav --min-snr 20 --usable-score 70
speech-quality --help

Exit codes: 0 every recording is usable, 1 at least one is not, 2 nothing could be read. That makes it a filter:

for f in takes/*.wav; do speech-quality "$f" --quiet || echo "redo: $f"; done

License

MIT. Copyright (c) 2026 Pranay Mahendrakar.

Metadata

Release files for speech-quality 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for speech-quality 0.1.0
File Size Uploaded
speech_quality-0.1.0.tar.gz 52.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for speech-quality 0.1.0
File Interpreter ABI Platform
speech_quality-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 92.0 kB

Release files / speech_quality-0.1.0.tar.gz

Download URL speech_quality-0.1.0.tar.gz
Size 52.1 kB
Tags Source
SHA-256 checksum
How to use checksums
c5c37272c7d2558c17bd5e7cc9718960672d0356e2e74cb7c4231928e1f04970
BLAKE2b-256 checksum
How to use checksums
d9444b3a721c5ba703a46434534d904d407c9eb7a16b75d2faa07cfaf26f1ff2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / speech_quality-0.1.0-py3-none-any.whl

Download URL speech_quality-0.1.0-py3-none-any.whl
Size 39.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
da2bff99da425b4bf92a2cdd6b4b90dfa1f96fa610b9729da9a306090e9b65da
BLAKE2b-256 checksum
How to use checksums
37bb96bf09e82837a2dd42ca1f0ef7083f005a35770a8b1636f4025bb72df0d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page