mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 18:09:05 -04:00
* feat(backend): add moss-tts-cpp text-to-speech backend Add a Go + purego backend wrapping the moss-tts.cpp ggml port of the OpenMOSS MOSS-TTS-Local v1.5 text-to-speech model (GPT-J local transformer decoded through MOSS-Audio-Tokenizer-v2), producing 48 kHz stereo audio with optional reference-audio voice cloning. Mirrors the qwen3-tts-cpp backend: dlopen the static-ggml shared library, bind the moss-tts.cpp C-API via purego, and serve the gRPC TTS method. A thin C shim holds the pipeline handle and copies engine PCM into a Go-freeable buffer. Wires the CI registration: backend-matrix.yml (CPU, CUDA 12/13, Intel SYCL f16/f32, Vulkan, ROCm, NVIDIA L4T, plus Darwin metal), backend/index.yaml metas and image entries pointing at mudler/MOSS-TTS-Local-Transformer-v1.5-GGUF, the root Makefile build targets, and the changed-backends.js path mapping. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * docs: list the moss-tts-cpp backend among the LocalAI-maintained engines Add moss-tts.cpp to the README "Backends built by us" table, the Text-to-Speech compatibility table, and the reference-audio voice-cloning backend list, so the new backend is documented alongside its peers. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * backend(moss-tts-cpp): pin moss-tts.cpp to the squashed single-commit release moss-tts.cpp history was collapsed to a single commit; repoint MOSSTTS_CPP_VERSION to ee722b8e9205ee9b1b1c398a4e87e4e393e9be41. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * backend(moss-tts-cpp): add the moss-tts-cpp-development gallery meta The gallery had the -development image entries but no matching -development meta anchor (as locate-anything-cpp and depth-anything-cpp have), so the master build was not installable as a gallery backend. Add moss-tts-cpp-development mirroring the production meta with the -development capability image names. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
73 lines
2.1 KiB
Go
73 lines
2.1 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/ebitengine/purego"
|
|
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
func ttsReq(text, voice, dst string) *pb.TTSRequest {
|
|
return &pb.TTSRequest{Text: text, Voice: voice, Dst: dst}
|
|
}
|
|
|
|
var _ = Describe("moss-tts-cpp e2e", Label("e2e"), func() {
|
|
var loaded bool
|
|
|
|
BeforeEach(func() {
|
|
modelPath := os.Getenv("MOSSTTS_MODEL")
|
|
codecPath := os.Getenv("MOSSTTS_CODEC")
|
|
tokenizerPath := os.Getenv("MOSSTTS_TOKENIZER")
|
|
if modelPath == "" || codecPath == "" || tokenizerPath == "" {
|
|
Skip("MOSSTTS_MODEL / MOSSTTS_CODEC / MOSSTTS_TOKENIZER not set; skipping e2e")
|
|
}
|
|
if !loaded {
|
|
lib := os.Getenv("MOSSTTS_LIBRARY")
|
|
if lib == "" {
|
|
lib = "./libgomosstts-cpp-fallback.so"
|
|
}
|
|
h, err := purego.Dlopen(lib, purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
purego.RegisterLibFunc(&CppLoad, h, "mtl_load")
|
|
purego.RegisterLibFunc(&CppTTS, h, "mtl_tts")
|
|
purego.RegisterLibFunc(&CppPCMFree, h, "mtl_pcm_free")
|
|
purego.RegisterLibFunc(&CppUnload, h, "mtl_unload")
|
|
Expect(CppLoad(modelPath, codecPath, tokenizerPath)).To(Equal(0))
|
|
loaded = true
|
|
}
|
|
})
|
|
|
|
It("synthesizes a stereo WAV file via TTS", func() {
|
|
m := &MossTtsCpp{opts: loadOptions{seed: 42}}
|
|
dst := GinkgoT().TempDir() + "/out.wav"
|
|
err := m.TTS(ttsReq("Hello world.", "", dst))
|
|
Expect(err).ToNot(HaveOccurred())
|
|
fi, err := os.Stat(dst)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(fi.Size()).To(BeNumerically(">", int64(44)))
|
|
})
|
|
|
|
It("streams audio chunks via TTSStream", func() {
|
|
m := &MossTtsCpp{opts: loadOptions{seed: 42}}
|
|
results := make(chan []byte, 4096)
|
|
done := make(chan error, 1)
|
|
go func() { done <- m.TTSStream(ttsReq("Hello there, streaming test.", "", ""), results) }()
|
|
|
|
var chunks int
|
|
var first []byte
|
|
for c := range results {
|
|
if chunks == 0 {
|
|
first = c
|
|
}
|
|
chunks++
|
|
}
|
|
Expect(<-done).ToNot(HaveOccurred())
|
|
Expect(chunks).To(BeNumerically(">=", 2))
|
|
Expect(string(first[0:4])).To(Equal("RIFF"))
|
|
Expect(strings.HasPrefix(string(first[8:12]), "WAVE")).To(BeTrue())
|
|
})
|
|
})
|