- voice/dashscope.go 新建:DashscopeSTT(qwen-audio-3.0-asr-flash, base64 data URI→text) + DashscopeTTS(cosyvoice 按 voice_id, 取非流式 24h URL 下载字节)
- cloud.go newDashboardSTT/TTS 由空 endpoint stub 改为真实百炼 adapter
- chain.go gating 从 DashscopeAPIKey!="" 改为 voiceConfigured(cfg); silent 兜底保留
- config.VoiceConfig 扩展 DASHSCOPE_BASE_URL/VOICE_STT_*/VOICE_TTS_* 字段(必填 DASHSCOPE_API_KEY)
- gateway: POST /api/v1/audio/{transcribe,speech} (owner+审计+MaxBytesReader); voiceInputRequest 增 deviceId 并透传 runner.Run
- 实测: cosyvoice-v3.5-plus+longxiaochun 回 418(引擎拒绝), 改用 cosyvoice-v3-flash+longxiaochun_v3; TTS 你好我是昔涟->106124B wav -> STT 回 你好我是西联, 链返回真实字节/文本
- 单测: dashscope_test/config voice defaults/gateway audio_test; go build/vet/test 全绿
- 声音复刻(响昔涟音色)待公网参考音频; 不做 create_voice
125 lines
3.6 KiB
Go
125 lines
3.6 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/multimodal/voice"
|
|
)
|
|
|
|
// audioStubVoice is a VoiceService returning fixed STT/TTS results for the
|
|
// REST /api/v1/audio/* handler tests.
|
|
type audioStubVoice struct {
|
|
sttText string
|
|
sttErr error
|
|
ttsAudio voice.Audio
|
|
ttsErr error
|
|
gotAudio voice.Audio
|
|
gotText string
|
|
sttCalled bool
|
|
ttsCalled bool
|
|
}
|
|
|
|
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
|
|
return s.ttsAudio, s.ttsErr
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|