fix(tts): default_speaker 查 voice 表,改后立即生效

Bug 链:

1. 【核心 bug】setting/config.go LoadRuntimeConfig 把
   "default_speaker" 当成"火山 speaker ID"用 — 但 DB 里存的是
   voice **名字**(如 "chun"),不是 speaker ID (如 "S_G8tEKnaJ1")。
   结果: TTSOptions.Speaker = "chun", TTSOptions.ResourceID 仍来自
   default_resource_id 配置。调 /v1/audio/speech 不传 voice 时,
   把 "chun" 当 speaker ID 发给火山 → 55000000 resource ID mismatched。

2. 【次生 bug】装完 install 没 reload TTS 配置。
   启动期 LoadRuntimeConfig 失败(无 lock 无 settings), TTSConfigErr 被设;
   装完 mode 变 normal 但 TTSConfigErr 没清, /v1/audio/speech 仍 503/500
   (要重启才生效)。

修法:
- setting/config.go: 拿到 default_speaker 后调 GetVoiceForTTS(name)
  查 voice 表, 用 voice.Speaker / ResourceID / Model 覆盖。
  (新增 store.Store interface 方法 GetVoiceForTTS,4 元组返回值
  避免循环 import;store 包加 GetVoiceForTTS 实现,内部包 VoiceGetByName)
- controller/setup.go: 装完 install 调 LoadRuntimeConfig 一次,
  让 TTSConfigErr 立即清,TTSOptions 立即可用。

测试:
- 已有 store/installer/middleware/router 测试全绿
- e2e (本机): install voice "chun" + speaker "S_G8tEKnaJ1" →
  启动 log 显示 "speaker=S_G8tEKnaJ1" (不是 "chun") ✓
  → /v1/audio/speech 不传 voice → 火山收到正确 speaker,
  错误从 55000000 变成 401 Invalid X-Api-Key (假 key,换真 key 就 work)

二进制大小: ~18MB → ~18MB (代码量不变)

未 push (等用户验证)
This commit is contained in:
sun
2026-08-30 01:53:30 +08:00
parent d22abecf12
commit f5563e6f31
3 changed files with 47 additions and 1 deletions
+25 -1
View File
@@ -153,7 +153,7 @@ func splitAndLowerOrigins(s string) []string {
// LoadRuntimeConfig 从 store 加载 TTS 全局配置到 TTSOptions / TTSTimeout / Auth.APIKeys 内存。
// 启动期(master 模式)调一次,或 PUT /api/settings 后调一次(改完立即生效)。
//
////
// 与原 InitTTSConfig 的区别:
// - 不再读 BYTEDANCE_TTS_* env;全部从 store.Settings 读
// - 必填项(api_key / default_resource_id / default_speaker)缺失时返 error
@@ -202,7 +202,22 @@ func LoadRuntimeConfig(s Store) error {
return TTSConfigErr
}
// 【BUG 修复】default_speaker 是 voice **名字**(如 "chun"),
// 不是火山 speaker ID (如 "S_G8tEKnaJ1")。原代码直接把 voice 名当
// speaker ID 用,导致调 /v1/audio/speech 不传 voice 时火山 55000000。
// 这里查 voice 表拿真正的 speaker / resource_id / model。
var voiceModel string
if vSpeaker, vResource, vModel, found, vErr := s.GetVoiceForTTS(speaker); vErr == nil && found {
speaker = vSpeaker
resourceId = vResource
voiceModel = vModel
// 找不到 voice 时不报错 — 保持原值(向后兼容)
}
model := all["model"]
if voiceModel != "" {
model = voiceModel // voice 行有 model 时优先用 voice 的
}
format := all["default_format"]
if format == "" {
format = "mp3"
@@ -290,11 +305,20 @@ func LoadRuntimeConfig(s Store) error {
}
// Store 是 LoadRuntimeConfig 需要的最小接口(避免 setting 包 import store 产生 cycle)。
//
// 重要:VoiceGetByName 用 4 个返回值(speaker/resourceID/model/found/err)
// 而非 (VoiceRef, error),这样 store 包不需要 import setting 包
// 就能实现这个接口(避免循环 import)。
type Store interface {
SettingsGetAll() (map[string]string, error)
SettingsGetInt(key string, def int) (int, error)
SettingsGetBool(key string, def bool) (bool, error)
SettingsGetDuration(key string, def time.Duration) (time.Duration, error)
// GetVoiceForTTS 给定 voice 名字,返 (speaker_id, resource_id, model, found, err)。
// - found=false 表示 voice 不存在(此时 err=nil,返回值是空串)
// - err!=nil 是真错误(db 失败等)
// - 找到时返 voice 行的真实字段值
GetVoiceForTTS(name string) (speaker, resourceID, model string, found bool, err error)
}
func getEnvDefault(name, def string) string {