Phonon imports Gradium's voice catalog
Phonon now imports Gradium's voice catalog
Every voice in the checkpoint, listed by ollama show, picked by name.
Phonon, the text to speech model in Ollama, can now carry Gradium's whole voice catalog. You pick a voice by name, and Phonon speaks in it. Behind that one flag, ollama create imports every voice file the checkpoint ships, ollama show lists them, names match in any case, and leaving the voice out means default.
One voice per checkpoint
Until now, create imported one voice
The default speaker, kept beside the model weights.
create/phonon.go
Checkpoint folder
Until this change, importing a Phonon checkpoint read exactly one voice. The publisher keeps its default speaker beside the model weights, in a file named default voice, and create stored it as a single tensor called voices dot default. Gradium keeps its other voices in a voices folder next to that file, and the old import didn't read that folder.
create reads the voices folder
Now every voices/NAME.safetensors becomes a voice
Also changed
The voice files join the model's weight files, so they ship as blobs with it.
create/inventory.go
Now create reads that folder too. Each safetensors file in that folder becomes a voice named after the file, so the Zoey file is stored as voices dot Zoey, right next to the default. The voice files are added to the model's weight files as well, so their blobs travel with the model. A checkpoint without a voices folder imports exactly as it did before.
Each voice file is checked
One helper checks every voice, default included
create/phonon.go · addPhononVoice
func addPhononVoice(inv Inventory, name, file string) error {
// … open the voice file
if names := reader.ListTensors(); !slices.Equal(names, []string{"speaker_wavs"}) {
return fmt.Errorf("Phonon %s voice must contain only speaker_wavs, got %v", name, names)
}
// … read the speaker_wavs tensor
if voice.Dtype != "F32" || len(voice.Shape) != 3 ||
voice.Shape[0] != 1 || voice.Shape[1] != 512 || voice.Shape[2] <= 0 {
return fmt.Errorf("unsupported Phonon %s voice: %s %v", name, voice.Dtype, voice.Shape)
}
if inv.Has("voices." + name) {
return fmt.Errorf("Phonon checkpoint already defines voices.%s", name)
}
inv.Tensors["voices."+name] = SourceTensor{Name: "speaker_wavs", …, File: file}
return nil
}
The old single-voice code became one helper, and every voice goes through it, the default included. The file must hold one tensor, called speaker wavs. That tensor must be 32-bit float, shaped one by 512 by any length. The name can't collide with a voice the checkpoint already defines. Only then is the voice stored, pointing back at its own file.
ollama show lists the voices
ollama show lists a speech model's voices
Expected output in cmd/cmd_test.go
Once the voices are in the model, ollama show lists them. This is the exact output the new test expects: a Voices table under Capabilities, with default, Fairy Godmother and Zoey. For speech models, the server builds that list from the manifest's voice layers, sorted without regard to case, and returns it as a new voices field in the show API.
Names match in any case
Voice names match in any case, with or without accents
mlxrunner/model/phonon/model.go
voice() · foldVoiceName()
You ask for → you get
The runner now finds a voice the way the publisher's own runtime does. An exact name wins. Otherwise both names are folded: decomposed, stripped of accent marks, and lowercased. So lowercase zoey finds Zoey, and a name typed without its accent still finds the accented voice. A name that matches nothing, like alloy, still returns a 400 error that lists every available voice.
No voice means default
Leaving the voice out now always means default
Before
The first name in sorted order
After
Asked for by name
Loading fails without it: model has no default voice
Before, a request without a voice took the first name in sorted order. With one voice, that was always default. In a catalog, byte order puts a capitalized name like Easton ahead of lowercase default, so the runner now asks for default by name. And a model now fails to load unless it has a default voice, so that request always has a voice to land on.
Aside: compact voice prompts
Aside
Each loaded voice now keeps a compact copy of its prompt
mlxrunner/model/phonon/model.go · prefillVoice()
m.Flow.forward(prompt, 0, caches)
+ // Copy out the prompt's frames so the
+ // cache's padded buffers are freed.
var kv []*mlx.Array
for _, c := range caches {
- kv = append(kv, c.State()...)
+ for _, s := range c.State() {
+ kv = append(kv, mlx.Contiguous(s, false))
+ }
}
return kv
An aside on memory. When the model loads, the runner runs every voice through the flow model once and keeps the resulting cache state. That state is now copied out contiguously, and the new comment says why: so the cache's padded buffers are freed. A catalog means one of these prefilled prompts per voice, held for as long as the model stays loaded.
Three tests
Three tests cover import, listing and lookup
The test voices hold no real speaker data, so these tests check names and wiring only.
The evidence is a test at each layer. The import test builds a checkpoint with a Zoey voice file and checks that both voices land in the model as blobs. The show test pins the exact Voices table. And the lookup test runs seven names through the runner, including an accented one and one that doesn't exist. The test voices are placeholders with no real speaker data, so these tests check names and wiring, not how a voice sounds. The pull request includes no audio comparison.
Try it
Phonon now imports Gradium's voice catalog
Voices come from the checkpoint's voices/ folder. Without one, a model has only default.
docs/capabilities/speech.mdx · docs/cli.mdx · docs/api/openai-compatibility.mdx
One limit to keep in mind. The voices come from the checkpoint, so a model created without a voices folder still has exactly one voice, default. To see what a model carries, run ollama show, then pass any listed name to the voice flag, in whatever case you type it. The speech guide, the CLI page and the OpenAI compatibility page all say the same now. Phonon now imports Gradium's voice catalog.










