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>
101 lines
3.1 KiB
Go
101 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/go-audio/audio"
|
|
"github.com/go-audio/wav"
|
|
)
|
|
|
|
// mossttsSampleRate is the MOSS-TTS-Local v1.5 output rate; the engine also
|
|
// reports it per-synthesis via out_sr, which the writers honour.
|
|
const mossttsSampleRate = 48000
|
|
|
|
// mossttsChannels is the MOSS-TTS-Local output layout: 48 kHz stereo, samples
|
|
// interleaved [L,R,L,R,...].
|
|
const mossttsChannels = 2
|
|
|
|
// wavHeaderStereo returns a 44-byte WAV header for a streaming 16-bit stereo PCM
|
|
// stream at sampleRate, with placeholder (0xFFFFFFFF) sizes since the total
|
|
// length is unknown up front. Emitted as the first chunk of TTSStream so the
|
|
// HTTP layer receives a self-describing WAV.
|
|
func wavHeaderStereo(sampleRate int) []byte {
|
|
if sampleRate <= 0 {
|
|
sampleRate = mossttsSampleRate
|
|
}
|
|
const blockAlign = mossttsChannels * 2 // 16-bit stereo
|
|
var buf bytes.Buffer
|
|
w := func(v any) { _ = binary.Write(&buf, binary.LittleEndian, v) }
|
|
buf.WriteString("RIFF")
|
|
w(uint32(0xFFFFFFFF))
|
|
buf.WriteString("WAVE")
|
|
buf.WriteString("fmt ")
|
|
w(uint32(16)) // Subchunk1Size
|
|
w(uint16(1)) // PCM
|
|
w(uint16(mossttsChannels)) // stereo
|
|
w(uint32(sampleRate)) // sample rate
|
|
w(uint32(sampleRate * blockAlign)) // byte rate = SR * blockAlign
|
|
w(uint16(blockAlign)) // block align
|
|
w(uint16(16)) // bits per sample
|
|
buf.WriteString("data")
|
|
w(uint32(0xFFFFFFFF))
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// floatToPCM16LE clamps each sample to [-1,1] and encodes it as little-endian
|
|
// signed 16-bit PCM. Input may be interleaved stereo; the layout is preserved.
|
|
func floatToPCM16LE(samples []float32) []byte {
|
|
out := make([]byte, len(samples)*2)
|
|
for i, s := range samples {
|
|
if s > 1 {
|
|
s = 1
|
|
} else if s < -1 {
|
|
s = -1
|
|
}
|
|
v := int16(s * 32767)
|
|
out[i*2] = byte(v) // #nosec G115 -- intentional little-endian split of a clamped int16
|
|
out[i*2+1] = byte(v >> 8) // #nosec G115 -- high byte of the same clamped int16
|
|
}
|
|
return out
|
|
}
|
|
|
|
// writeWAVStereo writes interleaved [L,R,...] float samples as a finalized
|
|
// 16-bit stereo WAV at sampleRate (falling back to the v1.5 default of 48 kHz).
|
|
func writeWAVStereo(dst string, samples []float32, sampleRate int) error {
|
|
if sampleRate <= 0 {
|
|
sampleRate = mossttsSampleRate
|
|
}
|
|
f, err := os.Create(dst) // #nosec G304 -- dst is the server-chosen output path from the TTS request, not user-traversable
|
|
if err != nil {
|
|
return fmt.Errorf("moss-tts: create %q: %w", dst, err)
|
|
}
|
|
enc := wav.NewEncoder(f, sampleRate, 16, mossttsChannels, 1)
|
|
ints := make([]int, len(samples))
|
|
for i, s := range samples {
|
|
if s > 1 {
|
|
s = 1
|
|
} else if s < -1 {
|
|
s = -1
|
|
}
|
|
ints[i] = int(s * 32767)
|
|
}
|
|
b := &audio.IntBuffer{
|
|
Format: &audio.Format{NumChannels: mossttsChannels, SampleRate: sampleRate},
|
|
Data: ints,
|
|
SourceBitDepth: 16,
|
|
}
|
|
if err := enc.Write(b); err != nil {
|
|
_ = enc.Close()
|
|
_ = f.Close()
|
|
return fmt.Errorf("moss-tts: encode WAV: %w", err)
|
|
}
|
|
if err := enc.Close(); err != nil {
|
|
_ = f.Close()
|
|
return fmt.Errorf("moss-tts: finalize WAV: %w", err)
|
|
}
|
|
return f.Close()
|
|
}
|