Supersede draft PR #26 with a merge-safe prerequisite: bake and preload the amy, irina, and claude Piper models, route only validated server-side language to fixed voices, and leave the live voice deployment manifest unchanged. Remove the pinned WebUI speaker selector and its persisted preference, omit client voice fields from every outbound TTS path, and keep hands-free Voice Mode and the conversation instrument intact. Hostile or legacy voice fields remain ignored by the Piper server. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
36 lines
1.1 KiB
JavaScript
36 lines
1.1 KiB
JavaScript
function _buildBrowserUtterance(text, btn){
|
|
const utter=new SpeechSynthesisUtterance(text);
|
|
const savedVoice=localStorage.getItem('hermes-tts-voice');
|
|
const voices=speechSynthesis.getVoices();
|
|
if(savedVoice&&voices.length){
|
|
const match=voices.find(v=>v.name===savedVoice);
|
|
if(match) utter.voice=match;
|
|
}
|
|
return utter;
|
|
}
|
|
function _playEdgeTtsChunked(text, btn){
|
|
const voice=localStorage.getItem('hermes-tts-voice')||'zh-CN-XiaoxiaoNeural';
|
|
return fetch('/api/tts',{body:JSON.stringify({text:chunk, voice:voice, rate:rate, pitch:pitch})});
|
|
}
|
|
function speakSelected(clean, btn, engine){
|
|
if(engine==='edge'){
|
|
_playEdgeTtsChunked(clean, btn);
|
|
}
|
|
}
|
|
function speakAutomatically(clean, engine){
|
|
if(engine==='edge'){
|
|
_playEdgeTtsChunked(clean, null);
|
|
}
|
|
}
|
|
function registeredTts(engine, clean){
|
|
const _opts={
|
|
voice: localStorage.getItem('hermes-tts-voice')||'',
|
|
rate: parseFloat(localStorage.getItem('hermes-tts-rate')),
|
|
};
|
|
const autoOpts={
|
|
voice: localStorage.getItem('hermes-tts-voice')||'',
|
|
pitch: parseFloat(localStorage.getItem('hermes-tts-pitch')),
|
|
};
|
|
return [engine, clean, _opts, autoOpts];
|
|
}
|