@@ -5501,12 +5501,18 @@ async function _loadPanelVoices(selectEl, currentVoiceId) {
55015501 }
55025502
55035503 // Gemini 原生音色(仅在 CORE_API_TYPE=gemini 时由后端注入)
5504- // 与已注册自定义音色冲突时(如用户克隆了名为 Puck 的音色),优先保留自定义条目,
5505- // 与 _has_custom_tts 路由优先级保持一致。
5504+ // 去重范围:已注册自定义音色 + 已渲染的免费预设音色 ID,
5505+ // 避免任一冲突时下拉里重复条目和多重 selected 视觉态。
5506+ // 自定义/免费音色优先保留,与 _has_custom_tts 的路由优先级一致。
55065507 if ( data . native_voices && Object . keys ( data . native_voices ) . length > 0 ) {
5507- const registeredVoiceIds = data . voices ? new Set ( Object . keys ( data . voices ) ) : new Set ( ) ;
5508+ const renderedVoiceIds = new Set ( data . voices ? Object . keys ( data . voices ) : [ ] ) ;
5509+ if ( data . free_voices ) {
5510+ Object . values ( data . free_voices ) . forEach ( function ( id ) {
5511+ if ( id ) renderedVoiceIds . add ( String ( id ) ) ;
5512+ } ) ;
5513+ }
55085514 const nativeEntries = Object . entries ( data . native_voices )
5509- . filter ( function ( [ voiceId ] ) { return ! registeredVoiceIds . has ( voiceId ) ; } ) ;
5515+ . filter ( function ( [ voiceId ] ) { return ! renderedVoiceIds . has ( voiceId ) ; } ) ;
55105516 if ( nativeEntries . length > 0 ) {
55115517 const nativeGroup = document . createElement ( 'optgroup' ) ;
55125518 const nativeLabel = window . t ? window . t ( 'character.geminiNativeVoices' ) : 'Gemini 原生音色' ;
@@ -5521,6 +5527,25 @@ async function _loadPanelVoices(selectEl, currentVoiceId) {
55215527 } ) ;
55225528 selectEl . appendChild ( nativeGroup ) ;
55235529 }
5530+
5531+ // 保底:currentVoiceId 是 Gemini 别名("中文男"、"male" 等)或本轮 catalog 没暴露
5532+ // 该 ID 时,下拉里没有匹配项 select 会回到首项;下次保存表单会被误判为
5533+ // "已清空"走 unregister_voice 分支,把用户保存的音色丢掉。这里仿 GSV 兜底,
5534+ // 给未知值补一条 "(?)" 占位条,保留原值供后端 normalize。
5535+ if ( currentVoiceId
5536+ && ! selectEl . querySelector ( 'option[value="' + CSS . escape ( currentVoiceId ) + '"]' ) ) {
5537+ const fallbackGroup = document . createElement ( 'optgroup' ) ;
5538+ const fallbackLabel = window . t ? window . t ( 'character.savedVoiceFallback' ) : '当前已保存音色' ;
5539+ fallbackGroup . label = '── ' + fallbackLabel + ' ──' ;
5540+ fallbackGroup . dataset . geminiFallbackGroup = 'true' ;
5541+ const fallbackOption = document . createElement ( 'option' ) ;
5542+ fallbackOption . value = currentVoiceId ;
5543+ fallbackOption . textContent = currentVoiceId + ' (?)' ;
5544+ fallbackOption . title = currentVoiceId ;
5545+ fallbackOption . selected = true ;
5546+ fallbackGroup . appendChild ( fallbackOption ) ;
5547+ selectEl . appendChild ( fallbackGroup ) ;
5548+ }
55245549 }
55255550 }
55265551
0 commit comments