// Shared voice input + TTS for sub-apps. Requires authH() to be defined by the host page. var _appSendFn = null; var _isRecording = false; var _speechRecog = null; var _micAutosend = true; var _ttsAudio = null; var _ttsBtnActive = null; function initAppVoice(sendFn) { _appSendFn = sendFn || null; } function toggleMicRecording() { if (_isRecording) stopMicRecording(); else startMicRecording(); } function startMicRecording() { var SpeechRecog = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecog) { alert('이 브라우저는 음성 인식을 지원하지 않습니다.\nChrome을 사용해주세요.'); return; } _speechRecog = new SpeechRecog(); _speechRecog.lang = 'ko-KR'; _speechRecog.interimResults = false; _speechRecog.maxAlternatives = 1; _speechRecog.continuous = false; var btn = document.getElementById('mic-btn'); _speechRecog.onstart = function() { _isRecording = true; if (btn) { btn.textContent = '⏹'; btn.title = '말하는 중… (클릭 시 중지)'; btn.classList.add('recording'); } }; _speechRecog.onresult = function(e) { var transcript = e.results[0][0].transcript; var input = document.getElementById('chat-input'); if (input && transcript) { input.value = (input.value ? input.value + ' ' : '') + transcript; input.focus(); input.dispatchEvent(new Event('input')); if (_micAutosend && _appSendFn) setTimeout(function() { _appSendFn(); }, 50); } }; _speechRecog.onerror = function(e) { if (e.error !== 'no-speech') alert('음성 인식 오류: ' + e.error); }; _speechRecog.onend = function() { _isRecording = false; _speechRecog = null; if (btn) { btn.textContent = '🎤'; btn.title = '음성 입력'; btn.classList.remove('recording'); } }; _speechRecog.start(); } function stopMicRecording() { if (_speechRecog) _speechRecog.stop(); } 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; } 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' }, typeof authH === 'function' ? authH() : {}), credentials: 'include', body: JSON.stringify({ text: text.slice(0, 2000) }), }) .then(function(r) { if (!r.ok) throw new Error(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() { btn.textContent = '🔊'; btn.classList.remove('playing'); _ttsBtnActive = null; }); }