- 只翻注释/文档字符串,代码零变化:go/scanner 词法流比对 21 文件 token+字面量全等,增删行数 1:1(467/467) - 刻意保留非散文英文:JSON Schema description、工具 Description() 返回值、adapter prompt、API 路径与命令示例、环境变量名 - 逐行确认代码前缀未变,未触碰其他模块
187 lines
4.6 KiB
Go
187 lines
4.6 KiB
Go
package voice
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
|
)
|
|
|
|
// stubSTT 是一个返回固定结果的 mock STT。
|
|
type stubSTT struct {
|
|
text string
|
|
err error
|
|
}
|
|
|
|
func (s *stubSTT) STT(_ context.Context, _ Audio) (string, error) { return s.text, s.err }
|
|
|
|
// stubTTS 是一个返回固定结果的 mock TTS。
|
|
type stubTTS struct {
|
|
audio Audio
|
|
err error
|
|
}
|
|
|
|
func (s *stubTTS) TTS(_ context.Context, _ string) (Audio, error) { return s.audio, s.err }
|
|
|
|
// stubProvider 是携带可选方向的 mock Provider。
|
|
type stubProvider struct {
|
|
name string
|
|
stt STT
|
|
tts TTS
|
|
}
|
|
|
|
func (s *stubProvider) Name() string { return s.name }
|
|
func (s *stubProvider) STT() STT { return s.stt }
|
|
func (s *stubProvider) TTS() TTS { return s.tts }
|
|
|
|
func TestChain_STT_Degrades(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
steps []Provider
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "first provider succeeds",
|
|
steps: []Provider{&stubProvider{name: "cloud", stt: &stubSTT{text: "hello"}}},
|
|
want: "hello",
|
|
},
|
|
{
|
|
name: "first fails second succeeds",
|
|
steps: []Provider{
|
|
&stubProvider{name: "cloud", stt: &stubSTT{err: ErrNotConfigured}},
|
|
&stubProvider{name: "local", stt: &stubSTT{text: "degraded"}},
|
|
},
|
|
want: "degraded",
|
|
},
|
|
{
|
|
name: "nil direction skipped",
|
|
steps: []Provider{
|
|
&stubProvider{name: "edge", stt: nil},
|
|
&stubProvider{name: "silent", stt: &stubSTT{text: "silence"}},
|
|
},
|
|
want: "silence",
|
|
},
|
|
{
|
|
name: "all fail no silent -> error",
|
|
steps: []Provider{
|
|
&stubProvider{name: "cloud", stt: &stubSTT{err: ErrNotConfigured}},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "all fail silent last -> no error",
|
|
steps: []Provider{
|
|
&stubProvider{name: "cloud", stt: &stubSTT{err: ErrNotConfigured}},
|
|
SilentProvider(),
|
|
},
|
|
want: "",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got, err := NewChain(tt.steps...).STT(context.Background(), Audio{Format: "wav", Data: []byte("x")})
|
|
if tt.wantErr {
|
|
if !errors.Is(err, ErrProviderFailed) {
|
|
t.Fatalf("err = %v, want ErrProviderFailed", err)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("STT error: %v", err)
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("text = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChain_TTS_Degrades(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
steps []Provider
|
|
want Audio
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "first provider succeeds",
|
|
steps: []Provider{&stubProvider{name: "cloud", tts: &stubTTS{audio: Audio{Format: "mp3", Data: []byte("a")}}}},
|
|
want: Audio{Format: "mp3", Data: []byte("a")},
|
|
},
|
|
{
|
|
name: "first fails second succeeds",
|
|
steps: []Provider{
|
|
&stubProvider{name: "cloud", tts: &stubTTS{err: ErrNotConfigured}},
|
|
&stubProvider{name: "edge-tts", tts: &stubTTS{audio: Audio{Format: "mp3", Data: []byte("b")}}},
|
|
},
|
|
want: Audio{Format: "mp3", Data: []byte("b")},
|
|
},
|
|
{
|
|
name: "all fail no silent -> error",
|
|
steps: []Provider{
|
|
&stubProvider{name: "cloud", tts: &stubTTS{err: ErrNotConfigured}},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got, err := NewChain(tt.steps...).TTS(context.Background(), "hi")
|
|
if tt.wantErr {
|
|
if !errors.Is(err, ErrProviderFailed) {
|
|
t.Fatalf("err = %v, want ErrProviderFailed", err)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("TTS error: %v", err)
|
|
}
|
|
if got.Format != tt.want.Format || string(got.Data) != string(tt.want.Data) {
|
|
t.Errorf("audio = %+v, want %+v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChain_Empty(t *testing.T) {
|
|
t.Parallel()
|
|
if _, err := NewChain().STT(context.Background(), Audio{}); !errors.Is(err, ErrProviderFailed) {
|
|
t.Fatalf("err = %v, want ErrProviderFailed", err)
|
|
}
|
|
}
|
|
|
|
func TestNew_ConfigAssembly(t *testing.T) {
|
|
t.Parallel()
|
|
// 配了云端 key 且 TTS 默认走 edge-tts 时,链从 cloud 开始、
|
|
// 到 silent 结束。
|
|
c := New(config.VoiceConfig{DashscopeAPIKey: "k"})
|
|
names := make([]string, 0, len(c.steps))
|
|
for _, p := range c.steps {
|
|
names = append(names, p.Name())
|
|
}
|
|
if len(names) < 2 || names[len(names)-1] != "silent" {
|
|
t.Fatalf("steps = %v, want to end at silent", names)
|
|
}
|
|
if names[0] != "cloud" {
|
|
t.Errorf("first step = %q, want cloud", names[0])
|
|
}
|
|
|
|
// 没有 key 时跳过云端一级。
|
|
c2 := New(config.VoiceConfig{})
|
|
foundCloud := false
|
|
for _, p := range c2.steps {
|
|
if p.Name() == "cloud" {
|
|
foundCloud = true
|
|
}
|
|
}
|
|
if foundCloud {
|
|
t.Error("cloud step present without an API key")
|
|
}
|
|
}
|