package tts import ( "context" "testing" "github.com/kami/maven/internal/audio" "github.com/kami/maven/internal/worker" ) type fakeSynthesizer struct{} func (fakeSynthesizer) Synthesize(_ context.Context, req worker.SynthesizeReq) (worker.SynthesizeResp, error) { if req.Text == "" { return worker.SynthesizeResp{}, nil } pcm := make([]byte, 3200) for i := 0; i < len(pcm)/2; i++ { v := int16(i) pcm[i*2] = byte(v) pcm[i*2+1] = byte(v >> 8) } return worker.SynthesizeResp{Audio: audio.Audio{Format: audio.PCM16kMono, Bytes: pcm}}, nil } func TestStubProducesValidAudio(t *testing.T) { t.Parallel() s := NewStub() a, err := s.Synthesize(context.Background(), "hi") if err != nil { t.Fatalf("Stub.Synthesize: %v", err) } if !a.Format.IsValid() { t.Fatalf("format invalid: %+v", a.Format) } // 200ms @ 16k ⇒ 3200 samples ⇒ 6400 bytes. if len(a.Bytes) != 6400 { t.Fatalf("bytes len: %d, want 6400", len(a.Bytes)) } // non-empty audio should not be all zeros (sine wave). allZero := true for _, b := range a.Bytes { if b != 0 { allZero = false break } } if allZero { t.Fatalf("Stub audio is all zeros; should be a tone") } if a.Duration() < 0.19 || a.Duration() > 0.21 { t.Fatalf("Duration: got %v, want ~0.2s", a.Duration()) } } func TestStubDifferentTextDifferentFreq(t *testing.T) { t.Parallel() s := NewStub() a, _ := s.Synthesize(context.Background(), "a") // 'a' = 97 b, _ := s.Synthesize(context.Background(), "z") // 'z' = 122 // different first byte ⇒ different modulo ⇒ different tones; bytes differ. if samePCM(a.Bytes, b.Bytes) { t.Fatalf("expected different tones for 'a' vs 'z'") } } func TestStubEmptyTextStillTones(t *testing.T) { t.Parallel() s := NewStub() a, err := s.Synthesize(context.Background(), "") if err != nil { t.Fatalf("Stub.Synthesize: %v", err) } if len(a.Bytes) == 0 { t.Fatalf("empty text should still produce tone (audible), got silence") } } func TestRemoteForwardsToClient(t *testing.T) { t.Parallel() srv := worker.NewSynthesizerServer(t.TempDir()+"/tts.sock", fakeSynthesizer{}) if err := srv.Listen(); err != nil { t.Fatalf("listen: %v", err) } defer srv.Close() go srv.Serve() c := worker.Dial(srv.Path()) defer c.Close() r := NewRemote(c, "ru", "") out, err := r.Synthesize(context.Background(), "hello") if err != nil { t.Fatalf("Remote.Synthesize: %v", err) } if !out.Format.IsValid() { t.Fatalf("invalid format: %+v", out.Format) } if len(out.Bytes) != 3200 { t.Fatalf("bytes len: %d, want 3200", len(out.Bytes)) } } func TestRemoteErrorWraps(t *testing.T) { t.Parallel() // a transcriber-only server ⇒ ErrUnknownMethod on Synthesize. srv := worker.NewServer(t.TempDir()+"/tts.sock", &fakeErrTranscriber{}) if err := srv.Listen(); err != nil { t.Fatalf("listen: %v", err) } defer srv.Close() go srv.Serve() c := worker.Dial(srv.Path()) defer c.Close() r := NewRemote(c, "ru", "") _, err := r.Synthesize(context.Background(), "hello") if err == nil { t.Fatalf("want error, got nil") } } type fakeErrTranscriber struct{} func (fakeErrTranscriber) Transcribe(context.Context, worker.TranscribeReq) (worker.TranscribeResp, error) { return worker.TranscribeResp{}, nil } func samePCM(a, b []byte) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true }