From f5563e6f3195cfa11f0e5cf574a625410ee73236 Mon Sep 17 00:00:00 2001 From: "3371392206@qq.com" <3371392206@qq.com> Date: Sun, 30 Aug 2026 01:53:30 +0800 Subject: [PATCH] =?UTF-8?q?fix(tts):=20default=5Fspeaker=20=E6=9F=A5=20voi?= =?UTF-8?q?ce=20=E8=A1=A8,=E6=94=B9=E5=90=8E=E7=AB=8B=E5=8D=B3=E7=94=9F?= =?UTF-8?q?=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 (等用户验证) --- controller/setup.go | 6 ++++++ setting/config.go | 26 +++++++++++++++++++++++++- store/voices.go | 16 ++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/controller/setup.go b/controller/setup.go index 61065ae..765c53a 100644 --- a/controller/setup.go +++ b/controller/setup.go @@ -176,6 +176,12 @@ func SetupSubmitHandler(w http.ResponseWriter, r *http.Request) { setting.Auth.APIKeys = []string{authKey} } + // 装完 reload TTS 全局配置(让 TTSOptions 立即有可用的 api_key/speaker/resource_id, + // 否则 /v1/audio/speech 会因为 TTSConfigErr 在启动期被设而返 503,要重启才生效)。 + if err := setting.LoadRuntimeConfig(GetSetupStore()); err != nil { + log.Printf("[setup] warning: 装完 LoadRuntimeConfig 失败: %v(下次启动会恢复)", err) + } + // 清空旧 voices 再插入(假设是首次安装;若不是,name 冲突会变成 409) // 这里选择 "清空+插入" 语义,符合"setup 是首次安装"的产品定位 // 如果想保留旧 voices,可以改成 UPSERT,但 M1 不做 diff --git a/setting/config.go b/setting/config.go index 98ae302..1712f00 100644 --- a/setting/config.go +++ b/setting/config.go @@ -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 { diff --git a/store/voices.go b/store/voices.go index 6a25d02..152fa05 100644 --- a/store/voices.go +++ b/store/voices.go @@ -98,6 +98,22 @@ func (s *Store) VoiceGetByName(name string) (*Voice, error) { return &v, nil } +// GetVoiceForTTS 实现 setting.Store 接口,返 voice 行的 TTS 关键字段。 +// - found=false: voice 不存在(ErrNotFound 翻译为 found=false) +// - err != nil: 真错误(db 失败等) +// 这个方法存在是为了让 *Store 满足 setting.Store 接口,且不引起 +// setting → store → setting 循环 import。 +func (s *Store) GetVoiceForTTS(name string) (speaker, resourceID, model string, found bool, err error) { + v, err := s.VoiceGetByName(name) + if err != nil { + if errors.Is(err, ErrNotFound) { + return "", "", "", false, nil + } + return "", "", "", false, err + } + return v.Speaker, v.ResourceID, v.Model, true, nil +} + // VoiceInsert 新增音色;name 冲突返回 ErrDuplicate。 // 空字符串/格式不合法返回 error;不依赖 SQLite 约束作为唯一校验。 func (s *Store) VoiceInsert(v Voice) (int64, error) {