- Added PerformanceMonitor class for tracking application performance metrics including FPS, memory usage, and response times. - Introduced SettingsManager class to handle application settings, configuration, and persistence with local storage. - Created UIManager class to manage UI state, updates, and user interactions, including theme toggling and loading indicators. - Developed VoiceManager class for voice recognition and speech synthesis functionalities, including voice input and output controls. - Added utility functions for common operations such as debouncing, throttling, UUID generation, data formatting, and local storage management.
270 lines
8.5 KiB
JavaScript
270 lines
8.5 KiB
JavaScript
/**
|
|
* Voice Manager Module
|
|
* Handles voice recognition and speech synthesis functionality
|
|
*/
|
|
|
|
export default class VoiceManager {
|
|
constructor(app) {
|
|
this.app = app;
|
|
this.recognition = null;
|
|
this.synthesis = null;
|
|
this.isListening = false;
|
|
this.isSpeaking = false;
|
|
this.voices = [];
|
|
|
|
// Voice settings
|
|
this.settings = {
|
|
language: 'en-US',
|
|
continuous: false,
|
|
interimResults: false,
|
|
maxAlternatives: 1,
|
|
pitch: 1,
|
|
rate: 0.9,
|
|
volume: 0.8,
|
|
voiceName: null // Will be set during initialization
|
|
};
|
|
}
|
|
|
|
async initialize() {
|
|
await this.initializeRecognition();
|
|
await this.initializeSynthesis();
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
async initializeRecognition() {
|
|
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
|
console.warn('Speech recognition not supported');
|
|
return;
|
|
}
|
|
|
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
this.recognition = new SpeechRecognition();
|
|
|
|
// Configure recognition
|
|
this.recognition.lang = this.settings.language;
|
|
this.recognition.continuous = this.settings.continuous;
|
|
this.recognition.interimResults = this.settings.interimResults;
|
|
this.recognition.maxAlternatives = this.settings.maxAlternatives;
|
|
|
|
// Setup recognition event handlers
|
|
this.recognition.onstart = () => this.handleRecognitionStart();
|
|
this.recognition.onend = () => this.handleRecognitionEnd();
|
|
this.recognition.onresult = (event) => this.handleRecognitionResult(event);
|
|
this.recognition.onerror = (event) => this.handleRecognitionError(event);
|
|
}
|
|
|
|
async initializeSynthesis() {
|
|
if (!('speechSynthesis' in window)) {
|
|
console.warn('Speech synthesis not supported');
|
|
return;
|
|
}
|
|
|
|
this.synthesis = window.speechSynthesis;
|
|
|
|
// Wait for voices to be loaded
|
|
if (this.synthesis.getVoices().length === 0) {
|
|
await new Promise(resolve => {
|
|
this.synthesis.addEventListener('voiceschanged', () => {
|
|
this.voices = this.synthesis.getVoices();
|
|
resolve();
|
|
}, { once: true });
|
|
});
|
|
} else {
|
|
this.voices = this.synthesis.getVoices();
|
|
}
|
|
|
|
// Set default voice (prefer en-US)
|
|
this.settings.voiceName = this.selectDefaultVoice();
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Voice toggle button
|
|
const voiceToggle = document.getElementById('voiceToggle');
|
|
if (voiceToggle) {
|
|
voiceToggle.addEventListener('click', () => this.toggleVoiceMode());
|
|
}
|
|
|
|
// Mic button
|
|
const micBtn = document.getElementById('micBtn');
|
|
if (micBtn) {
|
|
micBtn.addEventListener('click', () => this.startVoiceInput());
|
|
}
|
|
}
|
|
|
|
startVoiceInput() {
|
|
if (!this.recognition) {
|
|
this.app.notifications.show('❌ Voice recognition not supported', 'error');
|
|
return;
|
|
}
|
|
|
|
if (this.isListening) {
|
|
this.stopVoiceInput();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.recognition.start();
|
|
this.updateMicButton(true);
|
|
this.app.notifications.show('🎤 Listening... Speak now!');
|
|
} catch (error) {
|
|
console.error('Failed to start voice recognition:', error);
|
|
this.app.notifications.show('❌ Failed to start voice input', 'error');
|
|
}
|
|
}
|
|
|
|
stopVoiceInput() {
|
|
if (this.recognition && this.isListening) {
|
|
this.recognition.stop();
|
|
this.updateMicButton(false);
|
|
}
|
|
}
|
|
|
|
handleRecognitionStart() {
|
|
this.isListening = true;
|
|
this.updateMicButton(true);
|
|
}
|
|
|
|
handleRecognitionEnd() {
|
|
this.isListening = false;
|
|
this.updateMicButton(false);
|
|
}
|
|
|
|
handleRecognitionResult(event) {
|
|
const transcript = event.results[0][0].transcript;
|
|
this.app.messageInput.value = transcript;
|
|
this.app.messageInput.focus();
|
|
this.app.ui.adjustTextareaHeight(this.app.messageInput);
|
|
|
|
// Auto-send if confidence is high
|
|
if (event.results[0][0].confidence > 0.8) {
|
|
this.app.messages.sendMessage();
|
|
}
|
|
}
|
|
|
|
handleRecognitionError(event) {
|
|
console.error('Speech recognition error:', event.error);
|
|
this.app.notifications.show('❌ Voice recognition error: ' + event.error, 'error');
|
|
this.updateMicButton(false);
|
|
}
|
|
|
|
updateMicButton(isActive) {
|
|
const micBtn = document.getElementById('micBtn');
|
|
if (micBtn) {
|
|
micBtn.style.color = isActive ? 'var(--error-color)' : '';
|
|
micBtn.innerHTML = isActive ? '🎙️' : '🎤';
|
|
micBtn.classList.toggle('active', isActive);
|
|
}
|
|
}
|
|
|
|
speakText(text, options = {}) {
|
|
if (!this.synthesis) {
|
|
console.warn('Speech synthesis not supported');
|
|
return;
|
|
}
|
|
|
|
// Cancel any ongoing speech
|
|
this.synthesis.cancel();
|
|
|
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
|
|
// Apply settings and custom options
|
|
utterance.voice = this.getVoice(options.voiceName || this.settings.voiceName);
|
|
utterance.pitch = options.pitch || this.settings.pitch;
|
|
utterance.rate = options.rate || this.settings.rate;
|
|
utterance.volume = options.volume || this.settings.volume;
|
|
|
|
// Handle events
|
|
utterance.onstart = () => {
|
|
this.isSpeaking = true;
|
|
this.app.notifications.show('🔊 Speaking...', 'info');
|
|
};
|
|
|
|
utterance.onend = () => {
|
|
this.isSpeaking = false;
|
|
};
|
|
|
|
utterance.onerror = (event) => {
|
|
console.error('Speech synthesis error:', event);
|
|
this.app.notifications.show('❌ Speech synthesis error', 'error');
|
|
this.isSpeaking = false;
|
|
};
|
|
|
|
this.synthesis.speak(utterance);
|
|
}
|
|
|
|
stopSpeaking() {
|
|
if (this.synthesis && this.isSpeaking) {
|
|
this.synthesis.cancel();
|
|
this.isSpeaking = false;
|
|
}
|
|
}
|
|
|
|
toggleVoiceMode() {
|
|
this.app.setState('voiceMode', !this.app.getState('voiceMode'));
|
|
const btn = document.getElementById('voiceToggle');
|
|
|
|
if (this.app.getState('voiceMode')) {
|
|
btn.innerHTML = '🔊 Voice ON';
|
|
btn.style.background = 'var(--success-color)';
|
|
btn.style.color = 'white';
|
|
this.app.notifications.show('🎤 Voice mode enabled');
|
|
} else {
|
|
btn.innerHTML = '🎤 Voice';
|
|
btn.style.background = '';
|
|
btn.style.color = '';
|
|
this.app.notifications.show('🔇 Voice mode disabled');
|
|
this.stopSpeaking();
|
|
}
|
|
}
|
|
|
|
getVoice(name) {
|
|
return this.voices.find(voice => voice.name === name) ||
|
|
this.voices.find(voice => voice.lang.startsWith('en')) ||
|
|
this.voices[0];
|
|
}
|
|
|
|
selectDefaultVoice() {
|
|
// Prefer English voices, with priority for specific ones
|
|
const preferredVoices = [
|
|
'Google US English',
|
|
'Alex',
|
|
'Samantha',
|
|
'Microsoft David'
|
|
];
|
|
|
|
for (const name of preferredVoices) {
|
|
const voice = this.voices.find(v => v.name === name);
|
|
if (voice) return voice.name;
|
|
}
|
|
|
|
// Fall back to any English voice
|
|
const englishVoice = this.voices.find(voice => voice.lang.startsWith('en'));
|
|
return englishVoice ? englishVoice.name : this.voices[0]?.name;
|
|
}
|
|
|
|
speakMessage(button) {
|
|
if (!this.synthesis) return;
|
|
|
|
const message = button.closest('.message').querySelector('div').textContent;
|
|
this.speakText(message);
|
|
}
|
|
|
|
isVoiceSupported() {
|
|
return ('webkitSpeechRecognition' in window) ||
|
|
('SpeechRecognition' in window) ||
|
|
('speechSynthesis' in window);
|
|
}
|
|
|
|
setVoiceSettings(settings) {
|
|
Object.assign(this.settings, settings);
|
|
|
|
if (this.recognition) {
|
|
this.recognition.lang = this.settings.language;
|
|
this.recognition.continuous = this.settings.continuous;
|
|
this.recognition.interimResults = this.settings.interimResults;
|
|
this.recognition.maxAlternatives = this.settings.maxAlternatives;
|
|
}
|
|
}
|
|
}
|
|
|