mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-02 12:26:49 -04:00
* feat(supertonic): vendor upstream Go TTS pipeline (helper.go) Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(supertonic): add gRPC backend (Load/TTS/TTSStream, CPU) Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(supertonic): satisfy unused linter (use onnxProvider; exclude vendored helper.go) Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * test(supertonic): unit tests for resolvers + gated end-to-end synthesis Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * style(supertonic): gofmt backend.go comment block Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(supertonic): add Makefile, run.sh, package.sh (CPU build) Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * build(supertonic): wire backend into root Makefile Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(supertonic): check ort.DestroyEnvironment return (errcheck) Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(supertonic): resolve voice_styles as sibling of onnx dir; guard trim; test voice Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(supertonic): add CPU build matrix + gallery index entries Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(supertonic): expose as pref-only importable backend Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(supertonic): add Supertonic/supertonic-3 TTS model to the gallery 16 files (4 onnx + tts.json + unicode_indexer.json + 10 voice styles) from HF Supertone/supertonic-3, served via the supertonic backend. Defaults to voice F1; onnx/ + sibling voice_styles/ layout matches the backend's resolveVoicesDir. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(meta): register pipeline.max_history_items config field Pre-existing on master: the field was added without a registry entry, failing TestAllFieldsHaveRegistryEntries (core/config/meta). Add the entry so it renders properly in the model-config UI. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * ci(secscan): exclude vendored supertonic backend from gosec helper.go is vendored from supertone-inc/supertonic; its G304/G404/G104 findings are inherent to upstream and the math/rand use is correct for flow-matching noise (crypto/rand would be wrong). 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>
87 lines
3.0 KiB
Go
87 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
|
|
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
|
|
)
|
|
|
|
var _ = Describe("voiceStylePath", func() {
|
|
s := &SupertonicBackend{modelDir: "/models/st/onnx", voicesDir: "/models/st/voice_styles"}
|
|
|
|
It("resolves a bare name under the resolved voicesDir", func() {
|
|
Expect(s.voiceStylePath("M1")).To(Equal(filepath.Join("/models/st/voice_styles", "M1.json")))
|
|
})
|
|
It("keeps an explicit .json suffix", func() {
|
|
Expect(s.voiceStylePath("M1.json")).To(Equal(filepath.Join("/models/st/voice_styles", "M1.json")))
|
|
})
|
|
It("honors absolute paths", func() {
|
|
Expect(s.voiceStylePath("/abs/v.json")).To(Equal("/abs/v.json"))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("resolveVoicesDir", func() {
|
|
It("prefers voice_styles under modelDir", func() {
|
|
dir := GinkgoT().TempDir()
|
|
Expect(os.MkdirAll(filepath.Join(dir, "voice_styles"), 0o755)).To(Succeed())
|
|
Expect(resolveVoicesDir(dir)).To(Equal(filepath.Join(dir, "voice_styles")))
|
|
})
|
|
It("falls back to the sibling voice_styles next to an onnx subdir", func() {
|
|
root := GinkgoT().TempDir()
|
|
Expect(os.MkdirAll(filepath.Join(root, "voice_styles"), 0o755)).To(Succeed())
|
|
Expect(os.MkdirAll(filepath.Join(root, "onnx"), 0o755)).To(Succeed())
|
|
Expect(resolveVoicesDir(filepath.Join(root, "onnx"))).To(Equal(filepath.Join(root, "voice_styles")))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("resolveLang", func() {
|
|
It("accepts a valid request language", func() {
|
|
s := &SupertonicBackend{defaultLang: "na"}
|
|
Expect(s.resolveLang("ko")).To(Equal("ko"))
|
|
})
|
|
It("falls back to the model default for an invalid language", func() {
|
|
s := &SupertonicBackend{defaultLang: "en"}
|
|
Expect(s.resolveLang("zz")).To(Equal("en"))
|
|
})
|
|
It("falls back to na when nothing is valid", func() {
|
|
s := &SupertonicBackend{defaultLang: ""}
|
|
Expect(s.resolveLang("")).To(Equal("na"))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("pcmFloatToInt16LE", func() {
|
|
It("clamps and encodes little-endian", func() {
|
|
out := pcmFloatToInt16LE([]float32{0, 1.0, -1.0, 2.0})
|
|
Expect(out).To(HaveLen(8))
|
|
Expect(out[0:2]).To(Equal([]byte{0x00, 0x00})) // 0
|
|
Expect(out[2:4]).To(Equal([]byte{0xff, 0x7f})) // 32767
|
|
Expect(out[6:8]).To(Equal([]byte{0xff, 0x7f})) // clamp 2.0 -> 32767
|
|
})
|
|
})
|
|
|
|
var _ = Describe("end-to-end synthesis", Ordered, func() {
|
|
var modelDir string
|
|
BeforeAll(func() {
|
|
modelDir = os.Getenv("SUPERTONIC_MODEL_PATH")
|
|
if modelDir == "" {
|
|
Skip("set SUPERTONIC_MODEL_PATH to a supertonic model dir to run")
|
|
}
|
|
Expect(InitializeONNXRuntime()).To(Succeed())
|
|
})
|
|
|
|
It("synthesizes a wav file", func() {
|
|
b := &SupertonicBackend{}
|
|
Expect(b.Load(&pb.ModelOptions{ModelFile: modelDir, Options: []string{"supertonic.default_voice=F1"}})).To(Succeed())
|
|
dst := filepath.Join(GinkgoT().TempDir(), "out.wav")
|
|
lang := "en"
|
|
Expect(b.TTS(&pb.TTSRequest{Text: "Hello from LocalAI.", Dst: dst, Language: &lang})).To(Succeed())
|
|
info, err := os.Stat(dst)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(info.Size()).To(BeNumerically(">", 44)) // header + PCM
|
|
})
|
|
})
|