Skip to content

Commit 2f3dd40

Browse files
feat(import): route MLX TTS models to mlx-audio (#11267)
Detect text-to-speech MLX repositories during model import and emit a TTS-ready mlx-audio configuration. Expose mlx-audio in the backend preference dropdown for repositories without complete metadata. Assisted-by: Codex:gpt-5 Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
1 parent a7440f0 commit 2f3dd40

5 files changed

Lines changed: 69 additions & 5 deletions

File tree

core/gallery/importers/mlx.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package importers
33
import (
44
"encoding/json"
55
"path/filepath"
6+
"slices"
67
"strings"
78

89
"github.com/mudler/LocalAI/core/config"
@@ -31,7 +32,7 @@ func (i *MLXImporter) Match(details Details) bool {
3132
}
3233

3334
b, ok := preferencesMap["backend"].(string)
34-
if ok && b == "mlx" || b == "mlx-vlm" {
35+
if ok && slices.Contains([]string{"mlx", "mlx-vlm", "mlx-audio"}, b) {
3536
return true
3637
}
3738

@@ -71,27 +72,40 @@ func (i *MLXImporter) Import(details Details) (gallery.ModelConfig, error) {
7172
// (issue #10269). Send them to the mlx-vlm backend, which applies the
7273
// processor-aware chat template.
7374
backend := "mlx"
74-
if details.HuggingFace != nil && details.HuggingFace.PipelineTag == "image-text-to-text" {
75-
backend = "mlx-vlm"
75+
usecases := []string{config.UsecaseChat}
76+
useTokenizerTemplate := true
77+
if details.HuggingFace != nil {
78+
switch details.HuggingFace.PipelineTag {
79+
case "image-text-to-text":
80+
backend = "mlx-vlm"
81+
case "text-to-speech":
82+
backend = "mlx-audio"
83+
usecases = []string{config.UsecaseTTS}
84+
useTokenizerTemplate = false
85+
}
7686
}
7787
// An explicit backend preference always wins.
7888
b, ok := preferencesMap["backend"].(string)
7989
if ok {
8090
backend = b
91+
if backend == "mlx-audio" {
92+
usecases = []string{config.UsecaseTTS}
93+
useTokenizerTemplate = false
94+
}
8195
}
8296

8397
modelConfig := config.ModelConfig{
8498
Name: name,
8599
Description: description,
86-
KnownUsecaseStrings: []string{config.UsecaseChat},
100+
KnownUsecaseStrings: usecases,
87101
Backend: backend,
88102
PredictionOptions: schema.PredictionOptions{
89103
BasicModelRequest: schema.BasicModelRequest{
90104
Model: LocalModelPath(details.URI),
91105
},
92106
},
93107
TemplateConfig: config.TemplateConfig{
94-
UseTokenizerTemplate: true,
108+
UseTokenizerTemplate: useTokenizerTemplate,
95109
},
96110
}
97111

core/gallery/importers/mlx_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ var _ = Describe("MLXImporter", func() {
4848
Expect(result).To(BeTrue())
4949
})
5050

51+
It("should match when backend preference is mlx-audio", func() {
52+
preferences := json.RawMessage(`{"backend": "mlx-audio"}`)
53+
details := importers.Details{
54+
URI: "https://example.com/model",
55+
Preferences: preferences,
56+
}
57+
58+
Expect(importer.Match(details)).To(BeTrue())
59+
})
60+
5161
It("should not match when URI does not contain mlx-community/ and no backend preference", func() {
5262
details := importers.Details{
5363
URI: "https://huggingface.co/other-org/test-model",
@@ -123,6 +133,21 @@ var _ = Describe("MLXImporter", func() {
123133
Expect(modelConfig.ConfigFile).To(ContainSubstring("backend: mlx-vlm"))
124134
})
125135

136+
It("should configure explicit mlx-audio imports for text-to-speech", func() {
137+
preferences := json.RawMessage(`{"backend": "mlx-audio"}`)
138+
details := importers.Details{
139+
URI: "https://huggingface.co/mlx-community/Kokoro-82M-4bit",
140+
Preferences: preferences,
141+
}
142+
143+
modelConfig, err := importer.Import(details)
144+
145+
Expect(err).ToNot(HaveOccurred())
146+
Expect(modelConfig.ConfigFile).To(ContainSubstring("backend: mlx-audio"))
147+
Expect(modelConfig.ConfigFile).To(ContainSubstring("- tts"))
148+
Expect(modelConfig.ConfigFile).ToNot(ContainSubstring("use_tokenizer_template: true"))
149+
})
150+
126151
It("should auto-route vision-language models to the mlx-vlm backend", func() {
127152
// gemma-4 E4B and similar VLMs declare pipeline_tag
128153
// "image-text-to-text" on HuggingFace. The text-only mlx-lm
@@ -143,6 +168,23 @@ var _ = Describe("MLXImporter", func() {
143168
Expect(modelConfig.ConfigFile).To(ContainSubstring("backend: mlx-vlm"))
144169
})
145170

171+
It("should auto-route text-to-speech models to the mlx-audio backend", func() {
172+
details := importers.Details{
173+
URI: "https://huggingface.co/mlx-community/Kokoro-82M-4bit",
174+
HuggingFace: &hfapi.ModelDetails{
175+
ModelID: "mlx-community/Kokoro-82M-4bit",
176+
PipelineTag: "text-to-speech",
177+
},
178+
}
179+
180+
modelConfig, err := importer.Import(details)
181+
182+
Expect(err).ToNot(HaveOccurred())
183+
Expect(modelConfig.ConfigFile).To(ContainSubstring("backend: mlx-audio"))
184+
Expect(modelConfig.ConfigFile).To(ContainSubstring("- tts"))
185+
Expect(modelConfig.ConfigFile).ToNot(ContainSubstring("use_tokenizer_template: true"))
186+
})
187+
146188
It("should keep text-only models on the plain mlx backend", func() {
147189
details := importers.Details{
148190
URI: "https://huggingface.co/mlx-community/Llama-3.2-1B-Instruct-4bit",

core/http/endpoints/localai/backend.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ var knownPrefOnlyBackends = []schema.KnownBackend{
3838
{Name: "whisperx", Modality: "asr", AutoDetect: false, Description: "WhisperX transcription (preference-only)"},
3939
{Name: "crispasr", Modality: "asr", AutoDetect: false, Description: "CrispASR multi-architecture transcription (preference-only)"},
4040
// TTS
41+
{Name: "mlx-audio", Modality: "tts", AutoDetect: false, Description: "MLX-Audio text-to-speech models (auto-detected; pref-only fallback)"},
4142
{Name: "kokoros", Modality: "tts", AutoDetect: false, Description: "Kokoros TTS (preference-only)"},
4243
{Name: "qwen-tts", Modality: "tts", AutoDetect: false, Description: "Qwen TTS (preference-only)"},
4344
{Name: "qwen3-tts-cpp", Modality: "tts", AutoDetect: false, Description: "Qwen3 TTS C++ (preference-only)"},

core/http/endpoints/localai/backend_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ var _ = Describe("Backend Endpoints", func() {
152152
expectPrefOnly("tinygrad", "text")
153153
expectPrefOnly("trl", "text")
154154
expectPrefOnly("mlx-vlm", "text")
155+
expectPrefOnly("mlx-audio", "tts")
155156
expectPrefOnly("whisperx", "asr")
156157
expectPrefOnly("crispasr", "asr")
157158
expectPrefOnly("kokoros", "tts")

docs/content/getting-started/models.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ The WebUI provides a powerful model import interface that supports both simple a
8080
- Custom preferences
8181
5. Click "Import Model" to start the import process
8282

83+
Repositories under `mlx-community` are imported with the native MLX backend.
84+
LocalAI uses Hugging Face's pipeline metadata to select `mlx-vlm` for
85+
vision-language models and `mlx-audio` for text-to-speech models; other MLX
86+
repositories use `mlx`. An explicit backend selection in the import form always
87+
overrides this automatic routing.
88+
8389
### Advanced Import Mode
8490

8591
For full control over model configuration:

0 commit comments

Comments
 (0)