Files
AskaEth b0fbb8bc0d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/multimodal(21 文件,纯替换)
- 只翻注释/文档字符串,代码零变化:go/scanner 词法流比对 21 文件 token+字面量全等,增删行数 1:1(467/467)
- 刻意保留非散文英文:JSON Schema description、工具 Description() 返回值、adapter prompt、API 路径与命令示例、环境变量名
- 逐行确认代码前缀未变,未触碰其他模块
2026-10-04 12:28:16 +08:00

152 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 != ""
}