Files
Cyrene_v2/internal/runtime/voice/tts/provider.go
T
AskaEth 3a2711a7e0 feat(voice): emotionTTS 接成一级发音人(UTF-8 请求体 + 失败回落既有链)
侦察结论(真机 + 上游源码):
- POST /api/analyze_emotion ⇒ {primary,complex,intensity};primary 闭集
  实测 平/喜/惊/哀/怒/厌/惧/急,intensity ∈ Low/Medium/High。
- POST /api/synthesize 收 emo_vector(float[])+emo_alpha,但还要 ref_audio_filename
  且只回 {"audio_url"}(服务端落盘),不适合做运行时直出。
- POST /v1/audio/speech 内部先 LLM 分析情绪 + 匹配参考音,再把情绪向量以
  [EMO:{vector}|{alpha}]base64:... 注入 voice 交给 indexTTS2 ⇒ **情绪确实影响
  语气,但由服务端从待念文本自推**,我方没有可映射的参数。
- 上游把下游载荷的 speed 硬编码 1.0(core/utils/llm_provider.py),客户端
  speed 是空转 ⇒ 本切片不暴露 speed,也不暴露 emotion/tone(不造假开关)。

实现:
- internal/runtime/voice/tts:Client(唯一 HTTP 出口,json.Encoder 产出 UTF-8
  请求体、超时/非 200/非音频/超限各自可分类)+ 字节嗅探庭裁 + Service(emotionTTS
  在前、失败回落既有链,永不上抛成回复失败)+ Provider 适配(可作链的一级)。
- internal/config/runtimeconfig/voicetts.go:$DATA_DIR/config/voice-tts.json
  (enabled/baseUrl/character/format/timeoutSec),零新 .env 键。
- cmd/cyrene/wire_voice_tts.go + wireFrontend 的唯一注入点:两条既有出站口
  (POST /api/v1/audio/speech 与 WS voice_output)同时用上;不新建端点/帧族。
- 严守 voice 入站闭集(不扩 Source/Status),出站音频不挂 voice DTO(塔裁决 B)。

测试:中文 UTF-8 请求体、非 200、超时、非音频、超限、未启用、空文本、嗅探表、
降级回落、未启用透传、两条出站口的字节级实测、面板配置解析、组合根映射;
三条变异(去中文 / 嗅探恒真 / 去掉回落)各自打红。真机 live 用例(build tag
emotiontts_live)实测 278,036 字节 WAV。
2026-10-26 12:46:57 +08:00

48 lines
1.9 KiB
Go
Raw 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 tts
// 本文件把 `*Client` 变成一个**既有语音链的一级**(`mvoice.Provider`)。
//
// 当前组装走的是 `Service`(包在整条链前面,失败回落),不是把本级插进链里
// (`multimodal/voice.New` 组装的各级列表不外露,插进去要先改那条链,超出本切片
// 范围)。提供这个适配是为了让"作为一级"这条路**现在就是通的**:一旦组装方式
// 改成显式列级(`voice.NewChain(...)`),本级可以原样进链,不必重写 HTTP。
import (
"context"
mvoice "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/multimodal/voice"
)
// Provider 是 emotionTTS 作为降级链一级的适配:只提供 TTS,STT 方向返回 nil
// (它不做转写;链遇到 nil 会自动跳过这一级去找下一个能转写的)。
type Provider struct {
client *Client
}
// NewProvider 用客户端构造一级。client 为 nil 时 Name 仍可用,TTS 返回错误。
func NewProvider(client *Client) *Provider { return &Provider{client: client} }
// Name 返回稳定的级别名(降级信号里就显示这个)。
func (p *Provider) Name() string { return ProviderName }
// STT 返回 nil:本一级不提供转写。
func (p *Provider) STT() mvoice.STT { return nil }
// TTS 返回本一级的合成实现。
func (p *Provider) TTS() mvoice.TTS { return ttsAdapter{p.client} }
// ttsAdapter 把 `Client.Speech` 适配成链要求的 `TTS` 形状(`Audio` 类型不同)。
type ttsAdapter struct{ client *Client }
// TTS 合成音频;client 未接线时返回明确错误而不是 panic。
func (a ttsAdapter) TTS(ctx context.Context, text string) (mvoice.Audio, error) {
if a.client == nil {
return mvoice.Audio{}, mvoice.ErrNotConfigured
}
audio, err := a.client.Speech(ctx, text)
if err != nil {
return mvoice.Audio{}, err
}
return mvoice.Audio{Format: audio.Format, Data: audio.Data}, nil
}