Streaming and production ASR
This section is about streaming speech recognition, which turns speech into text while the person is still talking. It is also about the systems that surround every deployed recognizer. Live captions must show each word as it is spoken. A voice assistant must answer about as quickly as a person would. Both need a recognizer that writes each word a fraction of a second after it is said, not after the recording ends.
Every recognizer in this chapter so far reads the whole recording first. Its encoder, the network that turns spectrogram frames into a sequence of vectors, is usually a Conformer, a stack of blocks that mix self-attention with convolution (§36.1). Self-attention lets every frame read every other frame, including frames from later in the recording. Whisper reads a whole 30-second window this way before its decoder writes a single word (§36.4). A transducer, the model of §36.3 that reads one encoder frame at a time and decides whether to emit a label, could write as the audio arrives. But it can do so only if its encoder never reads future audio. Reading everything first is fine for an archive and useless for a conversation.
How fast is "as quickly as a person"? Stivers et al. (2009) timed the gap between yes-or-no questions and their answers in recordings of natural conversation in ten languages, from Danish and Japanese to Tzeltal and Yélî Dnye. In every language the most common gap was between 0 and 200 ms. People start planning a reply before the question ends. A system that waits for silence and only then starts to think is already late.
Here is the plan. First we pin down what latency means and add up a real budget. Then we build encoders that see only a bounded amount of future audio. We prove why letting every layer peek ahead is a trap while chunks are not, and we make chunk-by-chunk inference exact with caches. Then we make the decoder stream too, in three different ways.
The rest of the section covers what surrounds the recognizer in production. A voice activity detector decides when someone is speaking, and an endpointer decides when they have finished. Diarization labels who spoke when. On-device models run on the phone behind a wake-word detector. Servers transcribe thousands of streams at once, and sizing them is arithmetic you can do on a napkin. Each of these is a standard system-design interview topic.
What latency means
The word "latency" covers several different quantities. Mixing them up is the most common mistake in discussions of streaming recognition, so we name each one.
Start with a tiny example. Say the word "mom" ends at 1.00 s and appears on screen at 1.25 s. Part of that 250 ms is spent waiting for audio after the word, which the model needs before it can decide. Part is spent computing. And part is the model choosing to wait a little longer before it commits. Each part has a different cause and a different fix.
First, the units. Speech enters the recognizer as feature frames, one log-mel spectrum every 10 ms. The first layers of the encoder subsample them, merging several frames into one. So one encoder frame covers of audio, 40 ms after 4x subsampling and 80 ms after 8x (§36.1). In this section encoder frames are numbered from 0, as in code. Frame covers the audio interval , so frame 3 at 40 ms covers 120 to 160 ms.
Which of these can a faster GPU reduce? Only the computational latency, plus the compute that sits inside the finalization latency and the time to first token, such as a second pass. The attention mask fixes the algorithmic latency, training fixes the emission delay, and a policy fixes how long the endpointer waits.
Papers usually report latency per word. It is the time from a word's forced-aligned end to the moment the system outputs it, averaged over words. Products care about the slow cases too, so they also report the 50th and 90th percentiles. When an interviewer asks how you would cut latency, first say which of the five you are attacking.
One more piece of arithmetic finishes the budget. A chunked encoder cuts the audio into chunks of encoder frames, each seconds long. It processes a chunk only once all of it has arrived. A word can end anywhere inside its chunk, so it waits anywhere from 0 to for the chunk to fill. On average it waits .
The cell keeps the budget explicit. Change an assumption and see what moves.
The budget shows where the time goes. Chunking costs a few hundred milliseconds, but the largest single item is deciding that the user has finished. A recognizer cannot know that someone has stopped until some silence has passed. A pause inside a sentence ("call... mom") sounds the same as the end of one. That is why the spoken dialogue systems of §37.5 predict the end of a turn from what has been said, instead of waiting for silence. In a design interview about a voice product, start the latency discussion at the endpointer, not at the encoder.
Encoders that do not look ahead
An encoder can stream if each of its output frames depends on a bounded amount of future input. Two parts of a Conformer block read future frames, its convolution and its self-attention. Both must be constrained.
Causal convolutions
A one-dimensional convolution with a kernel of taps computes each output as a weighted sum of neighboring inputs. In symbols, over a window of offsets (receptive fields are in §0.8). The usual "same" padding centers the window. For the offsets are , so output 5 reads inputs 4, 5, and 6, and input 6 lies in the future. In general a centered window of odd width , with offsets , reads future frames.
A causal convolution pads zeros on the left only and uses the offsets . After renaming the taps it reads
so output reads inputs through . With , output 5 now reads inputs 3, 4, and 5. The parameters are exactly the same. Only the side of the padding changes, and it matters a great deal. A centered kernel of width 31, a size common in Conformer implementations, reads 15 future frames in every block. At 40 ms per frame that is 600 ms of audio.
Other parts of a standard recognizer use the future quietly.
- Feature normalization. Normalizing the features by the mean and variance of the whole utterance uses audio that has not arrived yet. Streaming models drop it or use running statistics.
- Batch normalization. The Conformer's convolution module normalizes with statistics computed over whole sequences during training (§0.7), so training sees numbers a streaming model never could. The cache-aware models below replace it with layer normalization, which normalizes each frame on its own.
- The subsampling stem. The strided convolutions at the bottom of the encoder read ahead too, so they are made causal as well (Noroozi et al. 2023).
How lookahead accumulates across layers
Self-attention has no kernel, so how far it looks ahead is whatever its attention mask allows. The obvious first idea is to let each frame attend to the whole past and to future frames, in every layer. We call this per-frame lookahead. One such layer waits for frames. A stack of layers waits for far more, because the frame a layer peeks at has itself peeked further ahead in the layer below.
To state this precisely we need one word. The reach of a layer is the last frame of the layer below that output frame reads.
∂ Proof by induction on depth
For the statement is the definition of . Suppose the output of layer at any frame depends on inputs up to . The output of layer at frame reads layer- frames . Each of those depends on inputs up to , because a composition of nondecreasing functions is nondecreasing. The frame is among those read, and it depends on input , so the bound is attained when connections carry signal.
For case 1, adding a total of times gives . For case 2, lies in chunk , so and every further composition changes nothing. For case 3, lies in chunk when . So , and each further layer adds one more chunk. ∎
In plain words, lookahead compounds through depth, the way a receptive field grows. With per-frame lookahead, layer 2 reads frame of layer 1, and that frame already read up to of the input. A chunk boundary is a wall that every layer respects, so the reach stops at the end of the chunk at any depth. But any leak through the wall lets the next chunk in at every layer. The leak can be a centered convolution, or lookahead frames that are ordinary members of the next chunk.
The same composition gives the reach of stacked sliding windows in §12.6, where windows of width see tokens into the past. In a streaming encoder, reach into the past is free, since that audio has already arrived, and costs only memory. Reach into the future costs latency, one for one.
The cell checks the theorem by brute force. It builds each rule as a boolean matrix over 320 encoder frames and composes it with itself layer by layer. Frame at layer reaches input if it reads some frame that reaches . The cell reports how much future audio output frame 0 depends on.
The per-frame rule climbs to 1,360 ms at 17 layers, as computed by hand. The chunked rule stays at 600 ms at every depth. The third column is the warning. One centered convolution per layer adds a whole chunk, 640 ms, per layer. After 17 layers a chunked encoder with centered convolutions, even of kernel 3, waits 10.9 seconds, far worse than per-frame lookahead.
This is why streaming Conformers use causal convolutions. The dynamic-chunk model of Zhang et al. (2020), described below, uses them precisely to keep its encoder independent of future frames. An interviewer who asks why a "chunked" model has seconds of latency is usually fishing for this leak.
The chunked attention mask
Streaming Conformers use case 2 of the theorem with one more setting, how many past chunks each frame may see. Here is the rule in plain words first. Each frame sees every frame of its own chunk, including the later ones, and a fixed number of whole chunks before it. It sees nothing after its chunk.
Here is the mask for , , and , with one row per attending frame. Every frame sees its whole chunk, including the future frames inside it, and the chunk before it. It sees nothing else.
0 1 2 3 4 5 6 7 8 9 10 11 0 1 1 1 1 . . . . . . . . 1 1 1 1 1 . . . . . . . . 2 1 1 1 1 . . . . . . . . 3 1 1 1 1 . . . . . . . . 4 1 1 1 1 1 1 1 1 . . . . 5 1 1 1 1 1 1 1 1 . . . . 6 1 1 1 1 1 1 1 1 . . . . 7 1 1 1 1 1 1 1 1 . . . . 8 . . . . 1 1 1 1 1 1 1 1 9 . . . . 1 1 1 1 1 1 1 1 10 . . . . 1 1 1 1 1 1 1 1 11 . . . . 1 1 1 1 1 1 1 1
The left context sets cost and memory, not latency. Each query sees at most keys, so attention costs the same per frame however long the stream runs. Each layer must keep the keys and values of the last frames. That is a chunk-by-chunk version of the rolling cache of §12.6.
The latency, in contrast, varies from frame to frame. In the drawing, frame 0 waits for frames 1 to 3 and frame 3 waits for nothing. So the algorithmic latency of a chunked encoder lies between 0 and .
The lookahead variant runs into case 3 of the theorem. Suppose the extra frames are ordinary frames of the next chunk, and every layer uses the same mask. Then each layer pulls in one more chunk of future. The fix is to give each chunk private copies of its lookahead frames. They are computed afresh for that chunk and never taken from the next chunk's outputs. The lookahead then stays frames at any depth. The widget's "recomputed lookahead" setting draws this case.
The widget draws, on top, the frames of every layer that the selected output frame depends on, down to the input, and below it one layer's mask. Start in "per-frame lookahead" with and predict how far ahead the selected frame reaches as you add layers from 1 to 12, or press Stack layers. Then switch to "chunks" with chunk size 4 and repeat. Last, compare "shared lookahead" with "recomputed lookahead", which both let a chunk see 2 frames past its end, at 8 layers, and decide which one is case 3 of the theorem.
A chunked streaming mask
Build the chunked mask of the definition and use it to check the theorem. Implement three functions.
chunk_mask(T, n_chunk, n_left, n_right=0)returns the boolean(T, T)mask, with True meaning "may attend". Frameisees its own chunk,n_leftwhole chunks before it, and then_rightframes just past the end of its own chunk.n_left=Nonemakes the whole past visible, andn_left=0allows the frame's own chunk only. The last chunk may be shorter thann_chunk.algorithmic_latency_ms(n_chunk, n_right, frame_ms, conv_lookahead_frames=0)returns the worst algorithmic latency of one chunked layer in milliseconds, measured from the end of the frame's own audio. A convolution stem that readsconv_lookahead_framesencoder frames ahead adds its lookahead on top.max_dependency(mask, n_layers)returns an integer array whose entryiis the last input frame that output frameidepends on aftern_layerslayers that all usemask, forn_layersat least 1.
Caches make streaming exact
A chunked mask defines what a streaming encoder computes. It does not say how to compute it as the audio arrives.
The wasteful way is buffered streaming. It runs a model on overlapping windows of audio and keeps only the output for the middle of each window. For example, each 1-second chunk is transcribed inside a 4-second buffer of past and future context. The context is recomputed every time. And if the model was trained on whole utterances, the context it sees at inference differs from what it saw in training.
Noroozi et al. (2023) showed how to do better with a cache-aware streaming encoder. The recipe has two halves.
- Train with the streaming context. Use the chunked mask, causal convolutions, and layer normalization, so that training already sees exactly the contexts that streaming will provide.
- Infer chunk by chunk with two caches per layer. Each causal depthwise convolution of kernel keeps the last inputs it saw. Each attention layer keeps the inputs of the last frames, from which it computes keys and values for the new chunk's queries. Caching the keys and values themselves, as in the KV cache of §6.5, is equivalent. Normalization, feed-forward, and pointwise layers need no cache, since they act on each frame separately.
Why is this exact? Each layer's output at a frame depends only on the inputs its mask allows, and the caches hold exactly those inputs. So chunk-by-chunk inference reproduces the whole-utterance computation with the same mask, number for number, and no work is repeated.
The PyTorch block builds a two-layer encoder of causal depthwise convolutions and chunked attention with two chunks of left context. It runs the encoder on a whole utterance with the mask, then chunk by chunk with caches, and compares the two.
The streamed output matches the offline one to float32 rounding. Dropping the caches, which is what processing chunks independently amounts to, changes the output at order one. Every chunk loses its left context, and its convolution sees zeros where the previous chunk should be.
A recurrent or state-space encoder (§21.4) is causal by construction and streams the same way. It carries a fixed-size state from chunk to chunk in place of the attention cache.
One model for many latencies
A chunk size fixed at training fixes the latency, and different products want different latencies. Captions can wait a second, while a voice command cannot. Zhang et al. (2020) proposed dynamic chunk training in their U2 model. For each training batch they use full attention with probability one half. Otherwise they draw a chunk size uniformly between 1 and frames, where is the longest utterance in the batch.
One model then works at any chunk size at inference, from fully streaming to offline, and the latency becomes a decoding setting. The model uses causal convolutions so that the encoder does not depend on future frames, for the reason the theorem gives.
Noroozi et al. (2023) do the same for cache-aware models, drawing a chunk size per batch. Trained on a large multi-domain set, their multi-latency transducer slightly beat single-latency models trained for each setting. On LibriSpeech test-other it scored 6.2, 5.5, and 5.2 percent against 6.4, 5.9, and 5.4 at 40, 240, and 520 ms. In practice, this means one trained model can serve every product's latency target.
Decoders that write before the audio ends
A streaming encoder is necessary but not sufficient. The decoder must also be able to emit words before the input ends.
Frame-synchronous decoders stream for free. A decoder is frame-synchronous when it makes its decisions one encoder frame at a time. CTC (§36.2) emits one symbol per encoder frame, possibly a blank that means "nothing new here". A transducer (§36.3) reads one encoder frame at a time and emits labels from it and from its own past labels. On top of a chunked encoder, both produce partial results chunk by chunk. Their remaining problem is emission delay, the habit of waiting for more context. FastEmit cut it by 150 to 300 ms on a Google Voice Search test set.
An attention decoder does not stream. Each label it writes cross-attends over all the encoder frames (§4.6), so the decoder cannot be sure of its first word until the input has ended. There are three families of fixes.
Two passes
The first fix keeps the attention decoder but uses it only at the end. Sainath et al. (2020) ran a streaming transducer as the first pass and showed its partial results while the user spoke. When the endpointer fired, they rescored the transducer's most likely hypotheses. The rescorer was an attention decoder that reads the whole encoder output, the Listen, Attend and Spell model of §36.4. A simple version rescores an n-best list, the first pass's few most likely hypotheses. Sainath et al. stored them instead as a lattice, a tree in which hypotheses that share a prefix share its branch, so the shared prefix is scored once.
Rescoring is not a search. It is a single forward pass that reads each hypothesis as given and scores it, the way a decoder is run during training (teacher forcing). With the lattice's branches batched together it took a median of 58 ms and a 90th percentile of 97 ms on a Pixel 4 CPU.
At the same latency, the system had 8 percent lower relative WER than a server-side conventional recognizer, with a model more than 400 times smaller. The U2 model of Zhang et al. (2020) is the same idea with CTC as the first pass, and it rescores an n-best list. The cost is a product decision rather than a technical one. The final transcript can differ from the partial one the user already saw, so the text changes at the end of each utterance.
Streaming an offline model by agreement
The second fix needs no retraining. Run the offline model again and again on a growing audio buffer, and show only what successive runs agree on. The rule for "agree" is called LocalAgreement. Whisper-Streaming (Macháček, Dabre and Bojar 2023) is the standard implementation, and it loops over three steps.
- Re-decode. Every time at least
MinChunkSizeseconds of new audio have arrived, it transcribes the whole buffer again. If the previous update took longer than that, the next one starts at once. Up to 200 committed words from audio already trimmed out of the buffer go in as Whisper's text prompt, which keeps the style consistent. - Commit. It compares the new transcript with the previous one and commits what they share beyond what is already committed.
- Trim. When the committed text contains the end of a sentence, it cuts the audio buffer there, which keeps the buffer at about 30 seconds, Whisper's window. If the buffer is still too long, it cuts at the end of the last committed segment instead. The released code defaults to cutting at Whisper's segment ends once the buffer passes 15 seconds.
That last sentence sets the latency floor of the method. A word can be committed only after it has appeared in two consecutive hypotheses.
The authors measured this latency without computation at 1.02, 1.91, and 3.73 seconds for chunks of 0.5, 1, and 2 seconds. That is about twice the chunk size, as they expected from two consecutive updates having to agree. It sits above the floor because real words fall short of the ideal. A word at the very end of the buffer is often transcribed wrongly or not at all, so it waits for a later pair of hypotheses to agree. Real computation added another 1.7 to 2.3 seconds, because every update re-encodes the buffer.
Compute is the second cost. Whisper's encoder always processes a full 30-second window (§36.4). With updates every second it runs about 30 encoder passes for every 30 seconds of audio, where offline transcription runs one, and the decoder rewrites the uncommitted text each time. Each stream also has its own buffer, so streams batch poorly. The delayed-streams authors below measured Whisper-Streaming's throughput on an H100 at 100 times lower than that of their natively streaming model (Zeghidour et al. 2025). If an interviewer asks how to stream Whisper, this method is the answer, and its two costs are the follow-up.
LocalAgreement
Implement local_agreement(hyps, n=2) from the definition. hyps is a list of hypotheses, each a list of words, produced one after another by re-decoding a growing buffer. Return one entry per hypothesis, the list of committed words after that hypothesis has been processed. Each entry must be its own list, not a reference to one list that keeps changing. Committed words are never removed or changed, even when a later hypothesis rewrites them.
Delayed streams
The third fix trains a model that streams by design. Line the transcript up with the audio before training, so that each word sits at the audio frame where it starts. Then shift the text later by a fixed delay, and train a decoder to write that shifted text while it reads the audio. At every moment the decoder has heard a fixed amount of audio beyond the word it is writing, and it never waits for more. This is delayed streams modeling (DSM, Zeghidour et al. 2025).
It differs from everything so far in how it treats alignment. CTC and transducers sum over all possible alignments, and attention decoders find their own. Delayed streams fix the alignment before training.
A forced aligner gives every word of the training transcripts a start time. For pretraining, the start times came from whisper-timestamped, a tool that adds word timestamps to Whisper's output. For fine-tuning on human transcripts, Whisper's timestamped transcript was matched to the human one word by word with dynamic time warping, a dynamic program that aligns two sequences. Audio and text then become two token streams at the same rate, one text slot per audio frame.
In plain words, the model writes the text of frame while it already hears frame . It always has frames of future audio, and it never waits for more. The latency is , an explicit number, plus the compute of one step.
Two more things follow from the construction. Since exactly, the WORD markers the model writes are word starts in the audio, shifted by . Shifting them back gives word timestamps to within one 80 ms frame. And because the loss covers the PAD and WORD steps, the model learns where words start, not only what they are.
Most text steps are PAD. Words last several frames and take only a token or two each. Moshi, the full-duplex dialogue model of §37.5 whose aligned text stream DSM adapts, found that about 65 percent of the text steps of English conversation are PAD (Défossez et al. 2024).
At inference the model is fed the audio as it arrives and writes the text behind it. There is no chunking, no re-decoding, and no search over alignments. Every stream advances one step per frame in lockstep, so many streams batch exactly like LLM decoding (§20.6).
The numbers are strong. DSM-ASR has a 2.6B-parameter decoder and a default delay of 2.5 seconds. It averaged 6.4 percent WER over the eight English short-form test sets of the Open ASR Leaderboard (§36.1). The offline models the paper compares with score 5.6 to 6.1 there, from Canary-Qwen-2.5B to Parakeet-TDT-0.6B-v2. It served 400 streams at once in real time on one H100.
A version trained with delays sampled per sequence from 0.25 to 4 seconds, and told the delay as an input, runs at any delay in that range. It lands within about 300 ms of the delay it is asked for. The same trick of shifting one stream against another generates speech instead of text in the codec language models of §37.4.
Why must the text lag at all? The authors give a two-line counterexample.
Speech is the same in a softer form. A word is often ambiguous until the sounds after it are heard, and the delay is how much of that future the model is allowed. An interviewer may ask you to choose . Measure WER against delay on your own traffic and take the shortest delay past which WER stops improving much. The curve depends on the language and the domain, so there is no universal answer.
Delayed text streams
Build the delayed target stream of the definition and invert it to recover timestamps. Implement two functions.
build_delayed_text_stream(word_starts_s, word_tokens, n_frames, frame_ms, delay_frames)returns the delayed target stream as a list ofn_framesstrings, with"WORD"and"PAD"as the special tokens.word_starts_sholds the sorted start times in seconds, andword_tokens[w]is the list of wordw's tokens. Follow the definition, with one rule for collisions. If a word's start frame is already taken by an earlier word, start it at the first free frame after that earlier word instead. Drop tokens that would land at or pastn_frames.recover_word_starts(stream, frame_ms, delay_frames)is the inverse. It returns one(start_ms, tokens)pair per WORD in the stream. Herestart_msis a float, andtokensis the list of tokens that follow the WORD up to the next WORD or PAD.
Delayed streams now run in production-grade open models. Voxtral Realtime (Mistral AI 2026) is a 4-billion-parameter delayed-streams recognizer for 13 languages. Its decoder writes a padding token while the acoustic evidence is incomplete. It writes a word-boundary token once a word is complete and the target delay has passed. On the FLEURS multilingual benchmark it came close to offline Whisper at a delay of 480 ms, 8.7 against 8.2 percent WER, and beat it at 960 ms with 7.7. At 2.4 seconds it came within one WER point of Mistral's own offline recognizer, 6.7 against 5.9.
The table puts the approaches side by side. It is the shape of a good answer to "how would you build streaming recognition".
| approach | where its latency comes from | extra compute | accuracy against offline | needs special training | batches well |
|---|---|---|---|---|---|
| chunked encoder with CTC or a transducer | chunk size and lookahead, plus emission delay | none, with caches | about 25 percent more errors at 1.4 s, far more near zero lookahead | yes, the streaming mask | yes |
| two-pass rescoring | first pass for partials, endpointer plus a short rescoring for finals | one teacher-forced pass per utterance | better than the first pass alone | yes, both decoders | yes |
| LocalAgreement over an offline model | update interval times about 2, plus the time to re-decode the buffer | the encoder re-runs at every update | a little worse than offline, from short buffers | no | poorly |
| delayed streams (DSM, Voxtral Realtime) | the chosen delay, an explicit number | none, one decoder step per frame | approaches offline at 1 to 2.5 s, competitive at 0.5 s | yes, aligned streams | yes, in lockstep |
| monotonic attention (MoChA) | how far the learned pointer has moved | small | close to offline in 2017 | yes | moderately |
Voice activity detection and endpointing
A deployed recognizer almost never sees raw audio. In front of it sits a voice activity detector (VAD), which decides for each short frame whether someone is speaking. It earns its place in three ways.
- It saves compute. Most of a phone's microphone input, and much of a meeting, is silence or noise.
- It cuts long recordings into segments that fit the model, which batch transcription needs.
- It guards encoder-decoder models against hallucination. §36.4 showed that Whisper's decoder, a language model conditioned on audio, writes fluent text when the audio carries no speech. So silence and music must never reach it. CTC and transducers have no free-running language model and are less exposed, but they still waste compute on silence.
The oldest VAD thresholds the energy of each frame, its loudness in decibels. It works in a quiet room and fails in noise, because a fan, music, or a door slam has as much energy as speech.
The cell builds a 30-second energy track of four utterances. It has dips between syllables, four 60 ms clicks, and a fan that switches on near the end. It compares a single threshold with hysteresis, which starts a speech region above one threshold and ends it only below a lower one. A thermostat works the same way, so that it does not flick on and off around one temperature.
The single threshold chatters. It flips on and off at the dips between syllables, giving 193 segments for 4 utterances, and it clips almost 3 seconds of speech. Hysteresis cuts the chatter to 18 segments and clips only 170 ms. Neither can tell the fan from a voice, and both report 3.5 to 3.7 seconds of false alarm, almost all of it the fan. Telling them apart is the job of a better frame classifier.
WebRTC's VAD, used in browsers, scores 10, 20, or 30 ms frames with Gaussian mixture models of speech and noise, and it has four aggressiveness modes. Neural VADs handle noise far better. Silero VAD, for example, is a model of about 2 MB. It outputs a speech probability for each 32 ms window at 16 kHz, in under a millisecond on one CPU thread. Every VAD, energy or neural, then needs the same post-processing to turn frame decisions into segments.
The order matters. Dropping short segments before bridging pauses deletes real words that a pause split off. The sweep after the exercise shows this when it drops short segments without bridging.
VAD with hysteresis
Implement vad_segments(probs, frame_ms, on, off, min_speech_ms, min_silence_ms, pad_ms). It follows the four steps of the definition exactly and returns a list of (start_ms, end_ms) pairs as floats, sorted by time.
- Frame
tcovers[t * frame_ms, (t + 1) * frame_ms), so a segment of framesstoe - 1spanss * frame_mstoe * frame_ms. - A pause is short when its length in milliseconds is less than
min_silence_ms. - A segment is kept when its length is at least
min_speech_ms.
With a post-processor like yours, the cell sweeps the parameters on the energy track above, adding one at a time.
Each parameter trades one kind of error for another.
- Minimum speech removes the clicks. Used alone, it also deletes fragments of real speech that pauses split off, and clipped speech doubles to 330 ms.
- Minimum silence, applied first, bridges those pauses. That leaves 5 segments, the 4 utterances and the fan.
- Padding recovers the soft onsets and endings that a threshold cuts, so clipped speech falls to 0, at the price of more false alarm.
- A minimum silence of a full second goes too far. Clicks in the pauses become stepping stones that join whole utterances, and the last utterance swallows the fan. That leaves 3 segments and 7.8 seconds of false alarm.
The thresholds trade the same way. In practice they are tuned on held-out audio from the deployment domain. The errors are measured as durations of missed speech and of false alarm. On frames, these are the miss rate (one minus recall) and the false positive rate of §0.11.
The detection cost function of the pyannote.metrics library weights them 0.75 for a miss and 0.25 for a false alarm. That reflects the usual asymmetry. Clipped speech loses words for good, while extra audio mostly costs compute.
Endpointing
The endpointer decides that the user has finished, so the microphone can close and the system can respond. The classical endpointer declares the end of the query once the VAD has seen speech followed by a fixed interval of silence. The interval is a direct trade-off. Too short, and the system cuts people off at a pause and loses the rest of the sentence. Too long, and every answer arrives late.
Sainath et al. (2020) measured the trade-off on Google's Voice Search. With a separate acoustic endpointer, their WER rose from 6.2 percent without endpointing to 7.4 percent. Its median endpoint latency was 450 ms, and its 90th percentile 860 ms.
They then trained the transducer itself to emit an end-of-query token </s>. During training, the log-probability of that token gets a penalty for coming before the true end and another for coming more than a grace period after it,
Here is the current frame, is the audio up to it, is the reference end frame, and are hyperparameters. In plain words, emitting the end token early is punished in proportion to how early it is. Emitting it late is punished only beyond a grace period of frames.
Combined with the acoustic endpointer, the trained end token reached 6.8 percent WER at median and 90th-percentile latencies of 430 and 790 ms. With the second pass it reached 6.1 percent at 430 and 780 ms. The server-side conventional system scored 6.6 percent at 460 and 870 ms. So the end token alone wins on latency, and with the second pass the system wins on both counts.
The reason is content. A model that has heard "call" can learn that requests rarely end there, which silence alone cannot tell it. Full-duplex dialogue models (§37.5) take the next step and decide when to speak from the content of the whole conversation.
Diarization, who spoke when
A meeting transcript is of little use without speaker labels. Diarization splits a recording by speaker. It answers "who spoke when" without knowing in advance who the speakers are or how many there are. Its output is a set of time segments, each with an anonymous label such as "speaker 2".
The labels are arbitrary. A system that calls Alice "speaker 2" and Bob "speaker 1" has done nothing wrong. So any scoring must first match the system's speakers, the hypothesis speakers, to the reference speakers of the human annotation.
In plain words, a missed second is reference speech the system left uncovered. A false-alarm second is a speaker the system invented. A confused second went to the wrong speaker. DER adds them up and divides by the total speaking time.
The Hungarian algorithm is the same matching step that pairs predicted and true objects in the DETR detector of §27.6. In code it is one call to scipy.optimize.linear_sum_assignment.
∂ Proof that only the matched overlap depends on the mapping
The terms , , and depend only on how many speakers are active in each frame, not on the mapping. Only does. Count it the other way round. Each mapped pair contributes one to in exactly the frames where both are active, so . Minimizing DER is therefore maximizing that sum, which is an assignment problem on the matrix .
For greedy, take . Greedy takes the 5 and is left with the 0, a total of 5, while the crossed mapping gets . ∎
So scoring a diarization takes three steps. Build the co-occurrence table, solve the assignment, then count the three kinds of error frame by frame. Here are all three on a recording small enough to do by hand.
The widget scores a longer recording the same way. It starts with a system that is close to the reference, scored with no collar and with overlap included. The reference has three overlaps (8 to 9 s, 20.5 to 21 s, and 24 to 25 s), and the system never outputs two speakers at once. Before touching anything, guess what DER becomes with a 0.25 s collar and with overlap skipped. Then press "Rename system speakers" and predict whether anything changes.
The same close system scores 12.2 percent with no collar and overlap scored. It scores 5.0 percent with a 0.25 s collar, 4.5 percent with overlap skipped instead, and 0.9 percent with both. Either convention alone cuts the number by more than half, which is why a DER without its scoring setup is not a number you can compare. Asking "with what collar, and is overlap scored?" is the right reply to any DER claim in an interview or a paper review. The "two speakers merged" preset shows the third kind of error. Its system finds only two speakers, so where C talks alone, C's speech goes to a system speaker matched to A and counts as confusion, 25.8 percent DER with neither rule.
Diarization error rate
Implement der(ref, hyp, frame_ms), with no collar and overlap scored.
refis a boolean array of shape(T, n_ref)whose columnrmarks the frames where reference speakerris active.hypis(T, n_hyp)for the system. Both may have overlap, andn_hypmay differ fromn_ref.- Find the optimal one-to-one mapping with
scipy.optimize.linear_sum_assignmenton the co-occurrence matrix, then compute the terms of the definition. - Return a dict with keys
"fa","miss","conf", and"total"(all in milliseconds, as floats),"der", and"mapping". The mapping is a dict from reference index to hypothesis index that includes only pairs with positive co-occurrence. - Raise
ValueErrorif the reference has no speech.
The cascade, and why it misses overlap
The classical system is a cascade of four steps.
- A VAD finds the speech.
- The speech is cut into short windows, a second or two long, and each window gets a speaker embedding. That is a vector from a network trained to tell voices apart, so that two windows of the same voice get nearby vectors. The common networks, x-vectors and ECAPA-TDNN, are in §35.6.
- The embeddings are clustered. Agglomerative clustering merges the closest clusters until their cosine distance passes a threshold. Spectral clustering instead estimates the number of speakers from the largest gap between consecutive eigenvalues of a matrix built from the pairwise similarities.
- A resegmentation pass, often a hidden Markov model over the cluster labels, refines the boundaries.
The cascade is modular and works with any number of speakers. But each window gets exactly one speaker, so overlapped speech is missed by construction. The two-speaker CALLHOME telephone calls that Fujita et al. (2019) tested on had an average overlap ratio of 13 percent. And no part of the cascade is trained to minimize DER.
End-to-end diarization and permutation-invariant training
End-to-end neural diarization (EEND, Fujita et al. 2019) treats diarization as frame-level multi-label classification. A self-attention encoder reads the features of the whole recording. It has output slots, one per possible speaker. For each frame and slot it outputs a logit , whose sigmoid is the probability that the slot's speaker is active. Overlap is no longer special, since two slots can be active at once.
The difficulty is the targets. The reference activities come with arbitrary speaker labels. (In this subsection is a speaker activity, not a transcript.) Nothing says which slot should be which speaker. Training slot 1 against "speaker 1" asks the network to predict a labeling that is a coin flip, and it learns to hedge.
The fix, borrowed from speech separation, is to score the network under whichever assignment of slots to speakers fits best. Write for a permutation, a one-to-one reordering of the speakers. Write for the binary cross-entropy computed from a logit.
In plain words, the loss asks only that some consistent assignment of slots to speakers fits. It finds the best one with the same matching algorithm that scores DER.
∂ Proof of invariance and of the decomposition
Relabeling the reference replaces by for a fixed permutation . The loss under for the relabeled targets equals the loss under for the original ones. As ranges over all permutations, so does , so the minimum is the same.
For the decomposition, swap the two sums, . That is the cost of the assignment that pairs slot with speaker . Training differentiates through the minimizing permutation, which gives the gradient of the minimum wherever the minimizer is unique. ∎
The cell shows what the minimum buys. Each of 80 recordings has the same two voices, which a per-frame logistic model can tell apart from 2-dimensional features. But the reference lists them in a random order in each recording, as real labels do. The fixed pair of voices keeps the toy linear. A real EEND model sees different speakers in every recording and must group frames by voice, using attention over the whole recording.
Trained against the labels in their given order, the model stalls at a loss of 0.41. Even scored under the best order for each recording it is at 0.38, because it learned to hedge between the two voices in each slot. With PIT it reaches 0.088. The model picks one consistent assignment, say voice one in slot one, and the loss stops punishing it for the labels' arbitrary order.
Permutation-invariant loss
Implement the PIT loss twice, once by brute force and once with a linear assignment.
pit_bce(logits, targets)takes two arrays of shape(T, S)with .logitsholds the model's andtargetsthe 0 or 1 reference activities. Return(loss, perm). Herelossis , the mean over all entries under the best permutation, found by trying every permutation.permis a tuple whose entrycis the reference column matched to output slotc, so thattargets[:, list(perm)]lines up withlogits. Compute BCE from logits in a numerically stable way.pit_bce_hungarian(logits, targets)returns the same thing using the pairwise cost matrix andscipy.optimize.linear_sum_assignment.
Two refinements are now standard. The first, the powerset formulation (Plaquet and Bredin 2023), replaces the independent sigmoids with one softmax over sets of speakers. Each class is a set of speakers who talk at once, such as "nobody", "speaker 2 alone", or "speakers 1 and 3 together".
With and there are classes, namely silence, each speaker alone, and each pair. Three things follow.
- No threshold. The argmax removes the detection threshold that multi-label EEND must tune.
- The permutation problem stays. Swapping two speakers swaps their singleton classes and every pair that contains them, which is why the matching is done on activity vectors.
- Windows must be short. Sixteen speakers with up to two at once would already need classes, most of them almost never seen in training. So the powerset model runs on short local windows with few speakers.
The second refinement, Sortformer (Park et al. 2024), makes the minimum unnecessary. It orders the targets by arrival time, so slot 1 is always whoever speaks first. The loss can then be plain binary cross-entropy against the sorted targets, which makes diarization easy to train alongside a speech-to-text model. Sorting alone did about as well as PIT, and the best reported model averages the two losses.
Production systems combine the two worlds. The pyannote.audio 3.x pipeline runs a powerset segmentation model on 10-second windows, with at most 3 speakers per window and at most 2 at once. It embeds each local speaker and clusters the embeddings across the whole recording. The end-to-end model handles overlap locally, and clustering handles any number of speakers globally.
Who said which word. Speaker-attributed recognition joins the transcript to the diarization. A common pipeline, of which WhisperX is a popular open-source example, takes word timestamps from CTC forced alignment (§36.2). Its code then gives each word the diarized speaker who overlaps it most. The combined error is measured by cpWER, defined for the CHiME-6 challenge on dinner-party recordings (Watanabe et al. 2020).
The numerator is a sum over matched pairs. So, exactly as for DER, the best mapping is a linear assignment on the matrix of pairwise edit distances. CHiME-6 had four speakers per session and tried all 24 permutations, and the MeetEval toolkit (von Neumann et al. 2023) uses the Hungarian algorithm.
In plain words, a word with the right text but the wrong speaker usually costs two errors, a deletion from its true speaker's stream and an insertion in the other. Diarization errors therefore show up as word errors. A system that swaps the names of all the speakers but gets every word right still scores 0.
On-device recognition
Running the recognizer on the phone, watch, or car removes the network from the latency budget. It works offline, and it keeps the audio on the device, a privacy property some products require. The constraints are memory, compute, and energy, with tight latency on top.
Memory. Weights dominate. A 600M-parameter model takes about 2.4 GB in fp32 (4 bytes per weight), 1.2 GB in bf16, and 0.6 GB with int8 quantization (§20.2), and int4 halves int8 again. He et al. (2018) quantized a 120M-parameter streaming transducer to 8 bits, cutting its size 4 times to about 120 MB. With symmetric quantization its 90th-percentile real-time factor fell by 64 percent to 0.51, twice as fast as real time on a Pixel phone. It was faster than the conventional on-device system it replaced, with more than 20 percent lower relative WER.
Compute. A matrix multiply costs about 2 FLOPs per parameter per input vector, one multiply and one add (§5.6). An encoder of parameters producing frames per second therefore needs about FLOPs per second of audio. A 100M-parameter encoder at 12.5 Hz needs GFLOP per second.
That is about 2.5 percent of a mobile accelerator that sustains operations per second. Kudlur et al. (2026) design for edge chips that sustain to operations per second. They note that many reach only tens to hundreds of billions in practice, because memory bandwidth and heat keep them below their peak. Lower frame rates help directly, which is one more reason for 8x subsampling.
Latency on device. A full-attention encoder must re-encode the whole utterance before the decoder can emit, so its time to first token grows with the utterance. Moonshine v2 uses a sliding-window encoder with no positional embeddings, whose cost per frame is constant. On a MacBook M3 its 34M, 123M, and 245M-parameter models respond in 50, 148, and 258 ms. Whisper large-v3 takes 11.3 seconds on the same machine (Kudlur et al. 2026).
Personalization. Contextual biasing boosts known phrases during decoding (§36.5). On device it can use the user's contact names and app names, which never leave the phone.
The wake word comes first
A phone or speaker that listens all day should not spend even the 2.5 GFLOP per second of the encoder above on audio that is almost never meant for it. In front of the VAD and the recognizer sits a keyword spotter. It is a tiny always-on model with one job, deciding whether the last second or so of audio contained the wake phrase ("Hey Siri", "OK Google").
A keyword spotter, its metrics, and its cascade are built in §35.6. Three facts from there matter here. First, the two errors have different units. False accepts are counted per hour of audio with no wake phrase, and false rejects are the fraction of real attempts that fail. Second, a detector is tuned to a fixed false-accept budget, such as Google's 0.1 per hour (one false wake in ten hours), and then judged by its false rejects. Third, it meets that budget cheaply as a cascade. A tiny first stage on a low-power processor fires on a loose threshold and wakes a larger checker, and a speaker verifier or a server-side recognizer can check again.
When the cascade accepts, the device hands the recognizer the recent audio it has been buffering, so the words said right after the wake phrase are not lost. From then on, the VAD and endpointer of the previous sections run the turn.
Every piece of the section now fits on one timeline. Follow one spoken command through it.
Serving at scale
Server-side recognition comes in two shapes, and each has its own arithmetic.
Batch transcription of stored audio (call centers, podcasts, meetings, media archives) maximizes throughput. A VAD cuts each recording into segments of up to about 30 seconds at pauses. Segments are sorted by length and batched, so little compute is wasted on padding. Long stretches without pauses are split into overlapping windows, and the transcripts are stitched together in the overlap by aligning their timestamps or their words. LLM decoders use the continuous batching of §20.6.
Throughput is measured as RTFx, audio duration divided by processing time (§36.1). At RTFx 3,390, one fully busy GPU transcribes an hour of audio in seconds.
The cell applies the same arithmetic to models from the Open ASR Leaderboard paper. Its RTFx values were measured on one A100-80GB GPU with batch 64 where memory allowed, with numbers as of 27 March 2026. The cell assumes $2 per GPU-hour and 60 percent utilization.
The fastest transducer and CTC models cost about a dollar per thousand hours and need 3 GPUs for the whole load. Whisper large-v3 costs more than $22 and needs 48. That factor of more than 20 in throughput is the decoder story of this chapter, in money. Frame-synchronous decoders emit in one pass, while autoregressive decoders generate token by token.
Streaming transcription is constrained by latency instead. Every stream sends a chunk every . The server must process all the chunks that arrived in one chunk period within that period, or it falls behind. Suppose one stream's chunk costs of GPU time on its own, and batching chunks from many streams raises throughput by a factor . Then one GPU sustains at most streams. With 320 ms chunks, 4 ms per chunk, and , that is 200 streams.
The fleet is sized for peak concurrency, not daily volume. Each stream also holds its caches in GPU memory between chunks, which is the streaming version of the KV-cache budget of §20.6. In a system-design interview, produce two numbers, streams per GPU from compute and streams per GPU from cache memory. The smaller one wins.
Sizing a fleet
Turn the serving arithmetic into code. Implement three functions.
gpus_for_batch(audio_hours_per_day, rtfx, utilization)returns the whole number of GPUs needed to keep up with a daily volume of batch audio.streams_per_gpu(chunk_ms, compute_ms_per_chunk_per_stream, batch_efficiency)returns the largest whole number of real-time streams one GPU can serve, as defined in the text.cost_per_audio_hour(gpu_dollars_per_hour, rtfx, utilization)returns the cost of one audio hour in dollars.
Monitoring in production. Offline benchmarks stop being informative the day a system ships, because traffic drifts to new devices, new accents, and new product names. Mature teams sample live traffic, have people transcribe it, and track WER with the text normalization and confidence intervals of §36.5. They break it down by device, locale, and domain. They also watch
- latency percentiles for partial and final results, separately,
- the VAD's missed speech, visible as users who had to repeat themselves,
- endpointer cut-offs,
- and, for encoder-decoder models, hallucination detectors such as the compression-ratio and log-probability checks of §36.4.
Read more
- Noroozi et al. (2023). Stateful Conformer with Cache-based Inference for Streaming Automatic Speech Recognition. Causal convolutions, chunk-aware lookahead, the activation caches, and the multi-latency training, with the 1,360 ms lookahead example and the comparison against buffered streaming.
- Zhang et al. (2020). Unified Streaming and Non-streaming Two-pass End-to-end Model for Speech Recognition, and Shi et al. (2020), Emformer: Efficient Memory Transformer Based Acoustic Model For Low Latency Streaming Speech Recognition. Dynamic chunk training with CTC-first-pass rescoring, and the memory bank with per-chunk right context. Chiu & Raffel (2017), Monotonic Chunkwise Attention, is the attention-based streaming method of the same period.
- Macháček, Dabre & Bojar (2023). Turning Whisper into Real-Time Transcription System. LocalAgreement over an offline model, the buffer management, and an honest latency evaluation with and without computation.
- Zeghidour et al. (2025). Streaming Sequence-to-Sequence Learning with Delayed Streams Modeling, with Mistral AI (2026), Voxtral Realtime, and Kudlur et al. (2026), Moonshine v2: Ergodic Streaming Encoder ASR for Latency-Critical Speech Applications. Natively streaming recognition at the 2026 frontier, on servers and on edge devices. Défossez et al. (2024), Moshi: a speech-text foundation model for real-time dialogue, introduced the aligned text stream that DSM builds on. The Qwen3-ASR report, Qwen Team (2026), Qwen3-ASR Technical Report, describes its dynamic attention windows.
- Sainath et al. (2020). A Streaming On-Device End-to-End Model Surpassing Server-Side Conventional Model Quality and Latency, and He et al. (2018), Streaming End-to-end Speech Recognition For Mobile Devices. Two-pass rescoring, the end-of-query endpointer with its penalties, quantization, and every latency measurement in this section's budget. For the wake word in front of them, Siri Team (2017), Hey Siri: An On-device DNN-powered Voice Trigger for Apple's Personal Assistant, and Alvarez & Park (2019), End-to-End Streaming Keyword Spotting.
- Fujita et al. (2019). End-to-End Neural Speaker Diarization with Self-attention, Plaquet & Bredin (2023), Powerset multi-class cross entropy loss for neural speaker diarization, and Park et al. (2024), Sortformer: A Novel Approach for Permutation-Resolved Speaker Supervision in Speech-to-Text Systems. The three ways end-to-end diarization handles the permutation problem. Watanabe et al. (2020), CHiME-6 Challenge:Tackling Multispeaker Speech Recognition for Unsegmented Recordings, defines cpWER, and von Neumann et al. (2023), MeetEval: A Toolkit for Computation of Word Error Rates for Meeting Transcription Systems, implements it.
- pyannote.metrics reference, pyannote.audio, and Silero VAD. The exact definitions of DER, collars, and detection cost, a production diarization pipeline, and a production VAD with the post-processing defaults quoted here.
- Stivers et al. (2009). Universals and cultural variation in turn-taking in conversation. The ten-language measurement of response gaps that sets the latency budget for spoken systems.