Files
CamTalk/backend/internal/ai/stt/mimo_test.go

256 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, 0, 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()
// 发送一个简单的有效 WAV44 字节头 + 少量 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)
}
// 应该是有效 WAVRIFF 头)
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
}