fix: 실시간 음성 통화 — 워클릿 경로 깨짐·시작 실패 무안내·답변 반복 낭독
- addModule('voice-call-worklets.js') 상대경로가 07-25 web-ui 폴더 정리 뒤 HTML(catch-all)을 돌려줘
통화가 시작 직후 멈췄다 → 절대경로 /js/voice/voice-call-worklets.js
- 시작 단계 예외 시 버튼이 📵에 갇히던 문제: try/catch로 정리 후 오류 안내
- 최종 답변(finalAnswer)이 토큰 스트림 뒤에 통째로 다시 들어와 읽은 위치가 0으로 리셋되면
같은 문장을 다시 읽던 문제: 턴 단위 문장 키로 중복 낭독 방지
- 통화 종료 사유를 콘솔에 기록(사용자/서버/네트워크 구분)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,12 @@
|
||||
let currentTtsId = null;
|
||||
let spokenUpTo = 0;
|
||||
let pendingTts = '';
|
||||
// 이번 턴에 이미 읽은(대기열에 넣은) 문장의 정규화 키. 최종 답변(finalAnswer)이 토큰 스트림 뒤에
|
||||
// 통째로 다시 들어오거나 가드가 답변을 다시 쓰게 해도, 같은 문장을 두 번 읽지 않는다.
|
||||
// (09-26 "말을 계속 반복한다": _voiceCallResetSpoken이 읽은 위치를 0으로 되돌려 처음부터 재낭독됐다)
|
||||
const spokenKeys = new Set();
|
||||
const MIN_DEDUP_KEY_CHARS = 6; // "네." 같은 짧은 추임새는 반복돼도 자연스러우니 제외
|
||||
function sentenceKey(t) { return String(t || '').replace(/[^\p{L}\p{N}]/gu, '').toLowerCase(); }
|
||||
let active = false;
|
||||
let wakeLock = null;
|
||||
|
||||
@@ -69,7 +75,10 @@
|
||||
if (gainNode) gainNode.gain.value = vol;
|
||||
};
|
||||
|
||||
window.voiceCallHangup = function () {
|
||||
window.voiceCallHangup = function (reason) {
|
||||
// 종료 사유를 콘솔에 남긴다 — 사용자가 누른 건지, 서버/네트워크가 끊은 건지 구분하기 위함
|
||||
// (09-26 "대답하다가 끊어졌다" 신고: 엔진 로그만으로는 어느 쪽인지 알 수 없었다)
|
||||
if (active) console.info('[voice-call] hangup:', typeof reason === 'string' ? reason : 'user/button', 'state=' + state);
|
||||
active = false;
|
||||
state = 'idle';
|
||||
try { ws && ws.close(); } catch {}
|
||||
@@ -105,17 +114,20 @@
|
||||
setCaption('');
|
||||
state = 'listening';
|
||||
speechChunks = 0; silenceMs = 0; preRoll = [];
|
||||
spokenUpTo = 0; pendingTts = ''; ttsQueue = []; ttsBusy = false; currentTtsId = null;
|
||||
spokenUpTo = 0; pendingTts = ''; spokenKeys.clear(); ttsQueue = []; ttsBusy = false; currentTtsId = null;
|
||||
|
||||
// 설정 단계(워클릿 로드·WebSocket) 어디서든 실패하면 버튼이 📵에 갇혀 "연결 중…"으로 멈추던 문제 —
|
||||
// (2026-09-26 실측: 워클릿 상대경로가 HTML을 돌려줘 addModule이 예외를 던졌는데 아무 안내가 없었다)
|
||||
try {
|
||||
captureCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
await captureCtx.audioWorklet.addModule('voice-call-worklets.js');
|
||||
await captureCtx.audioWorklet.addModule('/js/voice/voice-call-worklets.js');
|
||||
const source = captureCtx.createMediaStreamSource(micStream);
|
||||
captureNode = new AudioWorkletNode(captureCtx, 'mic-capture-processor');
|
||||
captureNode.port.onmessage = handleMicChunk;
|
||||
source.connect(captureNode);
|
||||
|
||||
playCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24000 });
|
||||
await playCtx.audioWorklet.addModule('voice-call-worklets.js');
|
||||
await playCtx.audioWorklet.addModule('/js/voice/voice-call-worklets.js');
|
||||
playerNode = new AudioWorkletNode(playCtx, 'pcm-player-processor');
|
||||
// A GainNode we control from the in-app slider — on some mobile browsers
|
||||
// the hardware volume rocker maps to the call/mic audio session while a
|
||||
@@ -135,8 +147,13 @@
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = () => setStatus('듣는 중…');
|
||||
ws.onmessage = handleWsMessage;
|
||||
ws.onerror = () => setStatus('연결 오류');
|
||||
ws.onclose = () => { if (active) voiceCallHangup(); };
|
||||
ws.onerror = (ev) => { console.warn('[voice-call] ws error', ev && ev.type); setStatus('연결 오류'); };
|
||||
ws.onclose = (ev) => { if (active) voiceCallHangup('ws closed code=' + (ev && ev.code) + ' reason=' + (ev && ev.reason || '-') + ' clean=' + (ev && ev.wasClean)); };
|
||||
} catch (e) {
|
||||
console.error('[voice-call] start failed:', e);
|
||||
voiceCallHangup();
|
||||
alert('음성 통화를 시작하지 못했습니다: ' + (e && e.message ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
function handleMicChunk(e) {
|
||||
@@ -210,7 +227,7 @@
|
||||
setCaption('');
|
||||
const text = (msg.text || '').trim();
|
||||
if (!text) { state = 'listening'; setStatus('듣는 중…'); break; }
|
||||
spokenUpTo = 0; pendingTts = '';
|
||||
spokenUpTo = 0; pendingTts = ''; spokenKeys.clear();
|
||||
const input = document.getElementById('chat-input');
|
||||
if (input) {
|
||||
input.value = text;
|
||||
@@ -426,6 +443,11 @@
|
||||
// silently absorbed the next bullet line and the pause between them
|
||||
// vanished (reported 2026-07-10).
|
||||
function bufferSentence(sentence, isLineBreak) {
|
||||
const key = sentenceKey(sentence);
|
||||
if (key.length >= MIN_DEDUP_KEY_CHARS) {
|
||||
if (spokenKeys.has(key)) return;
|
||||
spokenKeys.add(key);
|
||||
}
|
||||
if (isLineBreak) {
|
||||
flushPendingTts();
|
||||
enqueueSentence(sentence);
|
||||
|
||||
Reference in New Issue
Block a user