Nik Afiq 5238298b55
Some checks failed
CI / test (push) Successful in 6s
CI / build-ai-gateway (push) Failing after 28s
CI / build-ha-gateway (push) Failing after 24s
CI / build-discord-bot (push) Failing after 29s
Add TTS model components and inference server
- Implemented core model components in `modules.py` including various convolutional layers and normalization techniques.
- Added transformation functions in `transforms.py` for piecewise rational quadratic transformations.
- Created utility functions in `utils.py` for checkpoint management, logging, and hyperparameter handling.
- Introduced monotonic alignment functionality with Cython optimization in `monotonic_align`.
- Developed a minimal inference server in `server.py` to handle synthesis requests.
- Updated requirements to include necessary dependencies for Cython and scipy.
2026-07-24 22:38:36 +09:00

135 lines
4.5 KiB
Go

package app
import (
"context"
"errors"
"testing"
"gitea.nik4nao.com/nik/home-services/tts-gateway/internal/core/domain"
)
type mockNormalizer struct {
normalizeFunc func(ctx context.Context, text string) ([]int64, error)
}
func (m *mockNormalizer) Normalize(ctx context.Context, text string) ([]int64, error) {
return m.normalizeFunc(ctx, text)
}
type mockEngine struct {
synthesizeFunc func(ctx context.Context, speakerID int, symbolIDs []int64, params domain.SynthesisParams) ([]float32, int, error)
}
func (m *mockEngine) Synthesize(ctx context.Context, speakerID int, symbolIDs []int64, params domain.SynthesisParams) ([]float32, int, error) {
return m.synthesizeFunc(ctx, speakerID, symbolIDs, params)
}
type mockEncoder struct {
encodeFunc func(ctx context.Context, pcm []float32, sampleRate int) ([]byte, string, error)
}
func (m *mockEncoder) Encode(ctx context.Context, pcm []float32, sampleRate int) ([]byte, string, error) {
return m.encodeFunc(ctx, pcm, sampleRate)
}
func TestTTSAppSynthesize(t *testing.T) {
t.Run("unknown speaker returns ErrSpeakerNotFound without calling downstream ports", func(t *testing.T) {
a := NewTTSApp(
&mockNormalizer{normalizeFunc: func(context.Context, string) ([]int64, error) {
t.Fatal("Normalize should not be called for an unknown speaker")
return nil, nil
}},
&mockEngine{},
&mockEncoder{},
)
_, err := a.Synthesize(context.Background(), "Not A Real Speaker", "hello", domain.SynthesisParams{})
if !errors.Is(err, ErrSpeakerNotFound) {
t.Fatalf("Synthesize() error = %v, want %v", err, ErrSpeakerNotFound)
}
})
t.Run("happy path wires normalize -> engine -> encode", func(t *testing.T) {
wantParams := domain.SynthesisParams{NoiseScale: 0.1, NoiseScaleW: 0.2, LengthScale: 0.3}
wantSymbolIDs := []int64{1, 2, 3}
wantPCM := []float32{0.5, -0.5}
wantSampleRate := 22050
wantAudio := []byte{1, 2, 3, 4}
wantMimeType := "audio/aac"
a := NewTTSApp(
&mockNormalizer{normalizeFunc: func(_ context.Context, text string) ([]int64, error) {
if text != "hello" {
t.Fatalf("Normalize() text = %q, want %q", text, "hello")
}
return wantSymbolIDs, nil
}},
&mockEngine{synthesizeFunc: func(_ context.Context, speakerID int, symbolIDs []int64, params domain.SynthesisParams) ([]float32, int, error) {
if speakerID != 29 {
t.Fatalf("Synthesize() speakerID = %d, want 29 (Rice Shower)", speakerID)
}
if len(symbolIDs) != len(wantSymbolIDs) {
t.Fatalf("Synthesize() symbolIDs = %v, want %v", symbolIDs, wantSymbolIDs)
}
if params != wantParams {
t.Fatalf("Synthesize() params = %v, want %v", params, wantParams)
}
return wantPCM, wantSampleRate, nil
}},
&mockEncoder{encodeFunc: func(_ context.Context, pcm []float32, sampleRate int) ([]byte, string, error) {
if sampleRate != wantSampleRate {
t.Fatalf("Encode() sampleRate = %d, want %d", sampleRate, wantSampleRate)
}
return wantAudio, wantMimeType, nil
}},
)
got, err := a.Synthesize(context.Background(), "Rice Shower", "hello", wantParams)
if err != nil {
t.Fatalf("Synthesize() error = %v", err)
}
if string(got.Data) != string(wantAudio) || got.MimeType != wantMimeType {
t.Fatalf("Synthesize() = %+v, want Data=%v MimeType=%q", got, wantAudio, wantMimeType)
}
})
t.Run("propagates normalizer error", func(t *testing.T) {
wantErr := errors.New("normalize failed")
a := NewTTSApp(
&mockNormalizer{normalizeFunc: func(context.Context, string) ([]int64, error) { return nil, wantErr }},
&mockEngine{},
&mockEncoder{},
)
_, err := a.Synthesize(context.Background(), "Rice Shower", "hello", domain.SynthesisParams{})
if !errors.Is(err, wantErr) {
t.Fatalf("Synthesize() error = %v, want %v", err, wantErr)
}
})
}
func TestTTSAppListSpeakers(t *testing.T) {
a := NewTTSApp(&mockNormalizer{}, &mockEngine{}, &mockEncoder{})
t.Run("empty search returns full roster", func(t *testing.T) {
got := a.ListSpeakers(context.Background(), "")
if len(got) != len(domain.Speakers) {
t.Fatalf("ListSpeakers(\"\") returned %d names, want %d", len(got), len(domain.Speakers))
}
})
t.Run("case-insensitive substring filter", func(t *testing.T) {
got := a.ListSpeakers(context.Background(), "rice")
if len(got) != 1 || got[0] != "Rice Shower" {
t.Fatalf("ListSpeakers(\"rice\") = %v, want [\"Rice Shower\"]", got)
}
})
t.Run("no match returns empty, not nil roster", func(t *testing.T) {
got := a.ListSpeakers(context.Background(), "definitely not a speaker")
if len(got) != 0 {
t.Fatalf("ListSpeakers() = %v, want empty", got)
}
})
}