Files
Cyrene_v2/internal/runtime/multimodal/voice/dashscope_live_test.go
T
Aska 73dabb9dde
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(voice): 阿里百炼 STT/TTS 主链路接通(base64 STT + cosyvoice TTS按voice_id + REST /audio/* + voice_input.deviceId)
- 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
2026-09-08 06:06:02 +08:00

97 lines
3.3 KiB
Go

//go:build dashscope_live
package voice
import (
"bytes"
"context"
"encoding/binary"
"os"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
)
// TestDashscopeLive verifies the real Alibaba Bailian STT + TTS end to end. It is
// excluded from the default suite (build tag dashscope_live) so `go test ./...`
// never hits the paid API. Run it explicitly with the key sourced from .env:
//
// set -a; source .env; set +a
// go test -tags dashscope_live -run TestDashscopeLive -v ./internal/runtime/multimodal/voice/
func TestDashscopeLive(t *testing.T) {
key := os.Getenv("DASHSCOPE_API_KEY")
if key == "" {
t.Skip("DASHSCOPE_API_KEY not set")
}
cfg := config.VoiceConfig{DashscopeAPIKey: key}
// TTS: synthesize speech for a short line.
tts := NewDashscopeTTS(cfg)
audio, err := tts.TTS(context.Background(), "你好,我是昔涟")
if err != nil {
t.Fatalf("TTS error: %v", err)
}
t.Logf("TTS: format=%q bytes=%d", audio.Format, len(audio.Data))
if len(audio.Data) == 0 {
t.Fatal("TTS returned empty audio")
}
// STT closed loop: transcribe the synthesized speech back to text.
stt := NewDashscopeSTT(cfg)
text, err := stt.STT(context.Background(), Audio{Format: "wav", Data: audio.Data})
if err != nil {
t.Fatalf("STT error: %v", err)
}
t.Logf("STT (of TTS audio): text=%q", text)
// STT on a generated silence wav, to confirm the empty-audio path is accepted.
silence := silentWAVBytes(1, 16000)
sttText, sttErr := stt.STT(context.Background(), Audio{Format: "wav", Data: silence})
if sttErr != nil {
t.Logf("STT (silence): error=%v", sttErr)
} else {
t.Logf("STT (silence): text=%q", sttText)
}
// Exercise the degrading chain (the same object the gateway wires), proving it
// returns real bytes/text rather than degrading to the silent terminal rung.
ch := New(cfg)
chTTS, chTTSErr := ch.TTS(context.Background(), "你好,我是昔涟")
if chTTSErr != nil {
t.Fatalf("chain TTS error: %v", chTTSErr)
}
t.Logf("chain TTS: format=%q bytes=%d", chTTS.Format, len(chTTS.Data))
if len(chTTS.Data) == 0 {
t.Fatal("chain TTS degraded to empty (silent) audio")
}
chText, chSTTErr := ch.STT(context.Background(), Audio{Format: "wav", Data: chTTS.Data})
if chSTTErr != nil {
t.Fatalf("chain STT error: %v", chSTTErr)
}
t.Logf("chain STT: text=%q", chText)
if chText == "" {
t.Fatal("chain STT degraded to empty (silent) text")
}
}
// silentWAVBytes builds a mono 16-bit 16 kHz silence WAV of durS seconds.
func silentWAVBytes(durS, sampleRate int) []byte {
dataLen := durS * sampleRate * 2
buf := &bytes.Buffer{}
// RIFF header + fmt chunk + data chunk.
buf.WriteString("RIFF")
_ = binary.Write(buf, binary.LittleEndian, uint32(36+dataLen))
buf.WriteString("WAVEfmt ")
_ = binary.Write(buf, binary.LittleEndian, uint32(16))
_ = binary.Write(buf, binary.LittleEndian, uint16(1)) // PCM
_ = binary.Write(buf, binary.LittleEndian, uint16(1)) // mono
_ = binary.Write(buf, binary.LittleEndian, uint32(sampleRate))
_ = binary.Write(buf, binary.LittleEndian, uint32(sampleRate*2))
_ = binary.Write(buf, binary.LittleEndian, uint16(2)) // block align
_ = binary.Write(buf, binary.LittleEndian, uint16(16)) // bits per sample
buf.WriteString("data")
_ = binary.Write(buf, binary.LittleEndian, uint32(dataLen))
buf.Write(make([]byte, dataLen))
return buf.Bytes()
}