mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-31 10:28:43 -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>
94 lines
2.8 KiB
Go
94 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/go-audio/audio"
|
|
"github.com/go-audio/wav"
|
|
)
|
|
|
|
// magpieSampleRate is the fixed Magpie TTS Multilingual (NanoCodec) output
|
|
// rate: 22.05 kHz.
|
|
const magpieSampleRate = 22050
|
|
|
|
// magpieChannels is the fixed output layout: mono.
|
|
const magpieChannels = 1
|
|
|
|
// wavHeaderMono returns a 44-byte WAV header for a streaming 16-bit mono PCM
|
|
// stream at 22050 Hz, 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 wavHeaderMono() []byte {
|
|
const blockAlign = magpieChannels * 2 // 16-bit mono
|
|
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(magpieChannels)) // mono
|
|
w(uint32(magpieSampleRate)) // sample rate
|
|
w(uint32(magpieSampleRate * 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.
|
|
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
|
|
}
|
|
|
|
// writeWAVMono writes float samples as a finalized 16-bit mono WAV at
|
|
// 22050 Hz.
|
|
func writeWAVMono(dst string, samples []float32) error {
|
|
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("magpie-tts: create %q: %w", dst, err)
|
|
}
|
|
enc := wav.NewEncoder(f, magpieSampleRate, 16, magpieChannels, 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: magpieChannels, SampleRate: magpieSampleRate},
|
|
Data: ints,
|
|
SourceBitDepth: 16,
|
|
}
|
|
if err := enc.Write(b); err != nil {
|
|
_ = enc.Close()
|
|
_ = f.Close()
|
|
return fmt.Errorf("magpie-tts: encode WAV: %w", err)
|
|
}
|
|
if err := enc.Close(); err != nil {
|
|
_ = f.Close()
|
|
return fmt.Errorf("magpie-tts: finalize WAV: %w", err)
|
|
}
|
|
return f.Close()
|
|
}
|