package voice import ( "context" "fmt" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" ) // Chain 是一条降级提供方列表。STT 与 TTS 按序走各级, // 返回第一个成功的结果;某级不可用(方向为 nil)或 // 失败时则移向下一级。它实现 STT 与 TTS, // 因此可用在任何期望单个提供方的地方。 type Chain struct { steps []Provider cloner VoiceCloner } // NewChain 按给定提供方的尝试顺序构造一条降级链。 // 把 SilentProvider 放在最后,可保证 // 这条链总是成功。 func NewChain(steps ...Provider) *Chain { return &Chain{steps: steps} } // STT 走链取一次转写。 func (c *Chain) STT(ctx context.Context, audio Audio) (string, error) { text, _, err := c.STTWithProvider(ctx, audio) return text, err } // STTWithProvider 即 STT,但额外报告产出该转写的提供方 // 级别名(cloud / local / edge-tts / silent),使调用方能 // 展示实际是哪一级服务了该请求。 func (c *Chain) STTWithProvider(ctx context.Context, audio Audio) (string, string, error) { if len(c.steps) == 0 { return "", "", ErrProviderFailed } var lastErr error for _, p := range c.steps { stt := p.STT() if stt == nil { continue } text, err := stt.STT(ctx, audio) if err == nil { return text, p.Name(), nil } lastErr = err } return "", "", fmt.Errorf("voice: %w: %v", ErrProviderFailed, lastErr) } // CloneVoice 经云端提供方的音色注册 endpoint 创建自定义音色 // (声音复刻 create_voice)。没有云端一级的链返回 // ErrNotConfigured。它不是降级步骤 —— 调用方只拿到一个结果。 func (c *Chain) CloneVoice(ctx context.Context, referenceURL, prefix string) (string, error) { if c.cloner == nil { return "", ErrNotConfigured } return c.cloner.CloneVoice(ctx, referenceURL, prefix) } // TTS 走链取合成音频。 func (c *Chain) TTS(ctx context.Context, text string) (Audio, error) { audio, _, err := c.TTSWithProvider(ctx, text) return audio, err } // TTSWithProvider 即 TTS,但额外报告产出该音频的提供方 // 级别名。 func (c *Chain) TTSWithProvider(ctx context.Context, text string) (Audio, string, error) { if len(c.steps) == 0 { return Audio{}, "", ErrProviderFailed } var lastErr error for _, p := range c.steps { tts := p.TTS() if tts == nil { continue } audio, err := tts.TTS(ctx, text) if err == nil { return audio, p.Name(), nil } lastErr = err } return Audio{}, "", fmt.Errorf("voice: %w: %v", ErrProviderFailed, lastErr) } // Option 定制由 New 构造的链。 type Option func(*chainConfig) // chainConfig 承载 New 组装出的各级。 type chainConfig struct { steps []Provider } // WithProviders 覆盖由配置组装出的提供方各级。测试用它 // 注入确定性的 mock 提供方,而不是真实的适配点 // 各级。 func WithProviders(steps ...Provider) Option { return func(c *chainConfig) { c.steps = steps } } // New 用固定降级顺序从 cfg 组装一条链: // cloud → local → edge-tts/espeak → silent。 // // - 仅当存在云端语音凭据或模型(voiceConfigured)时才纳入 // cloud,使配置有误的构建安静降级。 // - local 是纯适配点,仅当经 WithProviders 提供时才纳入; // 默认跳过。 // - 依 cfg.TTSProvider 纳入 edge-tts 或 espeak 一级。 // - silent 始终追加在最后,使链绝不会直接失败。 // // opts 可提供一份完整组装好的各级列表来覆盖上述映射。 func New(cfg config.VoiceConfig, opts ...Option) *Chain { conf := &chainConfig{} for _, o := range opts { o(conf) } if len(conf.steps) > 0 { return NewChain(conf.steps...) } steps := make([]Provider, 0, 5) var cloner VoiceCloner if voiceConfigured(cfg) { steps = append(steps, CloudProvider(newDashboardSTT(cfg), newDashboardTTS(cfg))) cloner = newDashboardCloner(cfg) } switch cfg.TTSProvider { case "edge-tts", "edge": steps = append(steps, EdgeTTSProvider(newEdgeTTS())) case "espeak": steps = append(steps, EspeakProvider(newEspeak())) } steps = append(steps, SilentProvider()) chain := NewChain(steps...) chain.cloner = cloner return chain } // voiceConfigured 报告是否设置了任一云端语音凭据或模型,使 // 云端一级只在运行时有意使用 DashScope 时才加入。任一字段 // 存在(API key、base URL 或某个 STT/TTS 模型)都让云端 // 一级值得一试;随后缺失的 key 会让适配器返回 ErrNotConfigured, // 链则安静降级。 func voiceConfigured(cfg config.VoiceConfig) bool { return cfg.DashscopeAPIKey != "" || cfg.DashscopeBaseURL != "" || cfg.STTModel != "" || cfg.TTSModel != "" }