package voice import ( "context" "io" "net/http" "net/http/httptest" "strings" "testing" ) func TestNewServiceDefaultConfig(t *testing.T) { svc := New(Config{TTSProviders: []string{"silent"}}) if svc.sttProvider == nil { t.Fatal("expected STT provider") } if svc.sttProvider.Name() != "silent" { t.Errorf("expected silent STT, got %s", svc.sttProvider.Name()) } } func TestNewServiceEmptyConfig(t *testing.T) { svc := New(Config{}) if svc.sttProvider == nil { t.Fatal("expected fallback STT provider") } if len(svc.ttsProviders) == 0 { t.Fatal("expected at least one fallback TTS provider") } if got := svc.TTSProviderNames(); len(got) != 1 || got[0] != "silent" { t.Errorf("expected [silent] chain, got %v", got) } } func TestServiceRecognize(t *testing.T) { svc := New(Config{}) result, err := svc.Recognize(context.Background(), []byte("test audio")) if err != nil { t.Errorf("Recognize should not error: %v", err) } if result != "" { t.Logf("Recognize result: %s", result) } } func TestServiceSynthesize(t *testing.T) { svc := New(Config{TTSProviders: []string{"silent"}}) audio, err := svc.Synthesize(context.Background(), "你好世界", "zh-CN-XiaoxiaoNeural") if err != nil { t.Fatalf("Synthesize should not error: %v", err) } if audio == nil { t.Error("expected non-nil audio") } } func TestTTSProviderChainSkipsUnavailable(t *testing.T) { // edge-tts/espeak binaries are usually absent in CI — they must be // skipped, leaving at least the silent provider. svc := New(Config{TTSProviders: []string{"edge-tts", "espeak", "silent"}}) names := svc.TTSProviderNames() if names[len(names)-1] != "silent" { t.Errorf("chain must end with silent, got %v", names) } for _, n := range names { if n == "edge-tts" && newEdgeTTS().bin == "" { t.Error("edge-tts in chain but binary not found") } } } func TestProviderNames(t *testing.T) { cases := map[string]TTSProvider{ "cloud": &cloudTTS{}, "local": &localTTS{}, "edge-tts": &edgeTTS{}, "espeak": &espeakTTS{}, "silent": &silentTTS{}, } for want, p := range cases { if p.Name() != want { t.Errorf("provider name = %s, want %s", p.Name(), want) } } if n := (STTProvider)(&whisperSTT{}).Name(); n != "whisper-api" { t.Errorf("stt name = %s", n) } if n := (&silentSTT{}).Name(); n != "silent" { t.Errorf("stt name = %s", n) } } func TestCloudTTS(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/audio/speech" { t.Errorf("unexpected path: %s", r.URL.Path) } body, _ := io.ReadAll(r.Body) if !strings.Contains(string(body), "cosyvoice-v1") || !strings.Contains(string(body), "你好") { t.Errorf("unexpected body: %s", body) } w.Write([]byte("fake-mp3-bytes")) })) defer srv.Close() p := &cloudTTS{baseURL: srv.URL, apiKey: "k", model: "cosyvoice-v1", defaultVoice: "longxiaochun", client: srv.Client()} audio, err := p.Synthesize(context.Background(), "你好", "") if err != nil { t.Fatalf("Synthesize: %v", err) } if string(audio) != "fake-mp3-bytes" { t.Errorf("audio = %q", audio) } } func TestLocalTTS(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("local-audio")) })) defer srv.Close() p := &localTTS{url: srv.URL, client: srv.Client()} audio, err := p.Synthesize(context.Background(), "文本", "") if err != nil || string(audio) != "local-audio" { t.Errorf("audio=%q err=%v", audio, err) } } func TestWhisperSTT(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/audio/transcriptions" { t.Errorf("unexpected path: %s", r.URL.Path) } if err := r.ParseMultipartForm(1 << 20); err != nil { t.Errorf("parse multipart: %v", err) } w.Write([]byte(`{"text":"识别结果"}`)) })) defer srv.Close() p := &whisperSTT{baseURL: srv.URL, apiKey: "k", model: "whisper-large-v3", client: srv.Client()} text, err := p.Recognize(context.Background(), []byte("RIFF....fake-wav")) if err != nil || text != "识别结果" { t.Errorf("text=%q err=%v", text, err) } } func TestFallbackChain(t *testing.T) { // First provider unreachable, second (silent) succeeds svc := &Service{ttsProviders: []TTSProvider{ &localTTS{url: "http://127.0.0.1:1", client: &http.Client{}}, &silentTTS{}, }} audio, err := svc.Synthesize(context.Background(), "text", "") if err != nil || len(audio) != 0 { t.Errorf("expected silent fallback empty audio, got %d bytes, err=%v", len(audio), err) } }