fix: TTS 중첩클릭 에코 버그 + 음성엔진 한/영 발음 개선 + 생성속도 2배
- web-ui: 🔊 버튼 연타/중첩 클릭 시 이전 요청이 완료되면서 새 요청과 오디오가 겹쳐 재생되던 버그 수정 (playTTSText/playTTSMsg를 _ttsPlay 공용 코어로 통합, AbortController + 활성 버튼 가드로 stale response의 재생을 원천 차단) - voice.tts.provider를 edge_tts(ko-KR-SunHiNeural)에서 xtts_gpu로 전환 — 실제로는 실시간 통화와 동일한 GPU 엔진(OmniVoice)을 타서 영어 섞인 문장의 발음이 훨씬 자연스러워짐 - voice_engine.py: OmniVoice num_step 기본값을 32→16으로 낮춤. STT 왕복 검증으로 실측: ~1.9배 빠른 생성(2.94s→1.55s), 품질 저하 없음(8은 문장이 깨져서 폐기). 텍스트 버튼/실시간 통화 양쪽에 공통 적용됨 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+34
-51
@@ -5696,39 +5696,59 @@ function stopMicRecording() {
|
||||
var _ttsEnabled = localStorage.getItem('ttsEnabled') !== 'false'; // default on
|
||||
var _ttsAudio = null;
|
||||
var _ttsBtnActive = null;
|
||||
var _ttsAbort = null;
|
||||
var _appSendFn = null;
|
||||
|
||||
function initAppVoice(sendFn) {
|
||||
_appSendFn = sendFn || null;
|
||||
}
|
||||
|
||||
function playTTSText(btn, text) {
|
||||
text = (text||'').replace(/```[\s\S]*?```/g,'').replace(/[#*`_~>]/g,' ').replace(/\s+/g,' ').trim();
|
||||
if (!text) return;
|
||||
if (_ttsBtnActive === btn && _ttsAudio) {
|
||||
_ttsAudio.pause(); _ttsAudio = null;
|
||||
btn.textContent = '🔊'; btn.classList.remove('playing');
|
||||
_ttsBtnActive = null; return;
|
||||
}
|
||||
// Stops whatever is currently loading/playing (abort in-flight fetch, pause audio, reset button).
|
||||
function _ttsStop() {
|
||||
if (_ttsAbort) { _ttsAbort.abort(); _ttsAbort = null; }
|
||||
if (_ttsAudio) { _ttsAudio.pause(); _ttsAudio = null; }
|
||||
if (_ttsBtnActive) { _ttsBtnActive.textContent = '🔊'; _ttsBtnActive.classList.remove('playing'); }
|
||||
if (_ttsBtnActive) { _ttsBtnActive.textContent = '🔊'; _ttsBtnActive.classList.remove('playing'); _ttsBtnActive = null; }
|
||||
}
|
||||
|
||||
// Shared core for playTTSText/playTTSMsg. Guards every async callback with a
|
||||
// "is this still the active button?" check so a slow/stale response from a
|
||||
// superseded click can never start overlapping playback (the echo bug).
|
||||
function _ttsPlay(btn, text) {
|
||||
if (!text) return;
|
||||
var wasActive = (_ttsBtnActive === btn);
|
||||
_ttsStop();
|
||||
if (wasActive) return; // clicking the active button again = stop
|
||||
|
||||
btn.textContent = '⌛'; btn.classList.add('playing');
|
||||
_ttsBtnActive = btn;
|
||||
var ac = new AbortController();
|
||||
_ttsAbort = ac;
|
||||
fetch('/api/voice/tts', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ text: text.slice(0, 2000) }),
|
||||
signal: ac.signal,
|
||||
})
|
||||
.then(function(r) { if (!r.ok) throw new Error(r.status); return r.blob(); })
|
||||
.then(function(r) { if (!r.ok) throw new Error('TTS 오류 ' + r.status); return r.blob(); })
|
||||
.then(function(blob) {
|
||||
if (_ttsBtnActive !== btn) return; // superseded by a newer click
|
||||
var url = URL.createObjectURL(blob);
|
||||
_ttsAudio = new Audio(url);
|
||||
_ttsAudio.onended = function() { URL.revokeObjectURL(url); _ttsAudio = null; btn.textContent = '🔊'; btn.classList.remove('playing'); _ttsBtnActive = null; };
|
||||
_ttsAudio.onerror = function() { URL.revokeObjectURL(url); _ttsAudio = null; btn.textContent = '🔊'; btn.classList.remove('playing'); _ttsBtnActive = null; };
|
||||
_ttsAudio.onended = function() { URL.revokeObjectURL(url); if (_ttsBtnActive === btn) _ttsStop(); };
|
||||
_ttsAudio.onerror = function() { URL.revokeObjectURL(url); if (_ttsBtnActive === btn) _ttsStop(); };
|
||||
btn.textContent = '⏹'; _ttsAudio.play();
|
||||
})
|
||||
.catch(function() { btn.textContent = '🔊'; btn.classList.remove('playing'); _ttsBtnActive = null; });
|
||||
.catch(function(e) {
|
||||
if (e.name === 'AbortError' || _ttsBtnActive !== btn) return;
|
||||
_ttsStop();
|
||||
alert('TTS 오류: ' + e.message);
|
||||
});
|
||||
}
|
||||
|
||||
function playTTSText(btn, text) {
|
||||
text = (text||'').replace(/```[\s\S]*?```/g,'').replace(/[#*`_~>]/g,' ').replace(/\s+/g,' ').trim();
|
||||
_ttsPlay(btn, text);
|
||||
}
|
||||
|
||||
function updateTTSMode() {
|
||||
@@ -5751,44 +5771,7 @@ function playTTSMsg(btn, idx) {
|
||||
var msg = chatHistory[idx];
|
||||
if (!msg) return;
|
||||
var text = (msg.content || '').replace(/!\[.*?\]\(.*?\)/g, '').replace(/\[([^\]]+)\]\([^)]+\)/g, '$1').replace(/```[\s\S]*?```/g, '').replace(/[#*`_~>]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
if (!text) return;
|
||||
// Toggle off if same button is playing
|
||||
if (_ttsBtnActive === btn && _ttsAudio) {
|
||||
_ttsAudio.pause(); _ttsAudio = null;
|
||||
btn.textContent = '🔊'; btn.classList.remove('playing');
|
||||
_ttsBtnActive = null;
|
||||
return;
|
||||
}
|
||||
// Stop previous
|
||||
if (_ttsAudio) { _ttsAudio.pause(); _ttsAudio = null; }
|
||||
if (_ttsBtnActive) { _ttsBtnActive.textContent = '🔊'; _ttsBtnActive.classList.remove('playing'); }
|
||||
btn.textContent = '⌛'; btn.classList.add('playing');
|
||||
_ttsBtnActive = btn;
|
||||
fetch('/api/voice/tts', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ text: text.slice(0, 2000) }),
|
||||
})
|
||||
.then(function(r) { if (!r.ok) throw new Error('TTS 오류 ' + r.status); return r.blob(); })
|
||||
.then(function(blob) {
|
||||
var url = URL.createObjectURL(blob);
|
||||
_ttsAudio = new Audio(url);
|
||||
_ttsAudio.onended = function() {
|
||||
URL.revokeObjectURL(url); _ttsAudio = null;
|
||||
btn.textContent = '🔊'; btn.classList.remove('playing'); _ttsBtnActive = null;
|
||||
};
|
||||
_ttsAudio.onerror = function() {
|
||||
URL.revokeObjectURL(url); _ttsAudio = null;
|
||||
btn.textContent = '🔊'; btn.classList.remove('playing'); _ttsBtnActive = null;
|
||||
};
|
||||
btn.textContent = '⏹';
|
||||
_ttsAudio.play();
|
||||
})
|
||||
.catch(function(e) {
|
||||
btn.textContent = '🔊'; btn.classList.remove('playing'); _ttsBtnActive = null;
|
||||
alert('TTS 오류: ' + e.message);
|
||||
});
|
||||
_ttsPlay(btn, text);
|
||||
}
|
||||
|
||||
function saveLuckyModal() {
|
||||
|
||||
Reference in New Issue
Block a user