125 lines
4.3 KiB
Go
125 lines
4.3 KiB
Go
package ai
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/coder/websocket"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestBailianModelsCompleteLocalOpeningASRLLMTTSAndKeywordFlow(t *testing.T) {
|
|
for _, keyword := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("keyword=%t", keyword), func(t *testing.T) {
|
|
bound := bailianFixture(t)
|
|
var asrCalls, ttsCalls, llmCalls, hangups atomic.Int32
|
|
transcript := "请继续说明"
|
|
if keyword {
|
|
transcript = "我不用了"
|
|
}
|
|
expected := []byte{1, 0, 2, 0}
|
|
speech := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Authorization") != "Bearer mock-key" {
|
|
t.Error("wrong speech connection credential")
|
|
}
|
|
c, err := websocket.Accept(w, r, nil)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
defer c.CloseNow()
|
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
|
defer cancel()
|
|
cmd := mockRead(t, c, ctx)
|
|
mockEvent(c, ctx, cmd.Header.TaskID, "task-started", nil)
|
|
switch cmd.Payload["task"] {
|
|
case "asr":
|
|
asrCalls.Add(1)
|
|
if cmd.Payload["model"] != "fun-asr-flash-8k-realtime-2026-01-28" {
|
|
t.Error("task ASR model changed")
|
|
}
|
|
for {
|
|
kind, _, err := c.Read(ctx)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
if kind == websocket.MessageText {
|
|
break
|
|
}
|
|
}
|
|
mockEvent(c, ctx, cmd.Header.TaskID, "result-generated", map[string]any{"output": map[string]any{"sentence": map[string]any{"text": transcript, "sentence_end": true, "begin_time": 0, "end_time": 100}}})
|
|
case "tts":
|
|
ttsCalls.Add(1)
|
|
if cmd.Payload["model"] != "cosyvoice-v3-flash" {
|
|
t.Error("task TTS model changed")
|
|
}
|
|
text := mockRead(t, c, ctx)
|
|
finish := mockRead(t, c, ctx)
|
|
if text.Header.Action != "continue-task" || finish.Header.Action != "finish-task" {
|
|
t.Error("invalid TTS task lifecycle")
|
|
}
|
|
c.Write(ctx, websocket.MessageBinary, expected)
|
|
default:
|
|
t.Error("unknown speech task")
|
|
return
|
|
}
|
|
mockEvent(c, ctx, cmd.Header.TaskID, "task-finished", nil)
|
|
}))
|
|
defer speech.Close()
|
|
llm := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
llmCalls.Add(1)
|
|
var request map[string]any
|
|
if r.Method != "POST" || r.Header.Get("Authorization") != "Bearer mock-key" || json.NewDecoder(r.Body).Decode(&request) != nil {
|
|
t.Error("invalid LLM connection/request")
|
|
}
|
|
if request["model"] != "Qwen3.8-Flash" {
|
|
t.Error("task LLM model changed")
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprint(w, `{"id":"mock","object":"chat.completion","created":0,"model":"Qwen3.8-Flash","choices":[{"index":0,"message":{"role":"assistant","content":"模拟回复。"},"finish_reason":"stop"}]}`)
|
|
}))
|
|
defer llm.Close()
|
|
bound.ASR.Provider.Endpoint = "ws" + strings.TrimPrefix(speech.URL, "http")
|
|
bound.TTS.Provider.WSEndpoint = bound.ASR.Provider.Endpoint
|
|
bound.LLM.Provider.Endpoint = llm.URL + "/v1"
|
|
call, err := NewCall(bound, func(context.Context) error { hangups.Add(1); return nil })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
opening, err := call.Open(ctx)
|
|
if err != nil || !bytes.Equal(opening, expected) {
|
|
t.Fatalf("opening: %v", err)
|
|
}
|
|
turn, err := call.RunTurn(ctx, make([]byte, 3200))
|
|
if err != nil || turn.Transcript != transcript || !bytes.Equal(turn.AudioPCM16, expected) || turn.EndedByKeyword != keyword {
|
|
t.Fatalf("turn: %+v %v", turn, err)
|
|
}
|
|
if keyword {
|
|
if llmCalls.Load() != 0 || hangups.Load() != 0 {
|
|
t.Fatal("keyword must skip LLM and wait for closing playback before hangup")
|
|
}
|
|
if err = call.FinishKeyword(ctx); err != nil || hangups.Load() != 1 {
|
|
t.Fatalf("finish keyword: %v", err)
|
|
}
|
|
if _, err = call.RunTurn(ctx, make([]byte, 3200)); err == nil {
|
|
t.Fatal("keyword turn was replayed")
|
|
}
|
|
} else if llmCalls.Load() != 1 || turn.Reply != "模拟回复。" {
|
|
t.Fatal("normal reply did not pass through selected LLM")
|
|
}
|
|
if asrCalls.Load() != 1 || ttsCalls.Load() != 2 {
|
|
t.Fatalf("unexpected/repeated provider calls: asr=%d tts=%d", asrCalls.Load(), ttsCalls.Load())
|
|
}
|
|
})
|
|
}
|
|
}
|