- 只翻注释/文档字符串,代码零变化:go/scanner 词法流比对 21 文件 token+字面量全等,增删行数 1:1(467/467) - 刻意保留非散文英文:JSON Schema description、工具 Description() 返回值、adapter prompt、API 路径与命令示例、环境变量名 - 逐行确认代码前缀未变,未触碰其他模块
152 lines
4.6 KiB
Go
152 lines
4.6 KiB
Go
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 != ""
|
||
}
|