- 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
97 lines
3.3 KiB
Go
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()
|
|
}
|