Speech, embeddings and rerank¶
Beside its chat models, the gmlx server can run four services on the same
port: speech-to-text, text-to-speech, embeddings and reranking. The voice
client gmlx talk, RAG pipelines and apps such as
Open WebUI use them.
To turn a service on, name its model in the server block of the
configuration file, then
reload the server or run gmlx restart:
true selects each service's default model, the one shown here. The
embeddings and rerank models here are GGUF files that the server never
downloads, so pull them first, as Embeddings and
Reranking show.
Speech-to-text¶
Speech-to-text turns a recording into text with
mlx-whisper. It needs the stt
extra, which Optional features
installs, and ffmpeg (brew install ffmpeg). Set server.stt, restart, and
send a file:
/v1/audio/translations takes speech in any language and returns English
text:
The optional form fields are language (transcriptions only), prompt,
temperature (0 by default) and response_format: json, text,
verbose_json, srt or vtt. A recording longer than the
audio limit gets a 400, so split it.
server.stt takes one of these aliases, a Hugging Face repository in
MLX-Whisper format, or a local folder with a converted model:
| Alias | Repository | Notes |
|---|---|---|
whisper-turbo |
mlx-community/whisper-large-v3-turbo |
Default. Large-v3 quality at about six times the speed. |
whisper-turbo-q4 |
mlx-community/whisper-large-v3-turbo-q4 |
4-bit turbo, about 600 MB |
whisper-large |
mlx-community/whisper-large-v3-mlx |
Full large-v3 |
whisper-medium |
mlx-community/whisper-medium-mlx |
Smaller and faster than large-v3 |
whisper-small |
mlx-community/whisper-small-mlx |
Smaller and faster than medium |
whisper-base |
mlx-community/whisper-base-mlx |
Smaller and faster than small |
whisper-tiny |
mlx-community/whisper-tiny |
Smallest and fastest |
Granite Speech¶
The Granite Speech companion of Granite Switch can
serve this route too, without mlx-whisper. Name its GGUF, or use the alias
granite-speech for the companion a configured model pairs with:
It transcribes only. It gives the json and text formats, and it
ignores language, prompt and temperature. The other formats and
/v1/audio/translations get a 400. The chat models and this route share
one loaded copy of the companion.
A served model that hears audio¶
A chat model that hears audio through its own speech encoder, such as Nemotron 3.5 Transcribe, answers both routes itself, without mlx-whisper. Name it by its id or alias:
With server.stt unset, the server picks such a model by itself: the
default model when it hears audio, else the one configured model that
does. stt: false turns speech-to-text off. Such a model also gives
speakers and word times, as
Transcription and translation
shows.
Unlike the other service models, this one is a chat model. It loads,
counts against server.budget_gb and unloads as chat models do, and a
transcription waits in the same queue as chat requests.
Text-to-speech¶
Text-to-speech reads text aloud with
mlx-audio. It needs the tts extra.
Set server.tts, restart, and send a line:
curl localhost:8080/v1/audio/speech -H 'content-type: application/json' \
-d '{"model": "tts-1", "input": "Hello from MLX.", "voice": "af_heart"}' -o out.mp3
The optional fields are voice, speed (0.25 to 4.0) and
response_format: mp3 (the default), wav, flac, opus or pcm.
Every format except wav and pcm needs ffmpeg. The server removes
markdown and emoji from input before it speaks.
voice takes one voice name, or several joined by commas. Without it,
Kokoro speaks as af_heart. To list the model's voices:
curl localhost:8080/v1/audio/voices
# {"model": "mlx-community/Kokoro-82M-bf16", "voices": ["af_alloy", ...], "default": "af_heart"}
server.tts takes one of these aliases, a Hugging Face repository in
MLX-Audio format, or a local folder with a converted model:
| Alias | Repository | Notes |
|---|---|---|
kokoro |
mlx-community/Kokoro-82M-bf16 |
Default. Small English model with many preset voices. |
kokoro-8bit |
mlx-community/Kokoro-82M-8bit |
Smaller Kokoro |
kokoro-4bit |
mlx-community/Kokoro-82M-4bit |
Smallest Kokoro |
qwen3-tts |
mlx-community/Qwen3-TTS-12Hz-1.7B-Base-8bit |
Larger multilingual model with named voices |
qwen3-tts-small |
mlx-community/Qwen3-TTS-12Hz-0.6B-Base-bf16 |
Smaller Qwen3-TTS |
Kokoro downloads a small English language model the first time it speaks, so a server without internet access needs one run with access first.
A Sesame model, such as mlx-community/csm-1b, speaks the preset voices in
its repository, such as conversational_a. Set server.tts to the
repository, not a local folder. Sesame also needs files from two other
repositories in the Hugging Face cache:
hf download kyutai/moshiko-pytorch-bf16 tokenizer-e351c8d8-checkpoint125.safetensors
hf download unsloth/Llama-3.2-1B --include "*.json"
gmlx launch open-webui sets Open WebUI's voice to af_heart. With another
speech model, export AUDIO_TTS_VOICE with one of its voices before the
launch.
Embeddings¶
Embeddings turn text into vectors for search. They need no extra. The default model is a GGUF that the server never downloads, so pull it first:
Set server.embeddings, restart, and send text:
curl localhost:8080/v1/embeddings -H 'content-type: application/json' \
-d '{"model": "text-embedding-3-small", "input": ["hello", "world"]}'
input is a string or a list of strings. encoding_format is float (the
default) or base64. Vectors come back normalized to length 1. Input longer
than the model's context is cut to fit, and the OpenAI dimensions field is
ignored.
server.embeddings takes a GGUF alias, a *.gguf path or an
hf:<org>/<repo>/<file>.gguf reference. The dimension is the width of each
vector, and the context is the most input tokens the model reads:
| Alias | Repository, Q8_0 | Dimension | Context | Notes |
|---|---|---|---|---|
qwen3-embed-0.6b |
Qwen/Qwen3-Embedding-0.6B-GGUF |
1024 | 32K | Default. Small, fast and multilingual, about 0.6 GB. |
qwen3-embed-4b |
Qwen/Qwen3-Embedding-4B-GGUF |
2560 | 32K | Better retrieval, about 4.3 GB |
qwen3-embed-8b |
Qwen/Qwen3-Embedding-8B-GGUF |
4096 | 32K | Best retrieval of the family and the largest index, about 8 GB |
embeddinggemma-gguf |
ggml-org/embeddinggemma-300M-GGUF |
768 | 2K | Small multilingual encoder from Google, about 0.3 GB |
gmlx init can pick another quant and write its full reference.
The server also runs safetensors encoders, from an alias, a Hugging Face repository or a local folder. An MLX checkpoint, plain or quantized, runs on the encoders built into gmlx, which cover EmbeddingGemma, BERT, XLM-RoBERTa, ModernBERT and Qwen3-Embedding. These download once when they are not in the cache:
| Alias | Repository | Dimension | Context | Notes |
|---|---|---|---|---|
embeddinggemma |
mlx-community/embeddinggemma-300m-8bit |
768 | 2K | Small multilingual model from Google, about 0.3 GB |
arctic-l |
mlx-community/snowflake-arctic-embed-l-v2.0-8bit |
1024 | 8K | Multilingual, long inputs |
nomic-embed |
mlx-community/nomicai-modernbert-embed-base-8bit |
768 | 8K | English, long inputs |
bge-m3 |
mlx-community/bge-m3-mlx-8bit |
1024 | 8K | Multilingual, long inputs |
Choose a GGUF model unless you want an encoder for its size or languages.
The server checks the model at startup, from a safetensors encoder's
config.json or from a GGUF's header, and a model that fails the check
does not load. A safetensors encoder fails for another model type, and for
a Gemma 3 or Qwen3 chat model, which shares its model type with
EmbeddingGemma or Qwen3-Embedding. It also fails when its config.json or
modules.json cannot be read or is not valid JSON. A GGUF is an embeddings
model when it is of an encoder architecture, sets <arch>.pooling_type to 1
or more, or has embed or embedding as a word of its file name, as the
Qwen3-Embedding GGUFs do and a chat model does not. Any other GGUF fails.
So does a reranker, by its file name or a pooling type of 4, a GGUF of a
pooling type over 4, and a GGUF of an encoder architecture that gmlx cannot
run, which is every encoder architecture but EmbeddingGemma's. A GGUF of
EmbeddingGemma's architecture fails when it sets a pooling type other than
1, since that encoder takes the mean. A GGUF that sets
<arch>.attention.causal to false fails unless it is of the llama,
mistral3, qwen2 or qwen3 architecture and has no sliding window. The server
still starts, and the server log and every /v1/embeddings response give
the reason with a 501 status.
A Qwen3 checkpoint is Qwen3-Embedding when its modules.json lists a
sentence-transformers Pooling module, or when its repository, folder name or
model card names Qwen3-Embedding, as for a conversion without modules.json.
An encoder pools its vectors as the sentence-transformers configuration in
its folder says. When the folder has none, BGE-M3, Arctic-Embed, the English
and Chinese BGE v1 and v1.5 models, mxbai-embed-large-v1 and
mxbai-embed-2d-large-v1 use the first token, as their upstream
configurations do. The server knows these models by the name in
config.json or in the model card, or by the repository or folder name.
Qwen3-Embedding uses the last token, and the other models use the mean over
the tokens, as sentence-transformers does. The server log names the pooling
it chose.
A GGUF embedder that sets <arch>.pooling_type to 1 takes the mean over
the tokens, one that sets 2 the first token, and one that sets 3 the last
token. One that sets no pooling type, or sets 0, uses the last token and
appends an EOS to an input that does not end in one, as Qwen3-Embedding
does. One that sets <arch>.attention.causal to false, as Nemotron-3-Embed
does, runs without the causal mask, so each token attends to the whole
input.
EmbeddingGemma never runs in float16, which its model card rules out. It
runs in bfloat16, or in float32 where server.dtype
gives float16.
A cached encoder loads with no network access when its own files or its
name settle how it pools. Otherwise the server fetches the model card and
the sentence-transformers module files that the cached copy lacks, at the
commit of the copy, so the copy stays the one in use. Each file gets one
try, and the fetch stops after 5 seconds in all. Once the Hub has answered
for each file, or the fetch has timed out before the Hub answered any
request, the server leaves a mark in the cache,
models--<org>--<name>/.gmlx-fetched/<commit>, and does not fetch for that
copy again. To fetch again, delete the mark, which the log names when a
file is not fetched, and restart the server. A fetch that finds no
connection, or that times out after the Hub answered a request, as on a
slow connection, leaves no mark. The files it fetched stay, and the next
start asks for the others. With HF_HUB_OFFLINE=1, or in a cache the
server cannot write, the server fetches nothing.
Reranking¶
A reranker scores documents against a query more accurately than a vector search, so a RAG pipeline runs it on the short list that the search returns. The model is a Qwen3-Reranker GGUF, which the server never downloads:
Set server.rerank, restart, and rank some documents:
curl localhost:8080/v1/rerank -H 'content-type: application/json' \
-d '{"query": "how do I cancel?", "documents": ["Billing FAQ ...", "Setup guide ..."]}'
The request and response have the shape that Cohere and Jina use, and
/rerank works too. The response holds results, best first, each with
its index and relevance_score. The score is the probability that the
model gives to yes over no, from 0 to 1, so you can drop documents below a
threshold. The fields are:
| Field | Default | Meaning |
|---|---|---|
query |
Required | The text to rank against |
documents |
Required | Strings or {"text": ...} objects |
top_n |
Every document | How many results to return. top_k is another name for it. |
return_documents |
true |
With false, each result has only its index and score. |
instruction |
"Given a web search query, retrieve relevant passages that answer the query" | The task the reranker reads with the query |
server.rerank takes qwen3-rerank-0.6b, qwen3-rerank-4b or
qwen3-rerank-8b (each at Q8_0), a *.gguf path, or an hf: reference.
The model reads each document separately, up to 8192 tokens with the query,
so send tens of documents, not thousands.
How the services run¶
The server loads each service model in the background at start, and
again at a reload that changes it. Service
models do not count against server.budget_gb
and are never unloaded for chat models, so indexing and chatting do not
push each other out. Each service runs its requests one at a time, so a
long batch of embeddings holds up the next embeddings request.
A request may leave out model, or send a name that OpenAI clients use.
/v1/models lists each running service under the first of its names:
| Service | Names accepted in model |
|---|---|
| Speech-to-text | whisper-1 |
| Text-to-speech | tts-1, tts-1-hd, gpt-4o-mini-tts |
| Embeddings | text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002 |
| Reranking | reranker, or any name |
A request never makes the server download a model. The models you name in the config download at start, except GGUF models, which you pull, as Hugging Face policy explains.
A service that is not configured answers 404 with the key to set. When the
extra of a speech service in the config is missing, the server starts
without that service and logs a warning, which gmlx serve repeats, and
its routes answer 404 with the command that installs the extra. A reload of
the config turns the service off the same way, and the server stays up.
With --stt or --tts, a missing extra stops the server from starting. When an embeddings or rerank model
file is missing, the server runs without that service. Pull the file, then
reload the server or run gmlx restart.
The server finds ffmpeg on its PATH or in the Homebrew and system
folders, so a server started by a login item finds it too. gmlx doctor
reports FAIL when a speech service is configured and ffmpeg is missing.