TAI BUI
Back to curriculum
BuildPython

Speech Recognition (ASR) — CTC, RNN-T, Attention

Speech recognition is audio classification at every timestep, glued together by a sequence model that knows English and silence. CTC, RNN-T, and attention are the three ways to do it. Pick one and understand why.

TIME: ~45 minutesPREREQUISITES: Phase 6 · 02 (Spectrograms & Mel), Phase 5 · 08 (CNNs & RNNs for Text), Phase 5 · 10 (Attention)

Learning Objectives

  • Explain why ASR needs a dedicated alignment mechanism that plain classification doesn't: audio frames don't map one-to-one onto output characters, and output length isn't known in advance.
  • Contrast CTC, RNN-T, and attention encoder-decoder on streaming ability, training cost, and offline quality, and name a real model that ships each one.
  • Implement greedy CTC decoding and compute Word Error Rate (WER) from scratch, and state the WER thresholds for unusable versus human-parity transcription.
  • Pick a 2026 ASR model, decoding strategy, and chunking scheme for a given deployment target: streaming, offline, edge, or domain-specific.
  • Identify and avoid the pitfalls that still ship in production ASR: ungated silence hallucination, language-ID drift, and Whisper's 30-second window.

The Problem

You have a 10-second, 16 kHz clip and you want a string out of it: "turn on the kitchen lights". The problem is structural, not perceptual. Audio frames don't line up one-to-one with characters: the word "okay" might take 200 ms or 1200 ms, silence punctuates the utterance, some phonemes just run longer than others, and you don't even know the output length in advance.

Three formulations solve this:

  1. CTC (Connectionist Temporal Classification). Emit per-frame token probabilities, including a special blank, then collapse repeats and blanks at decode time. Non-autoregressive and fast. Used by wav2vec 2.0, MMS.
  2. RNN-T (Recurrent Neural Network Transducer). A joint network predicts the next token from the encoder frame and the token history. Streamable. Used by Google's on-device ASR, NVIDIA Parakeet.
  3. Attention encoder-decoder. The encoder compresses audio into hidden states, and the decoder cross-attends to generate tokens autoregressively. Used by Whisper, SeamlessM4T.

In 2026, SOTA WER on LibriSpeech test-clean sits at 1.4% (Parakeet-TDT-1.1B, NVIDIA) and 1.58% (Whisper-Large-v3-turbo). Barely any daylight between them on accuracy — the gap that actually matters is deployment.

Warm-up0/2
1. A speaker says the word 'okay' quickly in one recording and slowly in another. If you slice both recordings into fixed 20-millisecond audio frames, would you expect the same number of frames to cover the word each time?Warm-up
Two rows of fixed-size 20 millisecond frame cells, one for a recording where 'okay' is said quickly and one where it is said slowly. The word starts at the first cell in both rows and the number of frames it covers is marked with a question mark.
Setup only: the word's end point is deliberately not drawn.
2. In a machine translation model, the input sentence and the output sentence often have different numbers of words, with no fixed position-to-position mapping between them. What general approach lets a sequence model handle that kind of length mismatch?Warm-up
An English input sentence of three words, 'I am hungry', above its French translation of two words, 'J'ai faim', with a question mark between them marking the unknown mapping of positions.
Typed illustrative example, not model output.
Check: The Problem0/2
1. Why can't ASR simply classify each audio frame as a character and read off the result?Explain
A row of audio frame cells f1 to f11 followed by a final frame f_T, above the target string 'turn on the kitchen lights' drawn one character per box. A question mark between them marks which frame belongs to which character.
The number of frames T is left unspecified.
2. Parakeet-TDT-1.1B scores 1.40% and Whisper-Large-v3-turbo scores 1.58% WER on LibriSpeech test-clean. What follows for choosing between them?Explain
Two horizontal bars on a scale from 0 to 2 percent: Parakeet-TDT-1.1B at 1.40% and Whisper-Large-v3-turbo at 1.58% word error rate on LibriSpeech test-clean.
Values from the stem.

The Concept

Three ASR formulations: CTC, RNN-T, attention-encoder-decoder

CTC intuition. Let the encoder output T frame-level distributions over V+1 tokens (V chars + blank). For a target string y of length U < T, any frame alignment that collapses to y counts, and CTC loss sums over all such alignments. Inference is simple: per-frame argmax, collapse repeats, remove blanks.

Advantages: non-autoregressive, streamable, zero lookahead. The drawback is the conditional independence assumption: each frame prediction is independent of the others, so there's no internal language model. You fix that with an external LM via beam search or shallow fusion.

RNN-T intuition. RNN-T adds a predictor network that embeds the token history and a joiner that combines predictor state with the encoder frame into a joint distribution over V+1 (the +1 is a null / no-emit). That explicitly models the conditional dependence CTC ignores, and it's still streamable, because each step conditions only on past frames and past tokens.

Advantages: streamable, with an internal LM built in. Drawback: training is more complex and memory-hungry (a 3D loss lattice). RNN-T loss kernels are their own whole library category for that reason alone.

Attention encoder-decoder. An encoder (6-32 transformer layers) runs over log-mel frames, and a decoder (6-32 transformer layers) cross-attends to the encoder outputs to generate tokens autoregressively. There's no alignment constraint: attention can look anywhere in the audio. That's what makes it non-streamable unless you restrict attention (chunked Whisper-Streaming, 2024).

Advantages: the highest quality on offline ASR, and it's easy to train with standard seq2seq tooling. Drawback: autoregressive latency is proportional to output length, so it can't stream without extra engineering.

WER: the one number

Word Error Rate = (S + D + I) / N, where S=substitutions, D=deletions, I=insertions, N=reference word count. It's the Levenshtein edit distance at the word level, and lower is better. A WER above 20% is generally unusable; below 5% counts as human-parity for read speech. 2026 numbers on standard benchmarks:

ModelLibriSpeech test-cleanLibriSpeech test-otherSize
Parakeet-TDT-1.1B1.40%2.78%1.1B params
Whisper-Large-v3-turbo1.58%3.03%809M
Canary-1B Flash1.48%2.87%1B
Seamless M4T v21.7%3.5%2.3B

All of these are encoder-decoder or RNN-T based. Pure CTC systems (wav2vec 2.0) sit around 1.8–2.1% on test-clean.

Check: The Concept0/12
1. What is RNN-T, and what do its predictor and joiner do?Define
A dashed block labelled RNN-T containing two boxes, Predictor and Joiner, with their internal wiring not shown. Two inputs enter it, an audio frame t and the tokens emitted so far, 't u r', and its next output is marked with a question mark.
Schematic of the lesson's component names only. No wiring inside the block is drawn.
2. What is CTC in speech recognition?Define
A grid of probabilities with 12 frame columns and four token rows c, a, t and an underscore token. In each column one row holds 0.46 and every other cell holds 0.02.
Synthetic frames produced by the lesson's main.py (build_frame_probs), not a trained model's output.
3. What is an attention encoder-decoder ASR model?Define
An audio clip goes into a box labelled Whisper-Large-v3-turbo, 809M parameters, and text comes out. The spoken content is the phrase 'turn on the kitchen lights'. A note names SeamlessM4T as another model of the same family.
Model names and sizes are from the lesson.
4. What does Word Error Rate (WER) measure?Define
A six-word reference 'hello world this is a test' above a six-word model output 'hello world this is the test', and the value 0.167 returned by the lesson's wer function for the pair.
Value captured by running the lesson's wer() on this pair (it is one of main.py's Step 5 examples).
5. What is the blank token in CTC?Define
A row of token ids from a 28-entry vocabulary where id 0 is the underscore symbol, ids 1 to 26 are letters and id 27 is space, followed by twelve frame-level argmax tokens: c c c _ a a a _ t t t _.
Token ids and the frame row come from the lesson's main.py (synthetic frames).
6. What is LM fusion in ASR decoding?Define
Three candidate transcripts of 'turn on the kitchen lights' kept by a beam search, each with a question mark for its score, and a separate box labelled language model beside them.
Typed illustration of a beam, not output from a real decoder.
7. For read speech, which WER ranges does the lesson give for unusable and for human-parity transcription?Define
A horizontal WER scale from 0 on the left. A left zone is labelled human-parity, a right zone is labelled unusable, and the two boundaries between zones are marked with question marks.
Zone widths are not to scale.
8. What is the key drawback of CTC's conditional independence assumption?Explain
Five audio frames x1 to x5 feed an Encoder box, which outputs five per-frame distributions p1 to p5 over V+1 tokens. A box to the right applies argmax per frame, collapses repeats and drops blanks.
Generic schematic of CTC inference as the lesson describes it.
9. Why is RNN-T harder to train than CTC, even though both are streamable?Explain
Two block diagrams. CTC: an Encoder followed by a softmax. RNN-T: an Encoder and a Predictor both feeding a Joiner. Under each diagram, the tensor fed to its loss is marked with a question mark.
Schematic using the lesson's component names.
10. Why can't an attention encoder-decoder (Whisper-style) stream by default?Explain
A microphone timeline whose left part is already recorded and the rest not yet spoken, with a marker labelled now. Below it, an Encoder box followed by a Decoder box leads to an output marked text with a question mark.
Schematic. No timings are implied.
11. How can RNN-T model dependence between tokens, which CTC ignores, and still stream?Explain
Five audio frame cells f1 to f5 with a marker after f5 and a note that more audio is arriving, the three tokens 't u r' emitted so far, and the next emission marked with a question mark.
Schematic of one moment in a streaming decode.
12. A wav2vec 2.0 CTC system hears the sounds correctly, but its transcripts contain ungrammatical word choices that a language model would never produce. Which property explains this, and what is the fix?Analyze
A clear recording goes into a wav2vec 2.0 CTC system and comes out as a transcript. The spoken sentence is 'they're going to the store' and the transcript reads 'their going too the store'.
Typed illustration, not captured from a model.

Build It

Step 1: greedy CTC decode

python
def ctc_greedy(frame_logits, blank=0, vocab=None):
    # frame_logits: list of per-frame probability vectors
    preds = [max(range(len(p)), key=lambda i: p[i]) for p in frame_logits]
    out = []
    prev = -1
    for p in preds:
        if p != prev and p != blank:
            out.append(p)
        prev = p
    return "".join(vocab[i] for i in out) if vocab else out

Two rules: collapse consecutive repeats, then drop blanks. Example: a a _ _ a b b _ c → a a b c.

Step 2: beam-search CTC

python
def ctc_beam(frame_logits, beam=8, blank=0):
    import math
    beams = [([], 0.0)]  # (tokens, log_prob)
    for p in frame_logits:
        log_p = [math.log(max(pi, 1e-10)) for pi in p]
        candidates = []
        for seq, lp in beams:
            for t, lpt in enumerate(log_p):
                new = seq[:] if t == blank else (seq + [t] if not seq or seq[-1] != t else seq)
                candidates.append((new, lp + lpt))
        candidates.sort(key=lambda x: -x[1])
        beams = candidates[:beam]
    return beams[0][0]

Production systems use prefix-tree beam search with LM fusion. What's above is the conceptual skeleton, not the real implementation.

Step 3: WER

python
def wer(ref, hyp):
    r, h = ref.split(), hyp.split()
    dp = [[0] * (len(h) + 1) for _ in range(len(r) + 1)]
    for i in range(len(r) + 1):
        dp[i][0] = i
    for j in range(len(h) + 1):
        dp[0][j] = j
    for i in range(1, len(r) + 1):
        for j in range(1, len(h) + 1):
            cost = 0 if r[i - 1] == h[j - 1] else 1
            dp[i][j] = min(
                dp[i - 1][j] + 1,
                dp[i][j - 1] + 1,
                dp[i - 1][j - 1] + cost,
            )
    return dp[len(r)][len(h)] / max(1, len(r))

Step 4: inference against Whisper

python
import whisper
model = whisper.load_model("large-v3-turbo")
result = model.transcribe("clip.wav")
print(result["text"])

That's the strongest general ASR available in 2026, in one line — it runs on a 24 GB GPU at ~20× realtime.

Step 5: streaming with Parakeet or wav2vec 2.0

python
from transformers import pipeline
asr = pipeline("automatic-speech-recognition", model="nvidia/parakeet-tdt-1.1b")
for chunk in streaming_audio():
    print(asr(chunk, return_timestamps=True))

Streaming ASR needs chunked encoder attention and carryover state, so use a library that already handles it: NeMo for Parakeet, or the transformers pipeline with chunk_length_s.

Check: Build It0/4
1. The frame argmax sequence 'a a _ _ a b b _ c' (where _ is blank) goes through ctc_greedy. What is the output?Apply
Nine frame boxes holding the argmax tokens a, a, underscore, underscore, a, b, b, underscore, c, followed by a question mark for the output of ctc_greedy.
Setup from the stem.
2. The per-frame argmax tokens are b, _, b, b, _, b, a, a (where _ is the blank). What does ctc_greedy return?Apply
Eight frame boxes holding the argmax tokens b, underscore, b, b, underscore, b, a, a, followed by a question mark for the output of ctc_greedy.
Setup from the stem.
3. Using wer(), the reference is 'turn on the kitchen lights' and the hypothesis is 'turn the kitchen light'. What does it return?Apply
A five-word reference 'turn on the kitchen lights' above a four-word hypothesis 'turn the kitchen light', and the return value of wer marked with a question mark.
Strings from the stem.
4. What does wer('a b c', '') return, given the dp initialization in the lesson's code?Apply
A reference of three words a, b, c and an empty hypothesis. The dp table has four rows and one column, with each cell marked with a question mark.
Setup from the stem.

Use It

The 2026 stack:

SituationPick
English, offline, max qualityWhisper-large-v3-turbo
Multilingual, robustSeamlessM4T v2
Streaming, low latencyParakeet-TDT-1.1B or Riva
Edge, mobile, <500 ms latencyWhisper-Tiny quantized or Moonshine (2024)
Long-formWhisper with VAD-based chunking (WhisperX)
Domain-specific (medical, legal)Fine-tune wav2vec 2.0 + domain LM fusion
Check: Use It0/5
1. For offline English where quality matters most, the lesson picks Whisper-large-v3-turbo. Which property of its formulation supports that choice?Explain
One row of a model-picking table: for English, offline, maximum quality, the pick is Whisper-large-v3-turbo. Its formulation is marked with a question mark, and its LibriSpeech test-clean WER is given as 1.58%.
Row and WER from the lesson.
2. A team fine-tunes an attention encoder-decoder for a voice assistant and still cannot reach sub-500 ms turn latency, even after quantizing. What was the wrong assumption?Analyze
Four project steps: chose an attention encoder-decoder, fine-tuned it, quantized it, measured turn latency. Below, a requirement of sub-500 ms turn latency and the result, target not met.
Steps and outcome from the stem.
3. You are building live captioning that must show words with low latency while someone is still speaking. Which pick fits?Design
A speaker who is mid-sentence feeds an ASR model whose choice is marked with a question mark, which feeds captions on screen. A note gives the requirement of low latency while the speaker talks.
Schematic of the stem's scenario.
4. A hospital wants transcripts full of drug names and clinical jargon that general models mishandle. Which approach fits?Design
A dictated sentence 'start metformin five hundred milligrams' and a general model's output 'start met for men five hundred milligrams'. The goal is accurate transcripts with drug names and jargon, and the approach is marked with a question mark.
Typed illustration, not captured from a model.
5. A mobile app needs on-device transcription under 500 ms, and a separate service transcribes hour-long lectures. Which pair of picks fits?Design
Two cards side by side. A mobile app needs on-device transcription under 500 ms. A lecture service transcribes hour-long recordings. Each card has its model pick marked with a question mark.
Requirements from the stem.

Pitfalls that still ship in 2026

  • No VAD. Running Whisper on silence produces hallucinations ("Thanks for watching!"). Gate it with VAD, always.
  • Character vs word vs subword WER. Report word-level WER after normalization: lowercase, punctuation stripped.
  • Language ID drift. Whisper's auto LID mis-routes noisy clips to Japanese or Welsh. Force language="en" whenever you already know the language.
  • Long clips without chunking. Whisper has a 30-second window. Use chunk_length_s=30, stride=5 for anything longer.
Check: Pitfalls that still ship in 20260/6
1. What does a VAD (voice activity detector) do in an ASR pipeline?Define
Ten side-by-side blocks numbered 1 to 10 whose bar heights show signal energy, tall for blocks 1 to 3, 7 and 8 and short for blocks 4 to 6, 9 and 10. Beneath each block is a question mark for the output of a voice activity detector.
Energy values are made up for the drawing.
2. Before you compute and report word-level WER, what preprocessing does the lesson say to apply?Define
A reference 'Turn on the Kitchen Lights.' with capital letters and a final period, and a hypothesis 'turn on the kitchen lights' in lowercase. Both texts pass through an unknown step marked with a question mark before wer is computed.
Typed example strings.
3. What is Whisper's audio window, and what setting does the lesson recommend for longer clips?Define
A long recording bar cut by dashed lines into pieces. The window length per pass and the stride between neighbouring windows are each marked with a question mark.
Schematic. Spacing is not to scale.
4. What can happen when you rely on Whisper's automatic language ID on noisy audio, and what is the fix?Explain
A noisy clip goes into Whisper with no language specified, and the language token it picks is marked with a question mark.
Schematic.
5. A Whisper pipeline prints 'Thanks for watching!' during long silent gaps in a recording. Which missing step is the cause?Analyze
A timeline of a recording with speech stretches in blue and two silent stretches drawn dashed. The text 'Thanks for watching!' is printed once for each silent stretch, with dotted lines linking them.
Schematic layout based on the stem, not a real recording.
6. Two engineers score the same model on the same audio. One reports 14% WER and the other 6%, and the transcripts differ only in capitalization and punctuation. What is the likely cause?Analyze
A single box for the same audio and model feeds two engineers. Each has an unknown scoring recipe marked with a question mark, giving a WER of 14 percent for Engineer 1 and 6 percent for Engineer 2.
Numbers from the stem.

Ship It

Save it as outputs/skill-asr-picker.md: pick model, decoding strategy, chunking, and LM fusion for whatever deployment target you're building for.

Author's Note

The mistake I see most often here is picking a formulation for the wrong reason. "Whisper is what everyone talks about, so we'll use Whisper" is not an engineering decision, and it falls apart the moment the product needs sub-500ms turn latency that an autoregressive decoder can't hit no matter how you quantize it. Work backward from your latency and streaming budget first. That alone usually narrows CTC, RNN-T, and attention down to one real option before WER ever enters the conversation. I've watched a team burn weeks fine-tuning an attention encoder-decoder for a voice assistant that needed RNN-T from day one, because nobody asked the deployment question before the accuracy question.

Exercises

  1. Easy. Run code/main.py. It greedily decodes a hand-crafted CTC output and computes WER against a reference.
  2. Medium. Implement the prefix-tree beam search in Step 2 properly (account for the blank merge rule). Compare with greedy on a 10-example synthetic dataset.
  3. Hard. Use whisper-large-v3-turbo on LibriSpeech test-clean. Compute WER on the first 100 utterances. Compare with published numbers.

Key Terms

TermWhat people sayWhat it actually means
CTCThe blank-token lossMarginal over all frame-to-token alignments; non-AR.
RNN-TThe streaming lossCTC + next-token predictor; handles word-order.
Attention enc-decWhisper-styleEncoder + cross-attending decoder; best offline quality.
WERThe number you report(S+D+I)/N at word level.
BlankThe emptinessSpecial token in CTC signalling "no emission this frame".
LM fusionExternal language modelAdd weighted LM log-probs during beam search.
VADThe silence gateVoice activity detector; trims non-speech.

Further Reading

Code

Ships With