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
This commit is contained in:
1 parent
d8044763ac
commit
73dabb9dde
13 files changed
+1070
-15
No files matched your search
@@ -97,11 +97,35 @@ type IoTConfig struct {
|
||||
Provider string `env:"IOT_PROVIDER" default:"mock"`
|
||||
}
|
||||
|
||||
// VoiceConfig holds TTS/STT provider settings.
|
||||
// VoiceConfig holds TTS/STT provider settings and the Alibaba Bailian DashScope
|
||||
// credentials/models that back the cloud rung of the voice chain.
|
||||
type VoiceConfig struct {
|
||||
TTSProvider string `env:"TTS_PROVIDER" default:"edge-tts"`
|
||||
STTProvider string `env:"STT_PROVIDER" default:"dashscope"`
|
||||
// TTSProvider selects the non-cloud local TTS rung: "edge-tts" or "espeak".
|
||||
TTSProvider string `env:"TTS_PROVIDER" default:"edge-tts"`
|
||||
// STTProvider selects the cloud STT provider ("dashscope").
|
||||
STTProvider string `env:"STT_PROVIDER" default:"dashscope"`
|
||||
// DashscopeAPIKey is the Bailian API key backing STT + TTS. When empty the
|
||||
// dashscope adapters return ErrNotConfigured and the chain degrades.
|
||||
DashscopeAPIKey string `env:"DASHSCOPE_API_KEY"`
|
||||
|
||||
// DashscopeBaseURL is the Bailian API base; the STT/TTS service paths are
|
||||
// appended to it.
|
||||
DashscopeBaseURL string `env:"DASHSCOPE_BASE_URL" default:"https://dashscope.aliyuncs.com/api/v1"`
|
||||
|
||||
// STTModel is the DashScope speech-transcription model.
|
||||
STTModel string `env:"VOICE_STT_MODEL" default:"qwen-audio-3.0-asr-flash"`
|
||||
STTLanguage string `env:"VOICE_STT_LANGUAGE" default:"zh"`
|
||||
|
||||
// TTSModel is the DashScope speech-synthesis model. cosyvoice-v3.5-plus and
|
||||
// cosyvoice-v2 reject the HTTP SpeechSynthesizer call (engine 418); verified
|
||||
// working is cosyvoice-v3-flash.
|
||||
TTSModel string `env:"VOICE_TTS_MODEL" default:"cosyvoice-v3-flash"`
|
||||
// TTSVoiceID selects the synthesized timbre. Voices are model-scoped (a v3
|
||||
// model needs a _v3 voice). After cloning a custom voice in Bailian
|
||||
// (音色复刻) fill its voice_id here.
|
||||
TTSVoiceID string `env:"VOICE_TTS_VOICE_ID" default:"longxiaochun_v3"`
|
||||
TTSFormat string `env:"VOICE_TTS_FORMAT" default:"wav"`
|
||||
TTSSampleRate int `env:"VOICE_TTS_SAMPLE_RATE" default:"24000"`
|
||||
}
|
||||
|
||||
// BackgroundConfig holds the autonomous thinking settings. QuietStartHour and
|
||||
|
||||
@@ -162,6 +162,53 @@ func TestLoad_CapacityEnvOverrides(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_VoiceDefaults(t *testing.T) {
|
||||
t.Setenv("JWT_SECRET", strings.Repeat("s", 40))
|
||||
|
||||
cfg, err := Load("")
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error: %v", err)
|
||||
}
|
||||
if cfg.Voice.DashscopeBaseURL != "https://dashscope.aliyuncs.com/api/v1" {
|
||||
t.Errorf("DashscopeBaseURL = %q, want the default Bailian URL", cfg.Voice.DashscopeBaseURL)
|
||||
}
|
||||
if cfg.Voice.STTModel != "qwen-audio-3.0-asr-flash" {
|
||||
t.Errorf("STTModel = %q, want qwen-audio-3.0-asr-flash", cfg.Voice.STTModel)
|
||||
}
|
||||
if cfg.Voice.STTLanguage != "zh" {
|
||||
t.Errorf("STTLanguage = %q, want zh", cfg.Voice.STTLanguage)
|
||||
}
|
||||
if cfg.Voice.TTSModel != "cosyvoice-v3-flash" {
|
||||
t.Errorf("TTSModel = %q, want cosyvoice-v3-flash", cfg.Voice.TTSModel)
|
||||
}
|
||||
if cfg.Voice.TTSVoiceID != "longxiaochun_v3" {
|
||||
t.Errorf("TTSVoiceID = %q, want longxiaochun_v3", cfg.Voice.TTSVoiceID)
|
||||
}
|
||||
if cfg.Voice.TTSFormat != "wav" {
|
||||
t.Errorf("TTSFormat = %q, want wav", cfg.Voice.TTSFormat)
|
||||
}
|
||||
if cfg.Voice.TTSSampleRate != 24000 {
|
||||
t.Errorf("TTSSampleRate = %d, want 24000", cfg.Voice.TTSSampleRate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_VoiceEnvOverrides(t *testing.T) {
|
||||
t.Setenv("JWT_SECRET", strings.Repeat("s", 40))
|
||||
t.Setenv("VOICE_TTS_VOICE_ID", "custom-voice-1")
|
||||
t.Setenv("VOICE_STT_MODEL", "qwen-audio-3.0-asr-flash")
|
||||
|
||||
cfg, err := Load("")
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error: %v", err)
|
||||
}
|
||||
if cfg.Voice.TTSVoiceID != "custom-voice-1" {
|
||||
t.Errorf("TTSVoiceID = %q, want custom-voice-1", cfg.Voice.TTSVoiceID)
|
||||
}
|
||||
if cfg.Voice.STTModel != "qwen-audio-3.0-asr-flash" {
|
||||
t.Errorf("STTModel = %q, want qwen-audio-3.0-asr-flash", cfg.Voice.STTModel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDotEnv_QuotedAndExport(t *testing.T) {
|
||||
path := writeEnvFile(t, `export FOO=bar
|
||||
BAZ='quoted value'
|
||||
|
||||
Reference in new issue
Block a user