- 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.
135 lines
4.5 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|