diff options
| author | Agando <roduccomm@gmail.com> | 2026-07-09 06:00:32 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-07-09 06:00:32 +0200 |
| commit | cf7461ffeb4f53f3f50c9c692a2e4fdcecf17e82 (patch) | |
| tree | e7780cd41271381cf108c9c81d71a3cb65cfd555 /chat/chat-modules.js | |
| parent | ea4d09fb5ccba12ed774fcf7c913edfa1e6a851a (diff) | |
auto: 2026-07-09_06:00 changes
Diffstat (limited to 'chat/chat-modules.js')
| -rw-r--r-- | chat/chat-modules.js | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/chat/chat-modules.js b/chat/chat-modules.js index 295c7c9..e85cdff 100644 --- a/chat/chat-modules.js +++ b/chat/chat-modules.js @@ -398,3 +398,180 @@ function importModules() { if (document.getElementById('chat-overlay')) { loadModules(); } + +// ═══ TTS + SST Integration ═══ +var _chatTTS = false; +var _chatVoices = []; +var _chatMicRecording = false; +var _chatMediaRecorder = null; +var _chatAudioChunks = []; +var _chatRecordingTimeout = null; +var _chatSTTEndpoint = '/vmai-api/stt'; + +// Pre-load voices +if (typeof speechSynthesis !== 'undefined') { + _chatVoices = speechSynthesis.getVoices() || []; + if (_chatVoices.length === 0) { + speechSynthesis.onvoiceschanged = function() { + _chatVoices = speechSynthesis.getVoices() || []; + }; + } +} + +function toggleChatTTS() { + _chatTTS = !_chatTTS; + var btn = document.getElementById('chat-tts-btn'); + if (btn) { + btn.style.borderColor = _chatTTS ? 'var(--success-color)' : ''; + btn.style.color = _chatTTS ? 'var(--success-color)' : ''; + } + if (typeof addLog === 'function') addLog('TTS ' + (_chatTTS ? 'ON' : 'OFF')); + if (typeof showToast === 'function') showToast('🔊 TTS ' + (_chatTTS ? 'ON' : 'OFF')); +} + +function chatSpeakText(text) { + if (!_chatTTS || typeof speechSynthesis === 'undefined') return; + speechSynthesis.cancel(); + var clean = text.replace(/```[\s\S]*?```/g, ' code block ').replace(/[*_#|`>]/g, '').replace(/\n+/g, '. ').trim(); + if (!clean) return; + function doSpeak(txt) { + var utter = new SpeechSynthesisUtterance(txt); + utter.rate = 1.0; + utter.pitch = 1.0; + var voice = _chatVoices.find(function(v) { return v.lang && v.lang.startsWith('en'); }) || (_chatVoices.length > 0 ? _chatVoices[0] : null); + if (voice) utter.voice = voice; + if (_chatVoices.length === 0) { + var spoken = false; + speechSynthesis.onvoiceschanged = function() { + if (spoken) return; spoken = true; + _chatVoices = speechSynthesis.getVoices() || []; + var v = _chatVoices.find(function(v) { return v.lang && v.lang.startsWith('en'); }) || _chatVoices[0]; + if (v) utter.voice = v; + speechSynthesis.cancel(); speechSynthesis.speak(utter); + speechSynthesis.onvoiceschanged = null; + }; + setTimeout(function() { + if (!spoken) { spoken = true; speechSynthesis.onvoiceschanged = null; speechSynthesis.cancel(); speechSynthesis.speak(utter); } + }, 2000); + } else { + speechSynthesis.speak(utter); + } + } + if (clean.length > 500) { + var chunks = clean.match(/.{1,500}/g); + for (var i = 0; i < chunks.length; i++) doSpeak(chunks[i]); + } else { + doSpeak(clean); + } +} + +function toggleChatMic() { + if (_chatMicRecording) { + stopChatRecording(); + } else { + startChatRecording(); + } +} + +function startChatRecording() { + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + if (typeof showToast === 'function') showToast('🎤 Audio recording not supported'); + return; + } + _chatMicRecording = true; + _chatAudioChunks = []; + var btn = document.getElementById('chat-mic-btn'); + if (btn) { btn.textContent = '🔴'; btn.style.borderColor = 'var(--success-color)'; btn.style.color = 'var(--success-color)'; } + if (typeof addLog === 'function') addLog('🎤 Recording... (tap mic again to stop)'); + + navigator.mediaDevices.getUserMedia({ audio: true }) + .then(function(stream) { + var mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' + : MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : ''; + _chatMediaRecorder = new MediaRecorder(stream, mimeType ? { mimeType: mimeType } : {}); + _chatMediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) _chatAudioChunks.push(e.data); }; + _chatMediaRecorder.onstop = function() { + stream.getTracks().forEach(function(t) { t.stop(); }); + transcribeChatAudio(); + }; + _chatMediaRecorder.start(); + _chatRecordingTimeout = setTimeout(function() { + if (_chatMediaRecorder && _chatMediaRecorder.state === 'recording') _chatMediaRecorder.stop(); + }, 10000); + }) + .catch(function(err) { + if (typeof showToast === 'function') showToast('🎤 Mic denied: ' + err.message); + _chatMicRecording = false; + var btn = document.getElementById('chat-mic-btn'); + if (btn) { btn.textContent = '🎤'; btn.style.borderColor = ''; btn.style.color = ''; } + }); +} + +function stopChatRecording() { + if (_chatMediaRecorder && _chatMediaRecorder.state === 'recording') { + _chatMediaRecorder.stop(); + clearTimeout(_chatRecordingTimeout); + } + _chatMicRecording = false; + var btn = document.getElementById('chat-mic-btn'); + if (btn) { btn.textContent = '🎤'; btn.style.borderColor = ''; btn.style.color = ''; } +} + +function transcribeChatAudio() { + var blob = new Blob(_chatAudioChunks, { type: _chatMediaRecorder ? _chatMediaRecorder.mimeType || 'audio/webm' : 'audio/webm' }); + if (blob.size < 100) { + if (typeof addLog === 'function') addLog('🎤 No audio detected'); + return; + } + if (typeof addLog === 'function') addLog('🎤 Transcribing...'); + var reader = new FileReader(); + reader.onload = async function() { + try { + var ctx = new (window.AudioContext || window.webkitAudioContext)(); + var audioBuffer = await ctx.decodeAudioData(reader.result); + var pcmData = chatAudioBufferToWav(audioBuffer); + var b64 = btoa(String.fromCharCode.apply(null, new Uint8Array(pcmData))); + var resp = await fetch(_chatSTTEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ audio: b64, sample_rate: 16000 }) + }); + var data = await resp.json(); + var text = (data.text || '').trim(); + if (text) { + if (typeof addLog === 'function') addLog('🎤 Recognized: "' + text + '"'); + var input = document.getElementById('chatInput'); + if (input) { input.value = text; input.focus(); } + } else { + if (typeof addLog === 'function') addLog('🎤 No speech detected'); + } + } catch (err) { + if (typeof addLog === 'function') addLog('🎤 Transcription error: ' + err.message); + } + }; + reader.readAsArrayBuffer(blob); +} + +function chatAudioBufferToWav(audioBuffer) { + var targetSampleRate = 16000; + var src = audioBuffer.getChannelData(0); + var srcRate = audioBuffer.sampleRate; + var ratio = srcRate / targetSampleRate; + var newLen = Math.round(src.length / ratio); + var pcm = new Int16Array(newLen); + for (var i = 0; i < newLen; i++) { + var srcIdx = Math.round(i * ratio); + var sample = Math.max(-1, Math.min(1, src[Math.min(srcIdx, src.length - 1)])); + pcm[i] = sample < 0 ? sample * 0x8000 : sample * 0x7FFF; + } + var buffer = new ArrayBuffer(44 + pcm.byteLength); + var view = new DataView(buffer); + var writeStr = function(off, str) { for (var j = 0; j < str.length; j++) view.setUint8(off + j, str.charCodeAt(j)); }; + writeStr(0, 'RIFF'); view.setUint32(4, 36 + pcm.byteLength, true); writeStr(8, 'WAVE'); + writeStr(12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, 1, true); + view.setUint32(24, targetSampleRate, true); view.setUint32(28, targetSampleRate * 2, true); + view.setUint16(32, 2, true); view.setUint16(34, 16, true); + writeStr(36, 'data'); view.setUint32(40, pcm.byteLength, true); + new Uint8Array(buffer, 44).set(new Uint8Array(pcm.buffer)); + return buffer; +} |