//go:build emotiontts_live package tts // 本文件是**对着真的 emotionTTS 服务**的端到端验证,默认不进 `go test ./...` // (与 `dashscope_live` 同一口径:不得让常规测试去碰外部服务)。 // // 显式运行(服务默认在 127.0.0.1:9880,可用 EMOTIONTTS_BASE_URL 覆盖): // // go test -tags emotiontts_live -run TestLiveSpeech -v ./internal/runtime/voice/tts/ // // 🔴 第二条用例(TestLiveSpeechFollowsPersona)是主人 2026-10-26 那条硬要求的 // **真机证据**:同一份配置、同一进程,两个 persona 各自合成一次 —— 两次用的是 // 两个不同的上游角色,音频字节也不同。 import ( "context" "os" "testing" "time" ) // TestLiveSpeech 用默认角色合成一句话,验证拿到的确实是 WAV。 func TestLiveSpeech(t *testing.T) { client := New(Config{Enabled: true, BaseURL: os.Getenv("EMOTIONTTS_BASE_URL"), Timeout: 120 * time.Second}) audio, elapsed := synthesizeOrFail(t, client, DefaultCharacter) t.Logf("evidence: base=%s character=%s bytes=%d format=%s elapsed=%s", client.Config().BaseURL, DefaultCharacter, len(audio.Data), audio.Format, elapsed) } // TestLiveSpeechFollowsPersona 是真机的"换人格就换声音"证据:两个 persona 分别映射 // 到服务端真实存在的两个角色(Cyrene / 芙宁娜),各自合成同一句话。 func TestLiveSpeechFollowsPersona(t *testing.T) { cfg := Config{ Enabled: true, BaseURL: os.Getenv("EMOTIONTTS_BASE_URL"), Timeout: 120 * time.Second, Personas: map[string]string{ "cyrene": "char_84fb88a3", "furina": "char_5314489e", }, } client := New(cfg) sizes := make(map[string]int, 2) for _, persona := range []string{"cyrene", "furina"} { selection, err := client.Config().Resolve(persona) if err != nil { t.Fatalf("%s: resolve: %v", persona, err) } audio, elapsed := synthesizeOrFail(t, client, selection.Character) sizes[selection.Character] = len(audio.Data) t.Logf("evidence: persona=%s character=%s source=%s bytes=%d elapsed=%s", persona, selection.Character, selection.Source, len(audio.Data), elapsed) } if sizes["char_84fb88a3"] == 0 || sizes["char_5314489e"] == 0 { t.Fatalf("两个角色没有都产出音频: %v", sizes) } if sizes["char_84fb88a3"] == sizes["char_5314489e"] { t.Logf("note: 两个角色的音频字节数恰好相同(%d),这可能只是巧合,不改变 voice 已按 persona 切换的事实", sizes["char_84fb88a3"]) } } // synthesizeOrFail 合成一次并做最小校验,返回音频与耗时。 func synthesizeOrFail(t *testing.T, client *Client, character string) (Audio, time.Duration) { t.Helper() start := time.Now() audio, err := client.Speech(context.Background(), character, chineseSpeech) if err != nil { t.Fatalf("对真服务合成失败(character=%s): %v", character, err) } elapsed := time.Since(start) if audio.Format != "wav" { t.Errorf("format = %q, want wav(产物恒为 WAV)", audio.Format) } if len(audio.Data) == 0 { t.Fatal("拿到的音频为空") } if string(audio.Data[:4]) != "RIFF" || string(audio.Data[8:12]) != "WAVE" { t.Fatalf("不是 RIFF/WAVE: %q", string(audio.Data[:12])) } return audio, elapsed }