mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 18:09:05 -04:00
* feat(backend): add magpie-tts-cpp text-to-speech backend
Add a Go + purego backend wrapping the magpie-tts.cpp ggml port of NVIDIA's
Magpie TTS Multilingual 357M (encoder + autoregressive decoder over NanoCodec
tokens), producing 22.05 kHz mono audio in 5 baked voices (Aria, Jason, John,
Leo, Sofia; case-insensitive names or indices 0-4) across 9+ languages from a
single self-contained GGUF. Mirrors qwen3-tts-cpp / moss-tts-cpp: dlopen the
static-ggml shared library, bind the flat magpie_tts_capi_* C-API via purego
(no local C shim needed, the upstream .so exports it directly), and serve the
gRPC TTS + TTSStream methods behind base.SingleThread (the C context is not
reentrant across synthesize calls).
The backend CMakeLists translates the Makefile's -DGGML_{CUDA,METAL,VULKAN,HIP}
flags into upstream's MAGPIE_GGML_* toggles (upstream FORCE-overwrites the ggml
cache entries from those), pinned to magpie-tts.cpp v0.1.1
(e3f3dd1ebe22b64e7405f93b519f2d1930712568), which statically links ggml into
libmagpie-tts.so (ldd shows only system libs).
Wires the full registration: backend-matrix.yml (CPU amd64/arm64, CUDA 12/13,
Intel SYCL f16/f32, Vulkan amd64/arm64, ROCm, NVIDIA L4T + L4T CUDA 13, and
Darwin metal), backend/index.yaml metas and image entries, the root Makefile
build targets, the changed-backends backend-filter path mapping, the bump_deps
auto-bump matrix, a test-extra per-backend smoke job, the /backends/known
pref-only importer entry, the backend capabilities map (TTS + TTSStream, no
voice cloning), and the README / compatibility-table docs rows.
Verified locally: unit + e2e Ginkgo suites pass against the real q8_0 GGUF
(22.05 kHz mono WAV, RMS > 0.01), a live gRPC LoadModel + TTS round-trip
returns valid non-silent audio, and the pre-commit gates (make lint,
make test-coverage-check) pass, run manually with LOCALAI_TEST_HTTP_PORT
overriding the locally-occupied 9090.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* gallery: add magpie-tts-cpp model entries (q8_0 + f16)
Add the Magpie TTS Multilingual 357M GGUFs from mudler/magpie-tts.cpp-gguf to
the model gallery: q8_0 (~624 MB, near-lossless, fastest decode, recommended)
with an f16 (~784 MB) variant, both served by the magpie-tts-cpp backend.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* magpie-tts-cpp: bump pin to rewritten upstream v0.1.1 SHA
Upstream history was rewritten to purge accidentally committed build
artifacts; v0.1.1 now resolves to 6f7696cf.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
109 lines
3.2 KiB
Go
109 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// loadOptions holds the parsed model-level options. Magpie is a single
|
|
// self-contained GGUF (model + codec + tokenizer + G2P dictionaries), so the
|
|
// options only cover synthesis defaults.
|
|
type loadOptions struct {
|
|
// speaker is the default baked speaker when a request has no voice.
|
|
speaker string
|
|
// language is the default language when a request has none ("" = engine
|
|
// default, which is "en").
|
|
language string
|
|
}
|
|
|
|
func splitOption(o string) (key, value string, ok bool) {
|
|
i := strings.Index(o, ":")
|
|
if i < 0 {
|
|
return "", "", false
|
|
}
|
|
return strings.TrimSpace(o[:i]), strings.TrimSpace(o[i+1:]), true
|
|
}
|
|
|
|
// parseOptions reads the backend "key:value" option slice. Unknown keys are
|
|
// ignored.
|
|
func parseOptions(opts []string) loadOptions {
|
|
var o loadOptions
|
|
for _, oo := range opts {
|
|
key, value, ok := splitOption(oo)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch key {
|
|
case "speaker", "voice":
|
|
o.speaker = value
|
|
case "language", "lang":
|
|
o.language = value
|
|
}
|
|
}
|
|
return o
|
|
}
|
|
|
|
// magpieSpeakers are the baked speakers of Magpie TTS Multilingual 357M, in
|
|
// index order (the engine matches names exactly, so the Go side canonicalizes
|
|
// case-insensitive names and 0-4 indices to these strings).
|
|
var magpieSpeakers = []string{"Aria", "Jason", "John", "Leo", "Sofia"}
|
|
|
|
// resolveSpeaker maps the request voice (falling back to the model-level
|
|
// default) onto a canonical baked speaker name. Accepted forms:
|
|
// case-insensitive names (aria, JASON, ...) and indices 0-4. Empty selects the
|
|
// engine default (speaker 0, Aria). Anything else is rejected with the valid
|
|
// choices, instead of surfacing the engine's late error.
|
|
func resolveSpeaker(voice, fallback string) (string, error) {
|
|
v := strings.TrimSpace(voice)
|
|
if v == "" {
|
|
v = strings.TrimSpace(fallback)
|
|
}
|
|
if v == "" {
|
|
return "", nil
|
|
}
|
|
if idx, err := strconv.Atoi(v); err == nil {
|
|
if idx < 0 || idx >= len(magpieSpeakers) {
|
|
return "", fmt.Errorf("magpie-tts: speaker index %d out of range 0-%d", idx, len(magpieSpeakers)-1)
|
|
}
|
|
return magpieSpeakers[idx], nil
|
|
}
|
|
for _, s := range magpieSpeakers {
|
|
if strings.EqualFold(s, v) {
|
|
return s, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("magpie-tts: unknown voice %q (valid: %s, or 0-%d)",
|
|
voice, strings.Join(magpieSpeakers, ", "), len(magpieSpeakers)-1)
|
|
}
|
|
|
|
// magpieLanguages are the canonical language codes the tokenizer's language
|
|
// map knows (exact-match on the C side), keyed by their lowercase form so
|
|
// requests can be case-insensitive.
|
|
var magpieLanguages = map[string]string{
|
|
"en": "en", "es": "es", "de": "de", "fr": "fr", "it": "it",
|
|
"pt-br": "pt-BR", "hi": "hi", "vi": "vi", "ko": "ko",
|
|
"ar-ae": "ar-AE", "ar-sa": "ar-SA", "ar-msa": "ar-MSA",
|
|
}
|
|
|
|
// resolveLanguage picks the request language, else the model-level default,
|
|
// else "" (the engine defaults to "en"), canonicalizing case for the known
|
|
// codes. Unknown codes pass through verbatim so the engine reports them with
|
|
// its own exact-vocabulary error.
|
|
func resolveLanguage(reqLang *string, fallback string) string {
|
|
l := ""
|
|
if reqLang != nil {
|
|
l = strings.TrimSpace(*reqLang)
|
|
}
|
|
if l == "" {
|
|
l = strings.TrimSpace(fallback)
|
|
}
|
|
if l == "" {
|
|
return ""
|
|
}
|
|
if canon, ok := magpieLanguages[strings.ToLower(l)]; ok {
|
|
return canon
|
|
}
|
|
return l
|
|
}
|