侦察结论(真机 + 上游源码):
- 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。
48 lines
1.9 KiB
Go
48 lines
1.9 KiB
Go
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
|
||
}
|