From a9daa4bdc321b65eb794443918e7b652192b2a84 Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Thu, 26 Dec 2024 12:21:02 -0500 Subject: [PATCH] Refactor file paths for JavaScript resources and update text-to-speech feature implementation --- js/features/text-to-speech.js | 231 +++++++++++++++++++++++++--------- 1 file changed, 173 insertions(+), 58 deletions(-) diff --git a/js/features/text-to-speech.js b/js/features/text-to-speech.js index 6b9b380..02bab2e 100644 --- a/js/features/text-to-speech.js +++ b/js/features/text-to-speech.js @@ -1,6 +1,7 @@ import { BaseTool } from './base-tool.js'; import { notifications } from '../utils/ui.js'; import { STORAGE_KEYS } from '../utils/constants.js'; +import utils from '../utils/helpers.js'; /** * Text-to-Speech API wrapper @@ -10,41 +11,25 @@ class TextToSpeechAPI { this.synth = window.speechSynthesis; this.voices = []; this.currentVoice = null; + this.currentUtterance = null; this.loadVoices(); } loadVoices() { this.voices = this.synth.getVoices(); - if (speechSynthesis.onvoiceschanged !== undefined) { - speechSynthesis.onvoiceschanged = () => { + if (this.synth.onvoiceschanged !== undefined) { + this.synth.onvoiceschanged = () => { this.voices = this.synth.getVoices(); }; } } - getVoices() { - return this.voices; - } - - getVoicesForLanguage(langCode) { - return this.voices.filter(voice => voice.lang.startsWith(langCode)); - } - - setVoice(voice) { - if (typeof voice === 'string') { - const foundVoice = this.voices.find(v => v.name === voice); - if (foundVoice) { - this.currentVoice = foundVoice; - return true; - } - return false; - } - - if (voice instanceof SpeechSynthesisVoice) { + setVoice(voiceId) { + const voice = this.voices.find(v => v.voiceURI === voiceId); + if (voice) { this.currentVoice = voice; return true; } - return false; } @@ -57,29 +42,26 @@ class TextToSpeechAPI { this.stop(); - const utterance = new SpeechSynthesisUtterance(text); + this.currentUtterance = new SpeechSynthesisUtterance(text); if (this.currentVoice) { - utterance.voice = this.currentVoice; + this.currentUtterance.voice = this.currentVoice; } - utterance.rate = options.rate || 1; - utterance.pitch = options.pitch || 1; - utterance.volume = options.volume || 1; + this.currentUtterance.rate = options.rate || 1; + this.currentUtterance.pitch = options.pitch || 1; + this.currentUtterance.volume = options.volume || 1; - utterance.onend = () => resolve(); - utterance.onerror = (event) => reject(new Error(event.error)); + this.currentUtterance.onend = () => resolve(); + this.currentUtterance.onerror = (event) => reject(new Error(event.error)); - this.synth.speak(utterance); + this.synth.speak(this.currentUtterance); }); } stop() { this.synth.cancel(); - } - - isSpeaking() { - return this.synth.speaking; + this.currentUtterance = null; } pause() { @@ -90,12 +72,12 @@ class TextToSpeechAPI { this.synth.resume(); } - getSupportedLanguages() { - const languages = new Set(); - this.voices.forEach(voice => { - languages.add(voice.lang); - }); - return Array.from(languages); + isSpeaking() { + return this.synth.speaking; + } + + isPaused() { + return this.synth.paused; } } @@ -141,9 +123,7 @@ export default class TextToSpeech extends BaseTool { initializeState() { return { - synthesis: window.speechSynthesis, - voices: [], - currentUtterance: null, + maxLength: 5000, isPlaying: false, isPaused: false, settings: { @@ -156,39 +136,35 @@ export default class TextToSpeech extends BaseTool { } bindEvents() { - const { textInput, voiceSelect, rateInput, pitchInput, volumeInput, speakButton, pauseButton, stopButton, downloadButton, settingsToggle } = this.elements; + const { textInput, voiceSelect, rateInput, pitchInput, volumeInput, speakButton, pauseButton, stopButton, settingsToggle } = this.elements; // Text input events - textInput.addEventListener('input', this.handleTextInput.bind(this)); + textInput.addEventListener('input', this.debounce(this.handleTextInput.bind(this), 300)); // Voice options events voiceSelect.addEventListener('change', this.handleVoiceSelect.bind(this)); - rateInput.addEventListener('input', this.handleRateChange.bind(this)); - pitchInput.addEventListener('input', this.handlePitchChange.bind(this)); - volumeInput.addEventListener('input', this.handleVolumeChange.bind(this)); + rateInput.addEventListener('input', this.debounce(this.handleRateChange.bind(this), 100)); + pitchInput.addEventListener('input', this.debounce(this.handlePitchChange.bind(this), 100)); + volumeInput.addEventListener('input', this.debounce(this.handleVolumeChange.bind(this), 100)); // Action button events - speakButton.addEventListener('click', this.speak.bind(this)); - pauseButton.addEventListener('click', this.pause.bind(this)); + speakButton.addEventListener('click', this.toggleSpeech.bind(this)); + pauseButton.addEventListener('click', this.togglePause.bind(this)); stopButton.addEventListener('click', this.stop.bind(this)); - downloadButton.addEventListener('click', this.download.bind(this)); // Settings toggle settingsToggle.addEventListener('click', this.toggleSettings.bind(this)); // Voice list update - this.state.synthesis.addEventListener('voiceschanged', this.loadVoices.bind(this)); + this.api.synth.addEventListener('voiceschanged', this.loadVoices.bind(this)); // Keyboard shortcuts - this.addKeyboardShortcut('space', this.togglePlayPause.bind(this)); + this.addKeyboardShortcut('space', this.toggleSpeech.bind(this)); + this.addKeyboardShortcut('p', this.togglePause.bind(this), { ctrl: true }); this.addKeyboardShortcut('s', this.stop.bind(this), { ctrl: true }); - this.addKeyboardShortcut('d', this.download.bind(this), { ctrl: true }); } initialize() { - // Load available voices - this.loadVoices(); - // Set initial values this.elements.rateInput.value = this.state.settings.rate; this.elements.pitchInput.value = this.state.settings.pitch; @@ -196,9 +172,148 @@ export default class TextToSpeech extends BaseTool { // Hide settings panel initially this.elements.settingsPanel.style.display = 'none'; + + // Load voices + this.loadVoices(); + + // Load saved settings + this.loadSettings(); + } + + handleTextInput() { + const text = this.elements.textInput.value; + const length = text.length; + + this.elements.charCount.textContent = `${length}/${this.state.maxLength}`; + + if (length > this.state.maxLength) { + this.elements.textInput.value = text.slice(0, this.state.maxLength); + this.showNotification('Text exceeds maximum length', 'error'); + } + } + + handleVoiceSelect() { + const voiceId = this.elements.voiceSelect.value; + if (this.api.setVoice(voiceId)) { + this.state.settings.voice = voiceId; + this.saveSettings(); + } + } + + handleRateChange() { + this.state.settings.rate = parseFloat(this.elements.rateInput.value); + this.saveSettings(); + } + + handlePitchChange() { + this.state.settings.pitch = parseFloat(this.elements.pitchInput.value); + this.saveSettings(); + } + + handleVolumeChange() { + this.state.settings.volume = parseFloat(this.elements.volumeInput.value); + this.saveSettings(); } - // ... rest of the class implementation ... + loadVoices() { + const voices = this.api.voices; + if (voices.length === 0) return; + + this.elements.voiceSelect.innerHTML = voices + .map(voice => ``) + .join(''); + + if (this.state.settings.voice) { + this.elements.voiceSelect.value = this.state.settings.voice; + this.api.setVoice(this.state.settings.voice); + } + } + + async toggleSpeech() { + if (this.state.isPlaying) { + this.stop(); + return; + } + + const text = this.elements.textInput.value.trim(); + if (!text) { + this.showNotification('Please enter text to speak', 'error'); + return; + } + + try { + this.state.isPlaying = true; + this.updatePlayButton(); + + await this.api.speak(text, this.state.settings); + + this.state.isPlaying = false; + this.updatePlayButton(); + } catch (error) { + console.error('Speech error:', error); + this.showNotification('Failed to speak text', 'error'); + this.state.isPlaying = false; + this.updatePlayButton(); + } + } + + togglePause() { + if (!this.state.isPlaying) return; + + if (this.state.isPaused) { + this.api.resume(); + this.state.isPaused = false; + } else { + this.api.pause(); + this.state.isPaused = true; + } + + this.updatePauseButton(); + } + + stop() { + this.api.stop(); + this.state.isPlaying = false; + this.state.isPaused = false; + this.updatePlayButton(); + this.updatePauseButton(); + } + + toggleSettings() { + const isVisible = this.elements.settingsPanel.style.display === 'block'; + this.elements.settingsPanel.style.display = isVisible ? 'none' : 'block'; + this.elements.settingsToggle.setAttribute('aria-expanded', !isVisible); + } + + updatePlayButton() { + const icon = this.state.isPlaying ? 'stop' : 'play'; + this.elements.speakButton.innerHTML = ``; + this.elements.speakButton.setAttribute('aria-label', this.state.isPlaying ? 'Stop' : 'Play'); + } + + updatePauseButton() { + const icon = this.state.isPaused ? 'play' : 'pause'; + this.elements.pauseButton.innerHTML = ``; + this.elements.pauseButton.setAttribute('aria-label', this.state.isPaused ? 'Resume' : 'Pause'); + } + + loadSettings() { + const saved = this.loadFromStorage(STORAGE_KEYS.TTS_SETTINGS); + if (saved) { + this.state.settings = { ...this.state.settings, ...saved }; + this.elements.rateInput.value = this.state.settings.rate; + this.elements.pitchInput.value = this.state.settings.pitch; + this.elements.volumeInput.value = this.state.settings.volume; + if (this.state.settings.voice) { + this.elements.voiceSelect.value = this.state.settings.voice; + this.api.setVoice(this.state.settings.voice); + } + } + } + + saveSettings() { + this.saveToStorage(STORAGE_KEYS.TTS_SETTINGS, this.state.settings); + } } // Initialize the tool if we're on the text-to-speech page