Update · May 2026. Hakim Fast v1.3 is now part of the renamed Hakim TTS family as Hakim Saree' v1.3 (سريع · swift). Same model, same
hakim-fast-v1API id, same latency profile this post documents. The new family also adds Hakim Faseeh v2 (premium quality + non-verbal tag support) and Hakim Mulhim v3 (premium + voice generation from a free-form prompt). See Introducing the Hakim TTS family for the announcement.
When we started work on Hakim Fast v1.3, the baseline we inherited from v1.2 was already competitive: ~180 ms p95 end-to-end TTS in the Frankfurt region, first-audio-byte out in under 250 ms, 24 kHz output. That would be a perfectly acceptable launch number at most shops. The problem is most shops don't put synthesized voices inside conversational agents where human-perceived latency has a sharp cliff around 150 ms, and we do.
This is the latency-budget post: every millisecond we shaved, where we shaved it, and the parts we tried that didn't work.
The budget
End-to-end TTS p95, measured from the API gateway accepting a POST /v1/synthesize to the last audio byte arriving at the client socket, breaks down roughly like this:
| Stage | v1.2 p95 (ms) | v1.3 p95 (ms) | Delta |
|---|---|---|---|
| Request auth + routing | 8 | 6 | -2 |
| Token normalization + G2P | 22 | 12 | -10 |
| Diacritic recovery | 18 | 14 | -4 |
| Acoustic model (autoregressive) | 104 | 58 | -46 |
| Vocoder | 26 | 18 | -8 |
| Mux + network egress | 5 | 5 | 0 |
| Total | 183 | 113 | -70 |
Three moves got us most of the delta.
Move 1. Replacing the autoregressive decoder
The v1.2 acoustic model was a standard autoregressive transformer, emitting one mel-frame at a time. It's the highest-quality architecture we had, and it was also the single largest line item in the budget. In v1.3 we replaced it with a non-autoregressive duration-predictor + parallel-decode design, closer to FastSpeech 2 in spirit but with a much larger prosody adapter and a flow-matching head for fine-grained spectral detail.
The tradeoff is subtle: non-autoregressive models tend to sound "flatter" because every frame decodes in parallel, losing the natural inter-frame correlation an autoregressive model captures. We offset that by conditioning the decoder on a separately-trained prosody token stream, the model gets a coarse rhythm + pitch plan before it decodes any audio, which restores the expressiveness we'd otherwise lose.
Net: 46 ms off the acoustic model, no measurable MOS regression.
Move 2. Batched G2P with cached normalization
The grapheme-to-phoneme pipeline in v1.2 ran per-sentence, single-threaded, with a cold dictionary lookup for every token. In v1.3 we:
- Batched G2P across a prefetched sentence buffer so the request stays on a hot cache line.
- Moved the Arabic normalizer's common-case path (already-diacritized MSA) to a dedicated fast-path that skips the restoration pass entirely.
- Replaced the dictionary lookup with a perfect-hash table of the 50k most-used tokens, precomputed at build time.
Net: 10 ms off G2P + 4 ms off diacritic recovery.
Move 3. Co-located vocoder on the decoder GPU
Previously the vocoder ran on a separate inference pool, which meant we paid for a gRPC round-trip between the acoustic model and the vocoder, small on paper, not small on p95. In v1.3 we co-locate the vocoder on the same GPU as the acoustic model, using a streaming-pipelined dataflow so the vocoder starts consuming mel frames as soon as the first ones are ready.
Net: 8 ms off the vocoder stage, and a corresponding reduction in jitter at the long tail (p99 moved from 340 ms to 198 ms).
What didn't work
A few things we tried and reverted:
- Int8 quantization on the acoustic decoder. Saved ~10 ms but produced a noticeable MOS regression on dialect audio. Kept the encoder at fp16 and the decoder at bf16; the extra ms isn't worth the quality cost.
- Skipping the prosody adapter on MSA. The assumption was that MSA has "flatter" prosody so the adapter could be skipped. It couldn't, removing the adapter flattened output in exactly the place newsroom-register listeners notice.
- Kernel-fused attention for the duration predictor. A 2 ms win in isolation, but the kernel didn't play well with our dynamic batcher at p95, tail latency got worse under load.
Four regions, same number
The 113 ms is not a Frankfurt-only number. The production topology runs identical stacks in Frankfurt (eu-central-1 analogue), Dubai (UAE), Riyadh (KSA), and Doha (Qatar). Each region has its own model replicas and its own object store; nothing round-trips across regions for a synthesis call. The p95 we publish is the 95th percentile across all four regions, measured from synthetic probes running inside each region's user network.
If you want to verify the number in your own network, the /api/public/latency endpoint returns a rolling 15-second window of live measurements per region. It's the same data that drives the ticker on the landing page.