package stt import ( "context" "encoding/base64" "encoding/json" "io" "net/http" "net/http/httptest" "testing" "go.uber.org/zap" ) func newTestMiMoService(handler http.HandlerFunc) (*MiMoService, *httptest.Server) { srv := httptest.NewServer(handler) s := NewMiMoService("test-key", "mimo-v2.5-asr", srv.URL, zap.NewNop().Sugar()) return s, srv } func TestMiMoService_Recognize_Success(t *testing.T) { s, srv := newTestMiMoService(func(w http.ResponseWriter, r *http.Request) { // 验证请求 if r.Header.Get("Authorization") != "Bearer test-key" { t.Errorf("expected Authorization Bearer test-key, got %s", r.Header.Get("Authorization")) } if r.URL.Path != "/chat/completions" { t.Errorf("expected path /chat/completions, got %s", r.URL.Path) } var req mimoRequest body, _ := io.ReadAll(r.Body) if err := json.Unmarshal(body, &req); err != nil { t.Fatalf("unmarshal request: %v", err) } if req.Model != "mimo-v2.5-asr" { t.Errorf("expected model mimo-v2.5-asr, got %s", req.Model) } if len(req.Messages) == 0 || req.Messages[0].Role != "user" { t.Error("expected user message") } if req.ASROptions == nil || req.ASROptions.Language != "zh" { t.Errorf("expected language zh, got %v", req.ASROptions) } resp := mimoResponse{ Choices: []struct { Message struct { Content string `json:"content"` } `json:"message"` }{ {Message: struct { Content string `json:"content"` }{Content: "你好世界"}}, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) defer srv.Close() // 发送一个简单的有效 WAV(44 字节头 + 少量 PCM) wav := makeValidWAV([]byte{0x00, 0x00, 0x00, 0x00}) text, err := s.Recognize(context.Background(), wav, Options{Language: "zh-CN"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if text != "你好世界" { t.Errorf("expected '你好世界', got '%s'", text) } } func TestMiMoService_Recognize_EmptyAudio(t *testing.T) { s, srv := newTestMiMoService(func(w http.ResponseWriter, r *http.Request) {}) defer srv.Close() _, err := s.Recognize(context.Background(), nil, Options{}) if err == nil { t.Fatal("expected error for empty audio") } } func TestMiMoService_Recognize_ServerError(t *testing.T) { s, srv := newTestMiMoService(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("internal error")) }) defer srv.Close() wav := makeValidWAV([]byte{0x00, 0x00}) _, err := s.Recognize(context.Background(), wav, Options{}) if err == nil { t.Fatal("expected error for 500 response") } } func TestMiMoService_Recognize_EmptyChoices(t *testing.T) { s, srv := newTestMiMoService(func(w http.ResponseWriter, r *http.Request) { resp := mimoResponse{Choices: nil} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) defer srv.Close() wav := makeValidWAV([]byte{0x00, 0x00}) _, err := s.Recognize(context.Background(), wav, Options{}) if err == nil { t.Fatal("expected error for empty choices") } } func TestMiMoService_Recognize_PCMAutoWrap(t *testing.T) { // 测试原始 PCM 数据自动封装为 WAV s, srv := newTestMiMoService(func(w http.ResponseWriter, r *http.Request) { var req mimoRequest body, _ := io.ReadAll(r.Body) if err := json.Unmarshal(body, &req); err != nil { t.Fatalf("unmarshal request: %v", err) } // 验证 data URL 格式 if len(req.Messages) == 0 || len(req.Messages[0].Content) == 0 { t.Fatal("empty message content") } dataURL := req.Messages[0].Content[0].InputAudio.Data if len(dataURL) < 22 || dataURL[:14] != "data:audio/wav" { t.Errorf("expected wav data URL, got prefix: %s", dataURL[:min(len(dataURL), 30)]) } // 验证 base64 可解码 b64Part := dataURL[22:] // skip "data:audio/wav;base64," decoded, err := base64.StdEncoding.DecodeString(b64Part) if err != nil { t.Fatalf("base64 decode failed: %v", err) } // 应该是有效 WAV(RIFF 头) if len(decoded) < 44 || string(decoded[:4]) != "RIFF" { t.Error("decoded data is not a valid WAV") } resp := mimoResponse{ Choices: []struct { Message struct { Content string `json:"content"` } `json:"message"` }{ {Message: struct { Content string `json:"content"` }{Content: "test"}}, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) defer srv.Close() // 发送原始 PCM(非 WAV/MP3) pcm := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07} text, err := s.Recognize(context.Background(), pcm, Options{SampleRate: 16000}) if err != nil { t.Fatalf("unexpected error: %v", err) } if text != "test" { t.Errorf("expected 'test', got '%s'", text) } } func TestMapLanguage(t *testing.T) { tests := []struct { input string want string }{ {"", "auto"}, {"zh-CN", "zh"}, {"zh", "zh"}, {"en-US", "en"}, {"en", "en"}, {"ja", "auto"}, } for _, tt := range tests { got := mapLanguage(tt.input) if got != tt.want { t.Errorf("mapLanguage(%q) = %q, want %q", tt.input, got, tt.want) } } } func TestIsWAV(t *testing.T) { if !isWAV([]byte("RIFF....")) { t.Error("expected true for RIFF header") } if isWAV([]byte("ID3...")) { t.Error("expected false for ID3 header") } if isWAV([]byte{0x00}) { t.Error("expected false for short data") } } func TestIsMP3(t *testing.T) { if !isMP3([]byte("ID3\x03")) { t.Error("expected true for ID3 header") } if !isMP3([]byte{0xFF, 0xFB, 0x00}) { t.Error("expected true for MP3 sync word") } if isMP3([]byte("RIFF")) { t.Error("expected false for RIFF header") } } func makeValidWAV(pcm []byte) []byte { // 构造一个最小有效 WAV wav := make([]byte, 44+len(pcm)) copy(wav[:4], "RIFF") // little-endian size = 36 + len(pcm) size := uint32(36 + len(pcm)) wav[4] = byte(size) wav[5] = byte(size >> 8) wav[6] = byte(size >> 16) wav[7] = byte(size >> 24) copy(wav[8:12], "WAVE") copy(wav[12:16], "fmt ") // fmt chunk size = 16 wav[16] = 16 // PCM format = 1 wav[20] = 1 // channels = 1 wav[22] = 1 // sample rate = 16000 wav[24] = 0x80 wav[25] = 0x3E // byte rate = 32000 wav[28] = 0x00 wav[29] = 0x7D // block align = 2 wav[32] = 2 // bits per sample = 16 wav[34] = 16 copy(wav[36:40], "data") dSize := uint32(len(pcm)) wav[40] = byte(dSize) wav[41] = byte(dSize >> 8) wav[42] = byte(dSize >> 16) wav[43] = byte(dSize >> 24) copy(wav[44:], pcm) return wav } func min(a, b int) int { if a < b { return a } return b }