4 Commits
Author SHA1 Message Date
sun 3f18f85925 fix(ui): setup 默认 resource_id 改 seed-icl-2.0
与 admin 同一类问题 (见 admin 修复 commit):
项目是豆包声音复刻大模型 2.0, 资源 ID 必为 seed-icl-2.0。

setup.html 错的默认值有 5 处:
1. form.default_resource_id placeholder: volc.megatts.default
2. v.resource_id placeholder: volc.megatts.icl
3. v.model placeholder: seed-tts-2.0-standard
4. form 默认 (default_resource_id): volc.megatts.default
5. form 默认 voices[0] + addVoice push: volc.megatts.icl / seed-tts-2.0-standard

修法同 admin: resource_id 改 seed-icl-2.0, model 保持 seed-tts-2.0-standard
(用户后澄清模型名独立)。

附带修:
- step 2 hint 文案从 "volc.megatts.icl 用于复刻音色" 改为
  "本项目为豆包声音复刻大模型 2.0, 固定使用 seed-icl-2.0"
  (避免误导用户填错资源 ID)

未 push (待用户 push)
2026-08-30 15:57:00 +08:00
sun 155017a27b fix(ui): admin 默认 resource_id 改 seed-icl-2.0
背景: 项目是豆包声音复刻大模型 2.0, v3 API 只允许 2 个
资源 ID: seed-tts-2.0 (普通 TTS) + seed-icl-2.0 (复刻 2.0)。
本项目用复刻 2.0, 资源 ID 必为 seed-icl-2.0。

Bug 报告: admin (admin.html) 表单有 5 处默认值都是错的:
1. settingsForm.default_resource_id placeholder: volc.megatts.default
2. form.resource_id placeholder: volc.megatts.icl
3. form.model placeholder: seed-tts-2.0-standard (1.0 模型, 项目用不了)
4. form resource_id 默认: volc.megatts.icl
5. form model 默认: seed-tts-2.0-standard
   + openAdd 重置
   + settingsForm.model placeholder: seed-tts-2.0-standard

volc.megatts.* 是 1.0 API 的资源/服务品类编码, 不是 v3 API 2.0 的合法
资源 ID, 装完 admin 一看就是错的, 用户不知道要改成什么。

修法:
- 所有 resource_id 默认值 + placeholder 改成 seed-icl-2.0
- model 默认值保持 seed-tts-2.0-standard (用户确认模型名)
  (用户后澄清: 模型在 v3 API 2.0 里只此一个名字, 不跟 resource 同步)
- 不硬编码 — 用户仍可改

受影响的 7 处:
- admin.html: settingsForm.default_resource_id placeholder
- admin.html: settingsForm.model placeholder
- admin.html: form.resource_id placeholder
- admin.html: form.model placeholder
- admin.html: form 默认 (resource_id)
- admin.html: form 默认 (model) + openAdd reset
- 全部 7 处 detail 见 diff

未 push (待用户 push)
2026-08-30 15:57:00 +08:00
sun ad4727c507 fix(tts): voice 命中 log 跟合成 log 一致打码
Bug 报告: 同一次请求, controller/tts.go:193 输出明文 speaker
  [tts] voice=chun 命中 (speaker=S_G8tEKnaJ1 ...)

但 adapter/volcano/synthesis.go:148 输出打码
  TTS 合成成功 - 音色=S_G8****naJ1

不一致 — 同一进程同一请求, 一处裸奔一处打码, 脱裤子放屁。

修法: 全部用 telemetry.MaskSpeaker (前4+****+后4)。
不动 telemetry.SpeakerLabel (sha1[:8] 不可逆哈希, /metrics 用, 保留)。

注意: server log 仍能看到带前缀/后缀的 speaker (e.g. S_G8****naJ1),
但 *完整* speaker 不可见, 防 log 泄漏给非 root 用户。
如果 log 路径对外可读, 仍需考虑更严的方案 (e.g. 整体不 log speaker),
但自托管单用户场景下, 这个折中 OK。

未 push (待用户 push)
2026-08-30 15:56:59 +08:00
sun 65247b095a ux(setup): voice.resource_id 留空时自动用 settings.default_resource_id
问题: step 2 有 "默认资源 ID" 字段,但 step 3 的每个 voice 行
也有 "资源 ID" 字段,要求用户填 2 次。用户经常只在 step 2 填,
留空 step 3 的 resource_id,导致 voice 行拿到 form placeholder
"volc.megatts.icl" 或空串。结果 settings 和 voice 不一致,
运行时火山 500(mismatch / not granted)。

修法 (后端兜底,无需前端改动):
1. 装时校验放宽: resource_id 留空是允许的(原来是必填)
2. 装时遍历 voices, 如果 voice.resource_id == "" 就用
   settings.default_resource_id 兜底, 并记 log

用户收益: step 3 voice 行的 resource_id 字段可以不动,
step 2 填一次就行,始终保持一致。

可选前端优化 (待前端做): step 2 的 "默认资源 ID" 改变时,
自动同步所有 voice 行的 resource_id (placeholder 提示已自动填充)。

e2e 验证 (本机):
- 提交 install body 中 voice[0].resource_id=""
- 启动 log: '[setup] voice "chun" resource_id 留空,自动用 default_resource_id="seed-icl-2.0"'
- dumpdb 看 voice chun.resource_id = seed-icl-2.0 (从 settings 兜底) ✓

二进制: ~18MB → ~18MB (代码量几乎不变)

未 push (待用户确认)
2026-08-30 14:07:03 +08:00
5 changed files with 31 additions and 15 deletions
+6
View File
@@ -27,6 +27,12 @@ TODO.md
# Release artifacts (build output) # Release artifacts (build output)
release/ release/
# Local build output (start-local.ps1 builds to bin/)
bin/
# Local dev scripts (per-user; not part of repo)
start-local.ps1
# Runtime database artifacts (created by M1 installer flow) # Runtime database artifacts (created by M1 installer flow)
tts.db tts.db
tts.db-* tts.db-*
+14 -4
View File
@@ -190,11 +190,21 @@ func SetupSubmitHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("[setup] 检测到 %d 条已存在 voices,本次将跳过清空(name 冲突由 ErrDuplicate 处理)", len(existing)) log.Printf("[setup] 检测到 %d 条已存在 voices,本次将跳过清空(name 冲突由 ErrDuplicate 处理)", len(existing))
} }
inserted := 0 inserted := 0
// 【UX 改进】voice 行的 resource_id 留空时,自动用 settings.default_resource_id 兜底。
// 用户在 step 2 填了 default_resource_id 后,step 3 的 voice 行 resource_id
// 可以不填 — 保持一致。否则会出现 "settings 里 seed-icl-2.0,voice 里 volc.megatts.icl"
// 这种 mismatch,运行时 500。
defaultResourceID := settingsKV["default_resource_id"]
for _, v := range body.Voices { for _, v := range body.Voices {
voiceResourceID := v.ResourceID
if voiceResourceID == "" {
voiceResourceID = defaultResourceID
log.Printf("[setup] voice %q resource_id 留空,自动用 default_resource_id=%q", v.Name, defaultResourceID)
}
_, err := s.VoiceInsert(store.Voice{ _, err := s.VoiceInsert(store.Voice{
Name: v.Name, Name: v.Name,
Speaker: v.Speaker, Speaker: v.Speaker,
ResourceID: v.ResourceID, ResourceID: voiceResourceID,
Model: v.Model, Model: v.Model,
Language: v.Language, Language: v.Language,
Enabled: true, Enabled: true,
@@ -267,9 +277,9 @@ func validateSetupVoices(vs []SetupVoice) error {
if strings.TrimSpace(v.Speaker) == "" { if strings.TrimSpace(v.Speaker) == "" {
return fmt.Errorf("voices[%d] (%s): speaker is required", i, v.Name) return fmt.Errorf("voices[%d] (%s): speaker is required", i, v.Name)
} }
if strings.TrimSpace(v.ResourceID) == "" { // 【UX 改进】resource_id 留空是允许的 — 装时(下面那个循环)会自动用
return fmt.Errorf("voices[%d] (%s): resource_id is required", i, v.Name) // settings.default_resource_id 兜底。用户只填 step 2 一处即可。
} _ = v.ResourceID // (保留以便以后加更细的校验)
if _, dup := names[v.Name]; dup { if _, dup := names[v.Name]; dup {
return fmt.Errorf("voices[%d]: duplicate name %q", i, v.Name) return fmt.Errorf("voices[%d]: duplicate name %q", i, v.Name)
} }
+1 -1
View File
@@ -190,7 +190,7 @@ func OpenaiTTSHandler(w http.ResponseWriter, r *http.Request) {
opts.Model = v.Model opts.Model = v.Model
} }
log.Printf("[tts] voice=%s 命中 (speaker=%s resource=%s model=%s) - 客户端=%s", log.Printf("[tts] voice=%s 命中 (speaker=%s resource=%s model=%s) - 客户端=%s",
req.Voice, v.Speaker, v.ResourceID, v.Model, middleware.GetClientIP(r)) req.Voice, telemetry.MaskSpeaker(v.Speaker), v.ResourceID, v.Model, middleware.GetClientIP(r))
} }
ctx, cancel := context.WithTimeout(r.Context(), setting.TTSTimeout) ctx, cancel := context.WithTimeout(r.Context(), setting.TTSTimeout)
+4 -4
View File
@@ -684,7 +684,7 @@
<div class="grid-2"> <div class="grid-2">
<div class="field"> <div class="field">
<label>默认资源 ID</label> <label>默认资源 ID</label>
<input v-model="settingsForm.default_resource_id" placeholder="volc.megatts.default"> <input v-model="settingsForm.default_resource_id" placeholder="seed-icl-2.0">
</div> </div>
<div class="field"> <div class="field">
<label>默认音色 (name)</label> <label>默认音色 (name)</label>
@@ -773,7 +773,7 @@
</div> </div>
<div class="field"> <div class="field">
<label>资源 ID <span class="req">*</span></label> <label>资源 ID <span class="req">*</span></label>
<input v-model="form.resource_id" placeholder="volc.megatts.icl" class="mono"> <input v-model="form.resource_id" placeholder="seed-icl-2.0" class="mono">
</div> </div>
<div class="field"> <div class="field">
<label>Model</label> <label>Model</label>
@@ -847,7 +847,7 @@
const showAdd = ref(false); const showAdd = ref(false);
const adding = ref(false); const adding = ref(false);
const addErr = ref(''); const addErr = ref('');
const form = ref({ name: '', speaker: '', resource_id: 'volc.megatts.icl', model: 'seed-tts-2.0-standard', language: '', description: '' }); const form = ref({ name: '', speaker: '', resource_id: 'seed-icl-2.0', model: 'seed-tts-2.0-standard', language: '', description: '' });
// Settings // Settings
const settings = ref({}); const settings = ref({});
@@ -1006,7 +1006,7 @@
const resetSettingsForm = () => { loadSettings(); }; const resetSettingsForm = () => { loadSettings(); };
const openAdd = () => { const openAdd = () => {
form.value = { name: '', speaker: '', resource_id: 'volc.megatts.icl', model: 'seed-tts-2.0-standard', language: '', description: '' }; form.value = { name: '', speaker: '', resource_id: 'seed-icl-2.0', model: 'seed-tts-2.0-standard', language: '', description: '' };
addErr.value = ''; addErr.value = '';
showAdd.value = true; showAdd.value = true;
}; };
+6 -6
View File
@@ -223,8 +223,8 @@
<div class="field-row"> <div class="field-row">
<div class="field"> <div class="field">
<label>默认资源 ID <span class="req">*</span></label> <label>默认资源 ID <span class="req">*</span></label>
<input v-model="form.default_resource_id" placeholder="volc.megatts.default"> <input v-model="form.default_resource_id" placeholder="seed-icl-2.0">
<div class="hint">音色所属的计费资源 ID;volc.megatts.icl 用于复刻音色。</div> <div class="hint">音色所属的计费资源 ID;本项目为豆包声音复刻大模型 2.0,固定使用 <code>seed-icl-2.0</code>。</div>
</div> </div>
<div class="field"> <div class="field">
<label>默认音色 <span class="req">*</span></label> <label>默认音色 <span class="req">*</span></label>
@@ -275,7 +275,7 @@
<div v-for="(v, i) in form.voices" :key="i" class="voice-row"> <div v-for="(v, i) in form.voices" :key="i" class="voice-row">
<input v-model="v.name" placeholder="qian"> <input v-model="v.name" placeholder="qian">
<input v-model="v.speaker" placeholder="S_xxx"> <input v-model="v.speaker" placeholder="S_xxx">
<input v-model="v.resource_id" placeholder="volc.megatts.icl"> <input v-model="v.resource_id" placeholder="seed-icl-2.0">
<input v-model="v.model" placeholder="seed-tts-2.0-standard"> <input v-model="v.model" placeholder="seed-tts-2.0-standard">
<button class="rm" @click="removeVoice(i)" :disabled="form.voices.length <= 1" title="删除">×</button> <button class="rm" @click="removeVoice(i)" :disabled="form.voices.length <= 1" title="删除">×</button>
</div> </div>
@@ -340,18 +340,18 @@
const form = ref({ const form = ref({
auth_key: '', auth_key: '',
api_key: '', api_key: '',
default_resource_id: 'volc.megatts.default', default_resource_id: 'seed-icl-2.0',
default_speaker: '', default_speaker: '',
default_format: 'mp3', default_format: 'mp3',
sample_rate: 24000, sample_rate: 24000,
token: '', token: '',
voices: [{ name: '', speaker: '', resource_id: 'volc.megatts.icl', model: 'seed-tts-2.0-standard' }], voices: [{ name: '', speaker: '', resource_id: 'seed-icl-2.0', model: 'seed-tts-2.0-standard' }],
}); });
const errorMsg = ref(''); const errorMsg = ref('');
const submitting = ref(false); const submitting = ref(false);
const addVoice = () => { const addVoice = () => {
form.value.voices.push({ name: '', speaker: '', resource_id: 'volc.megatts.icl', model: 'seed-tts-2.0-standard' }); form.value.voices.push({ name: '', speaker: '', resource_id: 'seed-icl-2.0', model: 'seed-tts-2.0-standard' });
}; };
const removeVoice = (i) => { const removeVoice = (i) => {
if (form.value.voices.length > 1) form.value.voices.splice(i, 1); if (form.value.voices.length > 1) form.value.voices.splice(i, 1);