Intermediate17 min read

Evaluate local speech recognition in two languages

Compare English and Spanish transcripts against reference words and score whether important details survived.

On this page

Make two reference recordings

Record “The library opens at nine on Tuesday” in English and “La biblioteca abre a las nueve el martes” in Spanish. Save the files as english.wav and spanish.wav. Write down the words you actually said. If they differ from these examples, use your own words as the reference.

Use the Faster Whisper environment from the transcription lab. Save the current copies of both scripts. Cache the English-only tiny.en and the multilingual tiny models before evaluating. The evaluator then loads them locally.

Download evaluate_speech.py
View evaluate_speech.py
import argparse
from pathlib import Path
import re
from time import perf_counter
import unicodedata

from faster_whisper import WhisperModel
from transcribe_notes import transcribe


def words(text):
    return re.findall(r"[^\W_]+", unicodedata.normalize("NFC", text).casefold())


def edit_distance(expected, observed):
    previous = list(range(len(observed) + 1))
    for row, target in enumerate(expected, start=1):
        current = [row]
        for column, guess in enumerate(observed, start=1):
            current.append(min(
                previous[column] + 1,
                current[column - 1] + 1,
                previous[column - 1] + (target != guess),
            ))
        previous = current
    return previous[-1]


def evaluate(audio, model_name, language, reference, facts):
    model = WhisperModel(model_name, device="cpu", compute_type="int8", local_files_only=True)
    started = perf_counter()
    lines, detected = transcribe(model, audio, language, vad_filter=True)
    elapsed = perf_counter() - started
    text = " ".join(line for _, line in lines)
    observed = words(text)
    edits = edit_distance(reference, observed)
    normalized = " " + " ".join(observed) + " "
    matches = [label for label, tokens in facts if " " + " ".join(tokens) + " " in normalized]
    print(f"Model: {model_name} | language used: {detected} | transcribe: {elapsed:.2f}s")
    print(f"Transcript: {text or '(no speech)'}")
    print(f"Word error rate: {edits}/{len(reference)} = {edits / len(reference):.1%}")
    if facts:
        print("Key phrase:", " / ".join(matches) if matches else "missing")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Check a local transcript against reference words.")
    parser.add_argument("audio", type=Path)
    parser.add_argument("--reference", required=True)
    parser.add_argument("--model", choices=("tiny.en", "tiny"), default="tiny")
    parser.add_argument("--language", choices=("en", "es", "auto"), default="auto")
    parser.add_argument("--fact", action="append", default=[], help="Repeat for alternate spellings of one fact")
    args = parser.parse_args()
    reference = words(args.reference)
    facts = [(label, words(label)) for label in args.fact]
    if not args.audio.is_file() or not reference or any(not tokens for _, tokens in facts):
        parser.error("Supply a recording, a nonempty reference, and any key phrases.")
    if args.model == "tiny.en" and args.language == "es":
        parser.error("Use the multilingual tiny model for Spanish.")
    language = None if args.language == "auto" else args.language
    try:
        evaluate(args.audio, args.model, language, reference, facts)
    except (OSError, RuntimeError, ValueError) as error:
        raise SystemExit(str(error)) from None
Download transcribe_notes.py
View transcribe_notes.py
import argparse
from pathlib import Path

from faster_whisper import WhisperModel


def timestamp(seconds):
    minutes, seconds = divmod(int(seconds), 60)
    hours, minutes = divmod(minutes, 60)
    return f"{hours:02d}:{minutes:02d}:{seconds:02d}"


def transcribe(model, audio, language="en", vad_filter=False):
    segments, info = model.transcribe(str(audio), language=language, beam_size=3, vad_filter=vad_filter)
    lines = [(segment.start, segment.text.strip()) for segment in segments if segment.text.strip()]
    return lines, info.language


def main():
    parser = argparse.ArgumentParser(description="Transcribe an English recording locally.")
    parser.add_argument("audio", type=Path)
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    if not args.audio.is_file():
        parser.error(f"Audio file not found: {args.audio}")
    output = args.output or args.audio.with_name(args.audio.stem + ".transcript.md")
    if output.exists():
        parser.error(f"{output} already exists. Choose a new path with --output.")

    model = WhisperModel("tiny.en", device="cpu", compute_type="int8")
    lines, _ = transcribe(model, args.audio)
    if not lines:
        raise SystemExit("No speech was transcribed. Try a clear recording with one speaker.")
    paragraphs = [f"[{timestamp(start)}] {text}" for start, text in lines]
    try:
        with output.open("x", encoding="utf-8") as transcript:
            transcript.write("\n\n".join(paragraphs) + "\n")
    except OSError as error:
        raise SystemExit(f"Could not save {output}: {error}") from None
    print(f"Wrote {output}")


if __name__ == "__main__":
    main()
python -c 'from faster_whisper import WhisperModel; WhisperModel("tiny.en", device="cpu", compute_type="int8"); WhisperModel("tiny", device="cpu", compute_type="int8")'

Score words and key details

python evaluate_speech.py english.wav --model tiny.en --language en --reference "The library opens at nine on Tuesday." --fact nine --fact 9
python evaluate_speech.py english.wav --model tiny --language en --reference "The library opens at nine on Tuesday." --fact nine --fact 9
python evaluate_speech.py spanish.wav --model tiny --language es --reference "La biblioteca abre a las nueve el martes." --fact nueve --fact 9

Word error rate divides the minimum number of word substitutions, deletions, and insertions by the number of reference words. This script ignores case and punctuation and preserves accents. It counts nine and 9 as different words. The key phrase check accepts either spelling of the same detail.

You’re ready when

You can point to each difference in the printed transcript and say whether the opening time survived. Check the whole sentence too. A key phrase can be present in a sentence with the wrong meaning.

Compare language detection

Repeat the Spanish command with --language auto. Compare the selected language, the transcript, and the key phrase. In one run on synthetic recordings, both English transcripts rendered “nine” as “9,” which preserved the time. The Spanish transcript lost the opening time even when Spanish was selected explicitly.

Use multiple speakers and recordings before drawing conclusions about a language. Add examples with background noise and short utterances. If you switch to a language that needs a different word segmenter, define and verify the scoring unit first.

Check your understanding

Why can two transcripts with the same word error rate have different usefulness?

One may miss a number or name that the listener needs. The other may differ only in wording that preserves the meaning.

Sources

Search the guide