diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-22 18:00:43 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-22 18:00:43 +0200 |
| commit | 6f6161562c5f1028256f3163083b665719741646 (patch) | |
| tree | ccc9ab59e8433555557b6db8b6c1d4940b8652a4 /modules/ext/voice.js | |
| parent | 82270d6f2685d84d5c9b4a454ae7260de75d34cf (diff) | |
auto: 2026-06-22_18:00 changes
Diffstat (limited to 'modules/ext/voice.js')
| -rw-r--r-- | modules/ext/voice.js | 59 |
1 files changed, 59 insertions, 0 deletions
diff --git a/modules/ext/voice.js b/modules/ext/voice.js index de0caed..b243ace 100644 --- a/modules/ext/voice.js +++ b/modules/ext/voice.js @@ -27,6 +27,65 @@ class VoiceModule { } registerCommands() { this.commands['voice'] = (args) => this.handle(args); + this.commands['speak'] = (args) => this.speak(args); + } + + getHelpInfo() { + return { 'voice': 'voice [stop] — Start voice recording (Whisper STT), or type "voice stop" to cancel', 'speak': 'speak [text] — Text-to-speech. No args = toggle auto-speak for AI responses' }; + } + + speak(args) { + const text = args.join(' ').trim(); + if (!text) { + // If no text, check if speechSynthesis is available + if (typeof speechSynthesis === 'undefined') { + this.terminal.print('🔊 TTS not supported in this browser', 'error'); + return; + } + // Toggle auto-speak mode + this._autoSpeak = !this._autoSpeak; + this.terminal.print(this._autoSpeak ? '🔊 Auto-speak ON — AI responses will be spoken' : '🔊 Auto-speak OFF', 'info'); + return; + } + if (typeof speechSynthesis === 'undefined') { + this.terminal.print('🔊 TTS not supported in this browser', 'error'); + return; + } + speechSynthesis.cancel(); + const utter = new SpeechSynthesisUtterance(text); + utter.rate = parseFloat(localStorage.getItem('webx11_tts_rate') || '1.0'); + utter.pitch = 1.0; + utter.volume = 1.0; + // Try to use a good English voice + const voices = speechSynthesis.getVoices(); + const enVoice = voices.find(v => v.lang.startsWith('en') && v.name.includes('Google')) || voices.find(v => v.lang.startsWith('en')); + if (enVoice) utter.voice = enVoice; + utter.onstart = () => this._updateButton(true); + utter.onend = () => this._updateButton(false); + utter.onerror = () => this._updateButton(false); + speechSynthesis.speak(utter); + this.terminal.print('🔊 Speaking: "' + text.substring(0, 80) + (text.length > 80 ? '...' : '') + '"', 'info'); + } + + speakText(text) { + // Called by AI assistant to auto-speak responses + if (!this._autoSpeak || typeof speechSynthesis === 'undefined') return; + speechSynthesis.cancel(); + // Strip markdown/code blocks for cleaner speech + const clean = text.replace(/```[\s\S]*?```/g, ' code block ').replace(/[*_#|`>]/g, '').replace(/\n+/g, '. ').trim(); + if (clean.length > 500) { + // Split long text into chunks + const chunks = clean.match(/.{1,500}/g); + chunks.forEach(c => { + const u = new SpeechSynthesisUtterance(c); + u.rate = parseFloat(localStorage.getItem('webx11_tts_rate') || '1.0'); + speechSynthesis.speak(u); + }); + } else { + const u = new SpeechSynthesisUtterance(clean); + u.rate = parseFloat(localStorage.getItem('webx11_tts_rate') || '1.0'); + speechSynthesis.speak(u); + } } handle(args) { const sub = args[0] && args[0].toLowerCase(); |