# speech.cpp: six speech model families in one ggml binary, built and heard on a shared CPU

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.thesatyajit.com/articles/speech-cpp
> date: 2026-10-17
> tags: tts, asr, speech, audio, gguf, on-device, inference, realtime

Satyajit's note on this one was short: there is a speech.cpp, see if you can run it, and if you can, put samples in the article. So this piece is mostly listening and timing. I built it, made speech with it, transcribed that speech back with it, served it over its OpenAI-style API, and read the code that does the parts I could not run.

The post that started it is Japanese. In full, translated: "Released speech.cpp v0.8.8 🚀 It now supports Fish Audio's drama-3, and it is faster on M5 Macs! It needs neither Python nor CUDA, so you can install it and use it straight away. Please give it a try! Rough requirements for drama-3: on a Mac, 32 GB of memory or more; on Windows or Linux, 8 GB of VRAM or more plus 32 GB of RAM." That is [@gyakuse on X](https://x.com/gyakuse/status/2110944883121008900), who publishes as sakasegawa on Hugging Face. The [repository](https://github.com/nyosegawa/speech.cpp) is MIT. By the time I cloned it a day later it was at v0.8.9, commit `32cbe28`, and that is what everything below ran on.

<RepoCard repo="nyosegawa/speech.cpp" />

If the card says the repository has no tests, that is its heuristic looking for a `test` folder. The tests are in `checks/`, and they are a large part of this piece.

## What I ran, and on what

The machine is a shared cloud VM: four cores of an Intel Xeon at 2.1 GHz with AVX-512 and AMX, 15 GB of RAM, no GPU, and five other jobs running at the same time. I built from source with CMake in Release mode for the native CPU, two compile jobs, which took about three and a half minutes (measured). Every model run used `--threads 2` under `nice -n 19`. So every speed number here is a floor for this CPU, not its best.

I fetched five model files, 3.58 GB in all (measured): `qwen3-tts-0.6b` at its default Q8_0 (1.21 GB), `irodori-tts-mf` at Q4_K (0.86 GB, to stay within my download budget), `qwen3-asr-0.6b` at Q8_0 (0.84 GB), `parakeet-tdt-0.6b-v3` at Q8_0 (0.67 GB, not the F16 default) and `silero-vad` (1.2 MB). I did not fetch `qwen3-tts-1.7b`: even its Q4_K is 1.33 GB, which would have taken the total past 4 GB. I did not fetch Drama 3; its section explains why it cannot run here.

<ModelCard repo="sakasegawa/Qwen3-TTS-12Hz-0.6B-CustomVoice-GGUF" />

<ModelCard repo="sakasegawa/Irodori-TTS-v4.1-Small-MF-GGUF" />

<ModelCard repo="sakasegawa/Qwen3-ASR-0.6B-GGUF" />

<ModelCard repo="sakasegawa/parakeet-tdt-0.6b-v3-GGUF" />

| Finding | Number | Label |
|---|---|---|
| Qwen3-TTS 0.6B, time to first audio after load | 0.669 to 0.989 s over nine takes | measured |
| Qwen3-TTS 0.6B, synthesis speed | RTF 3.73 to 3.96, about 0.30 s per 0.08 s frame | measured |
| Same model on an Apple M5 | first audio 0.029 s, RTFx 3.7 | reported |
| Irodori-TTS MF, first audio | 4.01 to 5.09 s, RTF 4.38 to 4.54 | measured |
| Peak memory per TTS run | 1,288 MB for Qwen3-TTS, 1,090 MB for Irodori | measured |
| Qwen3-ASR 0.6B, 51.52 s of audio | 31.04 s, RTFx 1.7 | measured |
| Parakeet TDT 0.6B v3, 35.92 s of audio | 7.89 s, RTFx 4.6 | measured |
| Silero VAD | 7.60 s of audio in 0.034 s | measured |
| Round-trip error, eleven takes | 0% after spelling normalization, except one take at 35.7% WER | measured |
| Drama 3 Q4_K, routed experts | 18.12 GB of 20.78 GB of weights | measured from the header |

## Listen first

These are the samples, eight of the eleven takes I made. Each lists its input, the model and voice, the time speech.cpp reported for it on this CPU, and what its own recognizers wrote when given the WAV. All were generated with seed 1. Pick one at a time; starting another pauses the first.

<SamplePlayer />

The same eight, for readers without the player. RTF is synthesis time divided by audio length, so above 1 is slower than real time. Errors are mine, computed against the input after lower-casing, removing punctuation and treating "eighty" and "80" as the same word.

| # | Model, voice | Input | Audio | Made in | RTF | Qwen3-ASR 0.6B heard | Error | File |
|---|---|---|---|---|---|---|---|---|
| 1 | Qwen3-TTS 0.6B, ryan | One program reads the text, runs the talker, and streams the waveform while the rest is still being made. | 7.60 s | 28.80 s | 3.79 | the same | WER 0% | [webm](/articles/speech-cpp/sample-1-ryan.webm) |
| 2 | Qwen3-TTS 0.6B, aiden | Each frame is eighty milliseconds of sound, sixteen codes deep, and it is decoded the moment it exists. | 7.44 s | 29.14 s | 3.92 | "80 milliseconds", "16 codes" | WER 0% | [webm](/articles/speech-cpp/sample-2-aiden.webm) |
| 3 | Qwen3-TTS 0.6B, serena | Every stage was compared with the official implementation, on the same inputs, before it shipped. | 7.12 s | 26.86 s | 3.77 | the same | WER 0% | [webm](/articles/speech-cpp/sample-3-serena.webm) |
| 4 | Qwen3-TTS 0.6B, ryan | Version 0.8.8 shipped on October 16, and the Drama 3 file weighs 20.8 GB. | 9.92 s | 39.17 s | 3.95 | "October 16th", "20.8 gigabytes" | WER 0% | [webm](/articles/speech-cpp/sample-4-ryan-numbers.webm) |
| 5 | Qwen3-TTS 0.6B, ryan | Run speech.cpp, pull the GGUF file, and pick Q4_K when memory is tight. | 9.60 s | 35.77 s | 3.73 | "Run SpeechBox PP. Pull the G GUF file and pick Q4K…" | WER 35.7% | [webm](/articles/speech-cpp/sample-5-ryan-speechcpp.webm) |
| 6 | Qwen3-TTS 0.6B, ono_anna | 今日は四つのCPUコアだけで、日本語の音声を合成しています。 ("Today I am synthesizing Japanese speech on just four CPU cores.") | 5.44 s | 21.56 s | 3.96 | the same | CER 0% | [webm](/articles/speech-cpp/sample-6-ono-anna.webm) |
| 7 | Irodori-TTS MF Q4_K, voice `none` with instructions | 明日の東京は晴れですが、夕方から少し風が強くなるでしょう。 ("Tomorrow Tokyo will be sunny, but the wind will pick up a little from the evening.") | 6.16 s | 27.97 s | 4.54 | the same, punctuation included | CER 0% | [webm](/articles/speech-cpp/sample-7-irodori-design.webm) |
| 8 | Irodori-TTS MF Q4_K, take 6 as reference | 小さなパソコンでも、声はちゃんと作れます。 ("Even a small computer can make a proper voice.") | 4.00 s | 17.52 s | 4.38 | the same | CER 0% | [webm](/articles/speech-cpp/sample-8-irodori-clone.webm) |

All eight were generated on a shared 4-core CPU VM by us, 17 Oct 2026 (measured). Parakeet TDT 0.6B v3, which reads European languages only, heard the English takes the same way, except that it wrote take 5 as "Run SpeechBoss BP" and kept "GB" in take 4.

**What the round trip shows.** Ten of eleven takes come back word for word once spelling is set aside, in English and Japanese; the three not in the player are the README's example sentence, a tongue twister and take 6's sentence in the English voice `ryan`. Take 4 read a version string, a date and a unit written as digits, and both recognizers wrote back what it meant. Take 5 is the flaw. Spelling `GGUF` as "G GUF" is the recognizers' business, but both independently hear `speech.cpp` as something like "SpeechBox PP". Two recognizers making the same mistake on clean synthetic audio points at the TTS reading `.cpp` badly. If a voice has to say the name, write it as words.

Two cautions. A recognizer reading clean synthetic speech is a lenient judge, and Qwen3-ASR is from the same family as the TTS, so 0% here means "intelligible", not "natural". And the takes are not loudness-matched: ono_anna's take sits at about −29 dBFS RMS and aiden's at −18 (measured), so turn up take 6.

## One C API, one file per model

**A model is one GGUF file with its codec inside.** The Qwen3-TTS file holds the talker, the code predictor, the tokenizer and the 12 Hz codec decoder. The Irodori file holds its diffusion transformer, its text encoder and its 48 kHz codec. A model is named from a catalog pinned in each release, fetched with the system's `curl` into a cache folder (`SPEECH_MODEL_DIR` moves it; I pointed it at my scratch space) and checked against the catalog's SHA-256 before use. The library itself never touches the network.

**Each family is a port on ggml.** The families live in `src/families/`: Qwen3-TTS, Irodori-TTS, Drama 3, Qwen3-ASR, FastConformer (one port that runs ReazonSpeech and both Parakeet models) and Silero VAD. Together they are 13,562 lines of C++ and generated tables (measured). An engine per family adapts each to the common request.

**Everything speaks through `speech.h`.** It is plain C: opaque handles, UTF-8 strings, statuses with a category. A model does one task: synthesis, recognition or detection. Options are one shared vocabulary of names (`voice`, `seed`, `top_k`, `instructions`), and each family declares which it takes, with defaults and ranges read from the model file; that one table drives the setters, `speech info` and the server's `/v1/models`. A neutral value (speed 1, language `auto`) passes on any model, and any other value of an option a model does not take is refused by name, never silently ignored.

**The front ends are thin.** The `speech` command, `speech worker` (JSON Lines on stdin and stdout, for programs such as the author's ASIST) and `speech serve` (OpenAI's audio API on `cpp-httplib`) all call the same C API that a third-party binding would.

<Figure
  src="https://media.thesatyajit.com/ai/articles/speech-cpp/fig3.png?v=c7f041e6ae33b499"
  alt="The speech.cpp page on localhost:8080, Speak tab. Model qwen3-tts-0.6b, 1.21 GB. Text: Hello. It will be sunny in Tokyo tomorrow, with a high of twenty-four degrees. Voice ryan, language default (auto), a drawn seed. A Speak button and a player with a waveform at 0:03 of 0:09, Save WAVE and Transcribe this buttons, and the line: Qwen3-TTS-12Hz-0.6B-CustomVoice, first audio after 0.12 s, made in 2.87 s."
  caption="The page that `speech serve` puts on localhost. The timings in it are the author's machine, not mine (speech.cpp README, page-speak.png)."
/>

I tried the server. `speech serve qwen3-tts-0.6b qwen3-asr-0.6b` loaded both, listened on 127.0.0.1, and answered OpenAI-shaped requests with `curl`. `POST /v1/audio/speech` with the default WAV format sent its first byte only after the whole 4.32 s take was made, 18.68 s later, because a WAV header needs the length. With `"response_format": "pcm"` the first byte came after 1.14 s while the rest streamed (both measured). `POST /v1/audio/transcriptions` returned `{"text":"Served over the OpenAI speech endpoint on localhost."}` for that take. The PCM from a second server run, same voice and seed, was byte-identical to the first run's WAV payload (measured), which is the determinism the repository promises.

## How Qwen3-TTS turns text into a waveform

Qwen3-TTS's design is what makes a quick first sound possible. The [technical report](https://arxiv.org/abs/2601.15621) describes two tokenizers. The one speech.cpp runs is the 12 Hz one.

<Figure
  src="https://media.thesatyajit.com/ai/articles/speech-cpp/fig2.png?v=4dcd4419325131fb"
  alt="Two diagrams side by side. Left, Qwen-TTS-Tokenizer-25Hz: waveform into Audio Encoder 2, a Vector Quantizer, single-codebook tokens, then Audio Encoder 1 and Qwen2-Audio, with a Mel Decoder branch. Right, Qwen-TTS-Tokenizer-12Hz: waveform into an encoder of a Downsample ConvNet and a Window Transformer, split into one VQ token and several RVQ tokens, then a decoder of a Window Transformer and an Upsample ConvNet back to a waveform; WavLM with cosine similarity and adversarial losses are training-only."
  caption="The two speech tokenizers. Qwen3-TTS-12Hz uses (b): one semantic codebook plus residual codebooks, decoded by a window transformer and a causal convolutional upsampler (Qwen3-TTS technical report, Figure 2)."
/>

**The codec.** Speech is cut into frames of 0.08 s, 12.5 per second. Each frame is 16 codes: the first codebook carries content, the other fifteen are residual layers that add acoustic detail. Sixteen codes at 12.5 frames a second is 200 codes for every second of speech (reasoned). The decoder turns one frame's 16 codes into 1,920 samples of 24 kHz audio, and every stage of it is causal. In speech.cpp's `CodecDecoder` that means it keeps the tail each stage needs between calls: the inputs of each convolution, the overlapping half of each transposed convolution, the sliding-window keys and values of its transformer. Decode a sentence in pieces and you get the samples of decoding it whole.

<Figure
  src="https://media.thesatyajit.com/ai/articles/speech-cpp/fig1.png?v=976b60945b689bff"
  alt="Qwen3-TTS architecture. A row of text tokens and pads at the bottom, and above it codec pad, think, speaker and codec embedding tokens, feed a Qwen3 LM. The LM emits codebook 0 per frame. An MTP Module stacks codebooks 1, 2 up to 15 on each, the sum of each column becomes a codec embedding fed back, and the columns go up into a Streaming Codec Decoder that outputs a waveform. Example text: Please speak with a warm and encouraging voice; Hello, I am Qwen3-TTS!"
  caption="Qwen3-TTS: a Qwen3 language model predicts codebook 0 of each frame; a multi-token-prediction module fills codebooks 1 to 15; a streaming codec decoder turns each frame into audio. Dashed boxes are optional (Qwen3-TTS technical report, Figure 3)."
/>

**The talker and the code predictor.** In speech.cpp's words (`talker.h`), these are "two Qwen3 decoder stacks". The talker reads one embedding per frame, the sum of the previous frame's 16 code embeddings plus the text, and predicts that frame's first code. The code predictor, conditioned on the talker's last hidden state, predicts the other fifteen one after another. The talker's key/value cache spans the whole utterance; the code predictor's starts afresh at every frame. So each 0.08 s of speech costs one talker step and fifteen small predictor steps, sixteen sampling decisions in a row (reasoned from the code). On this CPU with two threads, that is the 0.30 s per frame in the table.

**What it does not do.** The 0.6B CustomVoice model speaks with nine fixed speakers (`aiden`, `ryan`, `serena`, `ono_anna` and five more). Cloning and VoiceDesign are other Qwen3-TTS checkpoints and are not ported. Only the 1.7B model follows a speaking instruction.

## Why the first sound comes early, and when that is enough

A sentence of the model takes 95 frames to say 7.6 s. A system that waits for all of them makes you wait for the slowest part. speech.cpp hands the codec chunks of **1, 1, 2 and then 4 frames**, fixed in advance. The first frame goes out alone, so the first audio leaves as soon as one frame exists; the small chunks keep the player fed while generation gets ahead; four frames is the cap so a cancel stops quickly.

The repository's design record explains why the sizes are fixed rather than adapted to the machine's speed. The codec carries its state across calls, but its samples still depend, by rounding, on how frames are grouped: regrouping one sentence changed 98% of its samples, by up to 7.0e-4 on the CPU (reported). Sizes that followed the clock would make the same seed give slightly different audio under load. With fixed sizes, a seed reproduces its samples, and my two server runs above came out byte-identical.

Irodori-TTS works the other way. It is a diffusion transformer that makes a whole sentence's latent first, in four MeanFlow steps for the MF model, and only then decodes it, in windows of 12 frames first and 24 to 48 after, sized by measured speed. Its decoder's receptive field reaches 8 frames either side, so decoding 8 frames of margin gives bit-identical audio whatever the window sizes (reported). That is why Irodori's first audio came after 4 to 5 s here and Qwen3-TTS's after under a second.

Streaming gives a fast first sound. It does not make a slow machine fast. The widget plays the schedule against the clock. Each frame lasts 0.08 s, so a machine that needs more than 0.08 s to make one runs the player dry between chunks.

<StreamBudget />

On the README's M5 numbers, RTFx 3.7 and first audio after 0.029 s, a frame takes about 0.022 s and the player never waits (reasoned). On this VM, at 0.303 s a frame and first audio after 0.876 s, the player waits at 25 of the 25 chunk boundaries, about 21 s in all, and must buffer roughly 21 s up front to play without a gap (reasoned from measured times). There is a second delay on top: the 0.6B model often starts with silence. My takes in the voice `ryan` open with 0.42 to 0.48 s of it and the other voices with 0.21 to 0.33 s (measured), and Silero VAD put the start of speech in take 1 at 0.386 s. At 0.30 s a frame, those five or six silent frames cost this CPU another 1.6 to 1.8 s before a voice is heard (reasoned). The model docs report the same silence on the M5 and from the official implementation.

## What "checked against the official implementations" means

The README's claim is that "every stage of every model is compared with the model's official implementation, from the same inputs." In the repository that is three separate mechanisms, and they cover different things.

**Stage checks.** For each family, `reference/<family>/dump.py` runs the official Python implementation at a pinned Hugging Face revision (Qwen3-TTS 0.6B at `85e237c`) and saves every intermediate tensor as `.npy`: the prompt embeddings, the talker logits per frame, the code predictor's logits, the codes, each codec stage and the waveform. A C++ check per stage in `checks/<family>/` loads the GGUF, feeds the same inputs and compares, using one formula in `checks/compare.h`: the largest absolute difference and an SNR, $10\log_{10}(\sum w^2/\sum (g-w)^2)$, where $w$ is the official value and $g$ the port's. The thresholds are concrete. The Qwen3-TTS codec check fails below 40 dB against the official decoder, or if decoding in streaming chunks is more than 3 dB worse than decoding whole; the docs report 114 dB and 132 dB on the CPU in F32. The talker check teacher-forces the official codes, then runs greedy decoding and fails if it picks a different code anywhere the official logit led by more than twice the largest error seen under teacher forcing, so only near-ties may differ. Drama 3's text model must stay above 100 dB per stage on the CPU (50 dB on a GPU); it measured 120.2 dB, and its sampler matched the official probabilities to within 3.2e-6 (reported). The `checks/` directory is 13,175 lines of C, C++ and Python (measured), about as much as the ports themselves.

**Quantized files.** Stage checks take F32 files. The Q8_0 and K-quant files you actually download are checked differently: `checks/compare/quantize_compare.py` requires that `speech quantize` writes the same bytes as the converters, and quality per weight type is measured end to end, as the CER of the speech as Qwen3-ASR 1.7B hears it, on 20 Japanese sentences. So the Q8_0 Qwen3-TTS I ran is "checked" at the level of its F32 parent plus a CER table, not stage by stage.

**Regressions.** `checks/compare/same_audio.py` speaks the same requests through two builds and fails on the first sample that differs bit for bit.

What I could run: the checks runner's model-free set, where `fft-check` and `quantize-check` passed and two were skipped for lack of reference data (measured). The stage checks need the official Python environment and F32 files of several GB, which this run did not cover. So the claim is well specified and the tolerances are tight and written down, but I did not reproduce those numbers.

## Speed: their machines and mine

The README's numbers were measured with the author's speech-bench tool on an Apple M5 with Metal and an RTX 2080 with Vulkan, on Japanese sentences, one request at a time (reported). I can't reproduce them without that hardware. Here they are beside mine, which use different sentences, so compare orders of magnitude, not decimals.

| Model | First audio, M5 / RTX 2080 | RTFx, M5 / RTX 2080 | First audio, this VM | RTFx, this VM |
|---|---|---|---|---|
| `qwen3-tts-0.6b` Q8_0 | 0.029 s / 0.024 s | 3.7 / 5.2 | 0.669 to 0.989 s | 0.25 to 0.27 |
| `irodori-tts-mf` | 0.16 s / 0.12 s (F16) | 9.0 / 18 | 4.01 to 5.09 s (Q4_K) | 0.22 to 0.23 |
| `qwen3-asr-0.6b` Q8_0 | p50 latency 0.107 s / 0.072 s | | 2.23 to 5.97 s per file | 1.7 |
| `parakeet-tdt-0.6b-v3` | p50 latency 0.058 s / 0.095 s (F16) | | 1.16 to 2.07 s per file (Q8_0) | 4.6 |

RTFx is the inverse of my RTF: seconds of audio per second of work, larger is faster. On this VM, Qwen3-TTS 0.6B runs at about one-fourteenth of the M5's reported rate (reasoned: 3.7 against 0.26). Recognition is a different story: both recognizers ran faster than real time on two shared threads, and Silero VAD covered 7.60 s of audio in 0.034 s (measured). A CPU-only box is fine for listening and too slow for live synthesis with this model. Loading cost another 1.1 to 5.9 s per run for Qwen3-TTS, depending on whether the file was still in the page cache (measured).

## Drama 3: why it needs 32 GB, counted from the file

[Drama 3](/articles/fish-drama-3) is the headline of v0.8.8 and the one model I could not run. The file speech.cpp fetches, `Drama-35B-A3B-v3-Q4_K.gguf`, is 20.8 GB, and the VM has 15 GB of RAM, no swap and no GPU. I read its GGUF header by HTTP range request instead, the first 24 MiB of the file, and summed the tensor sizes. They add up to 20,777,094,148 bytes, exactly the `weight_bytes` the model's own JSON lists (measured).

| Part of the Q4_K file | Size | Label |
|---|---|---|
| Routed experts, 40 layers x 256 experts x 3 matrices | 18.12 GB, 87% of the weights | measured |
| One layer's routed experts | 0.453 GB | measured |
| Everything else: attention and DeltaNet weights, shared experts, embeddings, the 13-layer audio decoder, the codec | 2.66 GB | measured |
| Caches and graph buffers for the largest request | about 2.1 GB | reported |

The header also confirms the shape the [Drama 3 article](/articles/fish-drama-3) found in the original checkpoint, 256 experts with 8 active, and a codec hop of 2,048 samples at 44.1 kHz, so one frame is 46.4 ms of audio (measured from the header).

Now the requirements in the post follow. **On a Mac** the whole model lives in unified memory: 20.78 GB of weights plus about 2.1 GB of working buffers is about 23 GB before the operating system and anything else (reasoned). The docs report 19.6 GiB for the process on a 32 GB M5. A 24 GB Mac would have almost nothing left; 32 GB is the first common size that fits.

**On a PC with an 8 GB GPU**, speech.cpp's loader (`placement.cpp`) puts the non-expert weights and the working buffers on the GPU first, then fills the remaining memory with whole layers of experts, from the first layer up, and leaves the rest of the experts in system RAM for the CPU to compute. With 0.453 GB per layer, an 8 GB card takes 5 layers, and the CPU holds 35 x 0.453 = 15.9 GB of experts (reasoned). The docs report exactly that split: "35 of its 40 layers on the CPU", with 6.9 GiB on the GPU and 15.1 GiB of system RAM. Weights are read into those buffers with `fread`, not memory-mapped (measured from `model-file.cpp`), so that 15 GB is resident, and with the OS and other programs on top a 16 GB machine is out. Hence 32 GB.

It is also slow in that configuration. Each layer whose experts run on the CPU costs about 1.15 ms of every 46 ms frame on an i9-9900K (reported), so 35 of them is about 40 ms (reasoned), and the reported result is RTFx 0.57 on the RTX 2080: slower than real time. On an RTX 5090 or H100 holding everything, it reports RTFx 2.6 and first audio after 0.12 to 0.14 s.

The port is held to the same checks as the rest: the docs report its codec at 96.9 dB or better per stage and all 40 text layers at 120.2 dB or better. Long texts go in chunks of about 300 bytes, each prompted with the first and latest chunk before it, as Fish's own server does. The weights stay under Fish Audio's licence, not MIT.

## What I did not check

- The M5 speedups that v0.8.8 announces. I had no Apple hardware, so every M5 and RTX 2080 number above is the README's.
- Vulkan and Metal at all. This was a CPU-only build.
- `qwen3-tts-1.7b`, `irodori-tts` (the 40-step model), `irodori-tts-large`, `reazonspeech-v2` and the Japanese Parakeet: not fetched, to stay within budget.
- The stage checks, which need the official Python reference environments and F32 files.
- Naturalness. I measured onsets, loudness and what two recognizers hear; the rest is for your ears.

## Where it fits

It is llama.cpp's shape applied to speech: one binary, GGUF files, quantized types, a server with an API people already use. The difference is the paperwork: 51 design records in `docs/adr/`, a page of measured accuracy per weight type, and checks as large as the code. For a voice agent like [speech-to-speech](/articles/speech-to-speech) it fills the TTS, ASR and VAD slots in one process. For one small voice on a CPU, [Paradee](/articles/paradee-tiny-tts) is far smaller; for on-device recognition see [Cactus Whistle](/articles/whistle-stt); for who spoke when, which speech.cpp does not do, [Nemotron 3 Diarization](/articles/nemotron-3-diarization).

On a shared CPU it works, it repeats itself to the byte, and its recognizers beat real time. Its synthesis is about four times too slow here, so run that part on a GPU or a Mac if someone is waiting to hear it.
