256 lines
6.4 KiB
Go
256 lines
6.4 KiB
Go
|
|
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("api-key") != "test-key" {
|
|||
|
|
t.Errorf("expected api-key test-key, got %s", r.Header.Get("api-key"))
|
|||
|
|
}
|
|||
|
|
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
|
|||
|
|
}
|