//go:build dashscope_live package voice import ( "bytes" "context" "encoding/binary" "os" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" ) // TestDashscopeLive 端到端验证真实的阿里百炼 STT + TTS。它 // 被排除在默认测试集之外(构建标签 dashscope_live),使 `go test ./...` // 绝不触碰付费 API。用从 .env 取到的 key 显式运行: // // 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:为一句短文本合成语音。 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 闭环:把合成的语音转写回文字。 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) // 对生成的静音 wav 做 STT,确认空音频路径可被接受。 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) } // 走一遍降级链(网关接线的就是同一个对象),证明它 // 返回真实的字节/文字,而不是降级到静默末端一级。 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 构造 durS 秒的单声道 16 位 16 kHz 静音 WAV。 func silentWAVBytes(durS, sampleRate int) []byte { dataLen := durS * sampleRate * 2 buf := &bytes.Buffer{} // RIFF 头 + fmt 块 + data 块。 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)) // 单声道 _ = binary.Write(buf, binary.LittleEndian, uint32(sampleRate)) _ = binary.Write(buf, binary.LittleEndian, uint32(sampleRate*2)) _ = binary.Write(buf, binary.LittleEndian, uint16(2)) // 块对齐 _ = binary.Write(buf, binary.LittleEndian, uint16(16)) // 每样本位数 buf.WriteString("data") _ = binary.Write(buf, binary.LittleEndian, uint32(dataLen)) buf.Write(make([]byte, dataLen)) return buf.Bytes() }