Files
Cyrene_v2/internal/services/gateway/audio_test.go
T
AskaEth 074ffc4ebf
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(gateway): gateway 剩余文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;gateway 至此整包注释中文化完成(本会话共 3 批:
已推库 16 个文件 + 本批剩余文件)。
校验:机械校验(非注释改动行)= 空;剥注释后代码文本逐行比对全等(不一致文件数 0);
全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
2026-10-03 22:28:25 +08:00

235 lines
7.1 KiB
Go

package gateway
import (
"context"
"encoding/base64"
"net/http"
"strings"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/multimodal/voice"
)
// audioStubVoice 是一个 VoiceService,为 REST /api/v1/audio/* handler
// 测试返回固定的 STT/TTS 结果。
type audioStubVoice struct {
sttText string
sttErr error
ttsAudio voice.Audio
ttsErr error
gotAudio voice.Audio
gotText string
sttCalled bool
ttsCalled bool
cloneVoiceID string
cloneErr error
cloneCalled bool
gotCloneURL string
gotClonePfx string
gotInstr string
}
func (s *audioStubVoice) STT(ctx context.Context, audio voice.Audio) (string, error) {
s.sttCalled = true
s.gotAudio = audio
return s.sttText, s.sttErr
}
func (s *audioStubVoice) TTS(ctx context.Context, text string) (voice.Audio, error) {
s.ttsCalled = true
s.gotText = text
s.gotInstr = voice.TTSInstructionFromContext(ctx)
return s.ttsAudio, s.ttsErr
}
func (s *audioStubVoice) CloneVoice(ctx context.Context, referenceURL, prefix string) (string, error) {
s.cloneCalled = true
s.gotCloneURL = referenceURL
s.gotClonePfx = prefix
return s.cloneVoiceID, s.cloneErr
}
func TestHandleTranscribe(t *testing.T) {
env := newEnv(t, nil, false)
sv := &audioStubVoice{sttText: "你好世界"}
env.srv.SetVoice(sv)
token := env.token(t, auth.RoleOwner)
resp, body := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/audio/transcribe", map[string]any{
"audio": base64.StdEncoding.EncodeToString([]byte("raw")),
"format": "wav",
}, token)
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d, body = %s", resp.StatusCode, body)
}
if !sv.sttCalled {
t.Fatal("STT not called")
}
if string(sv.gotAudio.Data) != "raw" || sv.gotAudio.Format != "wav" {
t.Errorf("audio = %+v, want format wav / data raw", sv.gotAudio)
}
env2 := decodeBody(t, body)
if got := string(env2["data"]); got == "" {
t.Errorf("data envelope empty: %s", body)
}
}
func TestHandleTranscribe_AuthRequired(t *testing.T) {
env := newEnv(t, nil, false)
sv := &audioStubVoice{sttText: "x"}
env.srv.SetVoice(sv)
resp, body := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/audio/transcribe", map[string]any{
"audio": base64.StdEncoding.EncodeToString([]byte("raw")),
}, "")
if resp.StatusCode != http.StatusUnauthorized {
t.Fatalf("status = %d, want 401 (body=%s)", resp.StatusCode, body)
}
}
func TestHandleTranscribe_EmptyAudio(t *testing.T) {
env := newEnv(t, nil, false)
sv := &audioStubVoice{sttText: "x"}
env.srv.SetVoice(sv)
token := env.token(t, auth.RoleOwner)
resp, body := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/audio/transcribe", map[string]any{
"audio": base64.StdEncoding.EncodeToString([]byte("")),
}, token)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("status = %d, want 400 (body=%s)", resp.StatusCode, body)
}
}
func TestHandleSpeech(t *testing.T) {
env := newEnv(t, nil, false)
sv := &audioStubVoice{ttsAudio: voice.Audio{Format: "wav", Data: []byte("audio-bytes")}}
env.srv.SetVoice(sv)
token := env.token(t, auth.RoleOwner)
resp, body := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/audio/speech", map[string]any{
"text": "你好",
}, token)
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d, body = %s", resp.StatusCode, body)
}
if ct := resp.Header.Get("Content-Type"); ct != "audio/wav" {
t.Errorf("Content-Type = %q, want audio/wav", ct)
}
if string(body) != "audio-bytes" {
t.Errorf("body = %q, want audio-bytes", body)
}
if !sv.ttsCalled || sv.gotText != "你好" {
t.Errorf("TTS got text = %q (called=%v), want 你好", sv.gotText, sv.ttsCalled)
}
}
func TestHandleSpeech_EmptyText(t *testing.T) {
env := newEnv(t, nil, false)
env.srv.SetVoice(&audioStubVoice{})
token := env.token(t, auth.RoleOwner)
resp, body := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/audio/speech", map[string]any{
"text": " ",
}, token)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("status = %d, want 400 (body=%s)", resp.StatusCode, body)
}
}
func TestHandleSpeech_InstructionPassthrough(t *testing.T) {
env := newEnv(t, nil, false)
sv := &audioStubVoice{ttsAudio: voice.Audio{Format: "wav", Data: []byte("audio-bytes")}}
env.srv.SetVoice(sv)
token := env.token(t, auth.RoleOwner)
resp, body := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/audio/speech", map[string]any{
"text": "你好,我是昔涟",
"instruction": "语气开朗俏皮,像女朋友撒娇",
}, token)
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d, body = %s", resp.StatusCode, body)
}
if !sv.ttsCalled {
t.Fatal("TTS not called")
}
if sv.gotText != "你好,我是昔涟" {
t.Errorf("TTS text = %q, want 你好,我是昔涟", sv.gotText)
}
if sv.gotInstr != "语气开朗俏皮,像女朋友撒娇" {
t.Errorf("instruction threaded = %q, want the per-request instruction", sv.gotInstr)
}
}
func TestHandleSpeech_NoInstruction(t *testing.T) {
env := newEnv(t, nil, false)
sv := &audioStubVoice{ttsAudio: voice.Audio{Format: "wav", Data: []byte("audio-bytes")}}
env.srv.SetVoice(sv)
token := env.token(t, auth.RoleOwner)
resp, _ := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/audio/speech", map[string]any{
"text": "你好",
}, token)
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
if sv.gotInstr != "" {
t.Errorf("instruction = %q, want empty when not supplied", sv.gotInstr)
}
}
func TestHandleVoiceClone(t *testing.T) {
env := newEnv(t, nil, false)
sv := &audioStubVoice{cloneVoiceID: "cosyvoice-v3-flash-cyrene-abc123"}
env.srv.SetVoice(sv)
token := env.token(t, auth.RoleOwner)
resp, body := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/audio/voice-clone", map[string]any{
"url": "https://example.com/ref.wav",
"prefix": "xilian",
}, token)
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d, body = %s", resp.StatusCode, body)
}
if !sv.cloneCalled {
t.Fatal("CloneVoice not called")
}
if sv.gotCloneURL != "https://example.com/ref.wav" || sv.gotClonePfx != "xilian" {
t.Errorf("clone url/prefix = %q / %q, want the request values", sv.gotCloneURL, sv.gotClonePfx)
}
env2 := decodeBody(t, body)
if got := string(env2["data"]); got == "" {
t.Errorf("data envelope empty: %s", body)
}
if !strings.Contains(string(env2["data"]), "cosyvoice-v3-flash-cyrene-abc123") {
t.Errorf("voiceId missing: %s", body)
}
}
func TestHandleVoiceClone_AuthRequired(t *testing.T) {
env := newEnv(t, nil, false)
env.srv.SetVoice(&audioStubVoice{})
resp, body := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/audio/voice-clone", map[string]any{
"url": "https://example.com/ref.wav",
}, "")
if resp.StatusCode != http.StatusUnauthorized {
t.Fatalf("status = %d, want 401 (body=%s)", resp.StatusCode, body)
}
}
func TestHandleVoiceClone_EmptyURL(t *testing.T) {
env := newEnv(t, nil, false)
env.srv.SetVoice(&audioStubVoice{})
token := env.token(t, auth.RoleOwner)
resp, body := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/audio/voice-clone", map[string]any{
"url": " ",
}, token)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("status = %d, want 400 (body=%s)", resp.StatusCode, body)
}
}