192 lines
5.2 KiB
Go
192 lines
5.2 KiB
Go
|
|
package stt
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"encoding/json"
|
|||
|
|
"net/http"
|
|||
|
|
"net/http/httptest"
|
|||
|
|
"strings"
|
|||
|
|
"testing"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/gorilla/websocket"
|
|||
|
|
"go.uber.org/zap"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var upgrader = websocket.Upgrader{
|
|||
|
|
CheckOrigin: func(r *http.Request) bool { return true },
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// newMockDeepgram 创建模拟 Deepgram WebSocket 服务。
|
|||
|
|
// 返回 httptest.Server 和对应的 ws:// URL。
|
|||
|
|
func newMockDeepgram(t *testing.T, handler func(conn *websocket.Conn)) *httptest.Server {
|
|||
|
|
t.Helper()
|
|||
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Logf("upgrade error: %v", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
defer conn.Close()
|
|||
|
|
handler(conn)
|
|||
|
|
}))
|
|||
|
|
return srv
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// wsToWss 将 http:// 转换为 ws://。
|
|||
|
|
func wsToWss(httpURL string) string {
|
|||
|
|
return "ws" + strings.TrimPrefix(httpURL, "http")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestDeepgramService_Recognize_Success(t *testing.T) {
|
|||
|
|
srv := newMockDeepgram(t, func(conn *websocket.Conn) {
|
|||
|
|
// 读取音频数据
|
|||
|
|
_, _, err := conn.ReadMessage()
|
|||
|
|
if err != nil {
|
|||
|
|
t.Errorf("read audio: %v", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送中间结果(非 final)
|
|||
|
|
intermediate := deepgramResponse{
|
|||
|
|
IsFinal: false,
|
|||
|
|
}
|
|||
|
|
intermediate.Channel.Alternatives = []struct {
|
|||
|
|
Transcript string `json:"transcript"`
|
|||
|
|
Confidence float64 `json:"confidence"`
|
|||
|
|
}{{Transcript: "你好", Confidence: 0.9}}
|
|||
|
|
data, _ := json.Marshal(intermediate)
|
|||
|
|
_ = conn.WriteMessage(websocket.TextMessage, data)
|
|||
|
|
|
|||
|
|
// 发送最终结果
|
|||
|
|
final := deepgramResponse{
|
|||
|
|
IsFinal: true,
|
|||
|
|
}
|
|||
|
|
final.Channel.Alternatives = []struct {
|
|||
|
|
Transcript string `json:"transcript"`
|
|||
|
|
Confidence float64 `json:"confidence"`
|
|||
|
|
}{{Transcript: "你好世界", Confidence: 0.95}}
|
|||
|
|
data, _ = json.Marshal(final)
|
|||
|
|
_ = conn.WriteMessage(websocket.TextMessage, data)
|
|||
|
|
|
|||
|
|
// 等待客户端关闭
|
|||
|
|
_, _, _ = conn.ReadMessage()
|
|||
|
|
})
|
|||
|
|
defer srv.Close()
|
|||
|
|
|
|||
|
|
svc := NewDeepgramService("test-key", wsToWss(srv.URL)+"/v1/listen", zap.NewNop().Sugar())
|
|||
|
|
|
|||
|
|
text, err := svc.Recognize(context.Background(), []byte("fake-pcm-audio"), Options{
|
|||
|
|
Encoding: "pcm_s16le",
|
|||
|
|
SampleRate: 16000,
|
|||
|
|
Language: "zh-CN",
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("Recognize() error: %v", err)
|
|||
|
|
}
|
|||
|
|
if text != "你好世界" {
|
|||
|
|
t.Errorf("Recognize() = %q, want %q", text, "你好世界")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestDeepgramService_Recognize_EmptyAudio(t *testing.T) {
|
|||
|
|
svc := NewDeepgramService("test-key", "ws://localhost", zap.NewNop().Sugar())
|
|||
|
|
_, err := svc.Recognize(context.Background(), nil, Options{})
|
|||
|
|
if err == nil {
|
|||
|
|
t.Fatal("Recognize() with empty audio should return error")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestDeepgramService_Recognize_ConnectError(t *testing.T) {
|
|||
|
|
svc := NewDeepgramService("test-key", "ws://localhost:1", zap.NewNop().Sugar())
|
|||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|||
|
|
defer cancel()
|
|||
|
|
|
|||
|
|
_, err := svc.Recognize(ctx, []byte("audio"), Options{})
|
|||
|
|
if err == nil {
|
|||
|
|
t.Fatal("Recognize() with bad endpoint should return error")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestDeepgramService_Recognize_Timeout(t *testing.T) {
|
|||
|
|
// 模拟一个永不响应的服务端
|
|||
|
|
srv := newMockDeepgram(t, func(conn *websocket.Conn) {
|
|||
|
|
// 读取音频但不发送任何结果,让客户端超时
|
|||
|
|
_, _, _ = conn.ReadMessage()
|
|||
|
|
time.Sleep(10 * time.Second)
|
|||
|
|
})
|
|||
|
|
defer srv.Close()
|
|||
|
|
|
|||
|
|
svc := NewDeepgramService("test-key", wsToWss(srv.URL)+"/v1/listen", zap.NewNop().Sugar())
|
|||
|
|
|
|||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|||
|
|
defer cancel()
|
|||
|
|
|
|||
|
|
_, err := svc.Recognize(ctx, []byte("audio"), Options{})
|
|||
|
|
if err == nil {
|
|||
|
|
t.Fatal("Recognize() should timeout")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestDeepgramService_Recognize_MultipleFinals(t *testing.T) {
|
|||
|
|
srv := newMockDeepgram(t, func(conn *websocket.Conn) {
|
|||
|
|
_, _, _ = conn.ReadMessage()
|
|||
|
|
|
|||
|
|
// 发送多个 final 结果(多句话场景)
|
|||
|
|
for _, text := range []string{"你好", "世界"} {
|
|||
|
|
resp := deepgramResponse{IsFinal: true}
|
|||
|
|
resp.Channel.Alternatives = []struct {
|
|||
|
|
Transcript string `json:"transcript"`
|
|||
|
|
Confidence float64 `json:"confidence"`
|
|||
|
|
}{{Transcript: text, Confidence: 0.9}}
|
|||
|
|
data, _ := json.Marshal(resp)
|
|||
|
|
_ = conn.WriteMessage(websocket.TextMessage, data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_, _, _ = conn.ReadMessage()
|
|||
|
|
})
|
|||
|
|
defer srv.Close()
|
|||
|
|
|
|||
|
|
svc := NewDeepgramService("test-key", wsToWss(srv.URL)+"/v1/listen", zap.NewNop().Sugar())
|
|||
|
|
|
|||
|
|
text, err := svc.Recognize(context.Background(), []byte("audio"), Options{})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("Recognize() error: %v", err)
|
|||
|
|
}
|
|||
|
|
if text != "你好世界" {
|
|||
|
|
t.Errorf("Recognize() = %q, want %q", text, "你好世界")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestDeepgramService_buildURL(t *testing.T) {
|
|||
|
|
svc := NewDeepgramService("key", "wss://api.deepgram.com/v1/listen", zap.NewNop().Sugar())
|
|||
|
|
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
opts Options
|
|||
|
|
want []string // URL 中应包含的参数
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "defaults",
|
|||
|
|
opts: Options{},
|
|||
|
|
want: []string{"encoding=pcm_s16le", "sample_rate=16000", "language=zh-CN"},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "custom",
|
|||
|
|
opts: Options{Encoding: "wav", SampleRate: 44100, Language: "en"},
|
|||
|
|
want: []string{"encoding=wav", "sample_rate=44100", "language=en"},
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
u := svc.buildURL(tt.opts)
|
|||
|
|
for _, param := range tt.want {
|
|||
|
|
if !strings.Contains(u, param) {
|
|||
|
|
t.Errorf("buildURL() = %q, should contain %q", u, param)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|