| |
| class VoiceRecognition { |
| constructor() { |
| this.recognition = null; |
| this.isListening = false; |
| this.isSupported = false; |
| this.onResult = null; |
| this.onError = null; |
| this.onStatusChange = null; |
| |
| |
| this.retryCount = 0; |
| this.maxRetries = 3; |
| this.retryDelay = 2000; |
| this.lastErrorTime = 0; |
| this.consecutiveErrors = 0; |
| this.isDisabled = false; |
| |
| |
| this.isStarting = false; |
| this.isStopping = false; |
| this.restartTimer = null; |
| this.startupPromise = null; |
| |
| |
| this.doubleClickRecoveryAdded = false; |
| |
| this.init(); |
| } |
| |
| init() { |
| |
| if ('webkitSpeechRecognition' in window) { |
| this.recognition = new webkitSpeechRecognition(); |
| this.isSupported = true; |
| } else if ('SpeechRecognition' in window) { |
| this.recognition = new SpeechRecognition(); |
| this.isSupported = true; |
| } else { |
| console.warn('浏览器不支持语音识别'); |
| this.handleError('浏览器不支持语音识别'); |
| return; |
| } |
| |
| this.setupRecognition(); |
| |
| |
| setTimeout(async () => { |
| try { |
| console.log('初始化语音识别...'); |
| await this.startListening(); |
| } catch (error) { |
| console.error('初始化启动失败:', error); |
| |
| setTimeout(() => { |
| this.showNotification('语音识别初始化遇到问题,双击页面重新尝试', 'warning'); |
| this.addDoubleClickRecovery(); |
| }, 2000); |
| } |
| }, 500); |
| } |
| |
| setupRecognition() { |
| |
| this.recognition.continuous = true; |
| this.recognition.interimResults = false; |
| this.recognition.lang = 'en-US'; |
| this.recognition.maxAlternatives = 3; |
| |
| |
| this.recognition.onstart = () => { |
| this.isListening = true; |
| this.updateStatus('listening'); |
| console.log('语音识别开始'); |
| }; |
| |
| this.recognition.onresult = (event) => { |
| this.handleResult(event); |
| }; |
| |
| this.recognition.onerror = (event) => { |
| this.handleError(event.error); |
| }; |
| |
| this.recognition.onend = () => { |
| this.isListening = false; |
| this.isStopping = false; |
| this.updateStatus('ready'); |
| console.log('语音识别结束'); |
| |
| |
| if (this.restartTimer) { |
| clearTimeout(this.restartTimer); |
| this.restartTimer = null; |
| } |
| |
| |
| if (this.shouldAttemptRestart()) { |
| const delay = this.calculateRestartDelay(); |
| console.log(`准备在 ${delay}ms 后重新启动语音识别...`); |
| |
| this.restartTimer = setTimeout(async () => { |
| if (this.isSupported && !this.isListening && !this.isDisabled && !this.isStarting) { |
| await this.startListening(); |
| } |
| }, delay); |
| } else { |
| console.log('由于频繁错误,暂停自动重启语音识别'); |
| this.showNotification('语音识别暂停,请点击页面重新激活或使用键盘输入(F9)', 'warning'); |
| } |
| }; |
| } |
| |
| handleResult(event) { |
| this.updateStatus('processing'); |
| |
| let finalTranscript = ''; |
| let interimTranscript = ''; |
| let confidence = 0; |
| |
| |
| for (let i = event.resultIndex; i < event.results.length; i++) { |
| const transcript = event.results[i][0].transcript; |
| confidence = event.results[i][0].confidence || 0; |
| |
| if (event.results[i].isFinal) { |
| finalTranscript += transcript; |
| } else { |
| interimTranscript += transcript; |
| } |
| } |
| |
| |
| const currentText = finalTranscript || interimTranscript; |
| if (currentText) { |
| this.showVoiceInput(currentText, confidence, !finalTranscript); |
| } |
| |
| |
| if (finalTranscript && this.onResult) { |
| console.log('语音识别结果:', finalTranscript); |
| this.onResult(finalTranscript.trim()); |
| |
| |
| setTimeout(() => { |
| this.hideVoiceInput(); |
| }, 2000); |
| } |
| |
| this.updateStatus('ready'); |
| } |
| |
| handleError(error) { |
| console.error('语音识别错误:', error); |
| this.updateStatus('error'); |
| |
| |
| if (error !== 'no-speech') { |
| |
| const now = Date.now(); |
| if (now - this.lastErrorTime < 5000) { |
| this.consecutiveErrors++; |
| } else { |
| this.consecutiveErrors = 1; |
| } |
| this.lastErrorTime = now; |
| } |
| |
| if (this.onError) { |
| this.onError(error); |
| } |
| |
| |
| switch (error) { |
| case 'not-allowed': |
| this.showPermissionError(); |
| this.isDisabled = true; |
| break; |
| case 'no-speech': |
| |
| console.log('没有检测到语音(正常状态)- 继续监听'); |
| |
| |
| |
| break; |
| case 'network': |
| this.showNetworkError(); |
| console.log('网络错误,准备重试...'); |
| if (this.consecutiveErrors < 5) { |
| const delay = Math.min(3000 + (this.consecutiveErrors * 2000), 15000); |
| setTimeout(async () => { |
| if (this.isSupported && !this.isDisabled && !this.isListening) { |
| console.log('网络错误重试中...'); |
| await this.safeRestart(); |
| } |
| }, delay); |
| } else { |
| this.showNotification('网络连接持续异常,语音识别已暂停。请检查网络后双击页面重启', 'error'); |
| this.addDoubleClickRecovery(); |
| this.isDisabled = true; |
| } |
| break; |
| case 'aborted': |
| this.handleAbortedError(); |
| break; |
| default: |
| |
| console.log(`未知错误类型: ${error},准备重试...`); |
| if (this.consecutiveErrors < 4) { |
| const delay = this.retryDelay * Math.pow(1.5, this.consecutiveErrors - 1); |
| setTimeout(async () => { |
| if (this.isSupported && !this.isDisabled && !this.isListening) { |
| console.log(`未知错误重试中(第${this.consecutiveErrors}次)...`); |
| await this.safeRestart(); |
| } |
| }, delay); |
| } else { |
| this.showNotification(`语音识别遇到未知错误:${error}。已暂停,双击页面重启`, 'error'); |
| this.addDoubleClickRecovery(); |
| } |
| break; |
| } |
| } |
| |
| handleAbortedError() { |
| console.log('处理 aborted 错误,连续错误次数:', this.consecutiveErrors); |
| |
| |
| if (this.consecutiveErrors >= 5) { |
| this.handleAbortedErrorLoop(); |
| } else { |
| |
| this.forceCleanup(); |
| |
| |
| const baseDelay = 2000; |
| const delay = Math.min(baseDelay * Math.pow(2, this.consecutiveErrors - 1), 30000); |
| console.log(`Aborted 错误(第${this.consecutiveErrors}次),${delay}ms 后重试`); |
| |
| |
| setTimeout(async () => { |
| if (this.isSupported && !this.isDisabled && !this.isListening && !this.isStarting) { |
| console.log('重试启动语音识别...'); |
| await this.safeRestart(); |
| } else { |
| console.log('跳过重试,当前状态不允许启动'); |
| } |
| }, delay); |
| } |
| } |
| |
| handleAbortedErrorLoop() { |
| console.warn('检测到 aborted 错误循环,暂停自动重启'); |
| this.isDisabled = true; |
| |
| |
| this.showNotification( |
| '语音识别遇到连续错误已暂停。解决方案:\n' + |
| '1. 按Ctrl+R刷新页面\n' + |
| '2. 使用键盘输入(F9键)\n' + |
| '3. 检查麦克风权限\n' + |
| '4. 双击页面手动重启', |
| 'warning' |
| ); |
| |
| |
| this.addDoubleClickRecovery(); |
| |
| |
| setTimeout(() => { |
| this.resetErrorState(); |
| this.showNotification('语音识别已重新启用,双击页面激活', 'info'); |
| }, 120000); |
| } |
| |
| |
| addDoubleClickRecovery() { |
| if (this.doubleClickRecoveryAdded) return; |
| |
| const handleDoubleClick = async (event) => { |
| if (this.isDisabled) { |
| console.log('用户双击恢复语音识别'); |
| this.showNotification('正在重启语音识别...', 'info'); |
| |
| |
| this.resetErrorState(); |
| await this.forceCleanup(); |
| await new Promise(resolve => setTimeout(resolve, 1000)); |
| |
| if (this.isSupported) { |
| try { |
| await this.startListening(); |
| this.showNotification('语音识别已成功重启!', 'info'); |
| } catch (error) { |
| this.showNotification('重启失败,请稍后再试或刷新页面', 'error'); |
| } |
| } |
| } |
| }; |
| |
| document.addEventListener('dblclick', handleDoubleClick); |
| this.doubleClickRecoveryAdded = true; |
| |
| console.log('已添加双击恢复功能'); |
| } |
| |
| shouldAttemptRestart() { |
| |
| if (this.isDisabled) return false; |
| if (this.consecutiveErrors >= 5) return false; |
| |
| return true; |
| } |
| |
| calculateRestartDelay() { |
| |
| let baseDelay = this.retryDelay; |
| |
| |
| const multiplier = Math.min(this.consecutiveErrors, 5); |
| baseDelay *= multiplier; |
| |
| return Math.min(baseDelay, 30000); |
| } |
| |
| resetErrorState() { |
| |
| this.consecutiveErrors = 0; |
| this.retryCount = 0; |
| this.isDisabled = false; |
| this.lastErrorTime = 0; |
| console.log('错误状态已重置'); |
| } |
| |
| |
| forceCleanup() { |
| console.log('执行强制清理...'); |
| |
| |
| if (this.restartTimer) { |
| clearTimeout(this.restartTimer); |
| this.restartTimer = null; |
| } |
| |
| if (this.hideTimer) { |
| clearTimeout(this.hideTimer); |
| this.hideTimer = null; |
| } |
| |
| |
| if (this.recognition) { |
| try { |
| this.recognition.abort(); |
| } catch (error) { |
| console.log('强制停止时出错:', error); |
| } |
| } |
| |
| |
| this.isListening = false; |
| this.isStarting = false; |
| this.isStopping = false; |
| |
| console.log('强制清理完成'); |
| } |
| |
| |
| async safeRestart() { |
| console.log('执行安全重启...'); |
| |
| try { |
| |
| this.forceCleanup(); |
| |
| |
| await new Promise(resolve => setTimeout(resolve, 1000)); |
| |
| |
| if (this.isDisabled) { |
| console.log('语音识别已禁用,取消重启'); |
| return; |
| } |
| |
| if (this.isListening || this.isStarting) { |
| console.log('语音识别状态异常,取消重启'); |
| return; |
| } |
| |
| |
| if (this.recognition) { |
| console.log('重新配置语音识别...'); |
| this.setupRecognition(); |
| await new Promise(resolve => setTimeout(resolve, 500)); |
| |
| console.log('启动语音识别...'); |
| this.recognition.start(); |
| console.log('安全重启成功'); |
| } |
| |
| } catch (error) { |
| console.error('安全重启失败:', error); |
| this.consecutiveErrors++; |
| |
| |
| if (this.consecutiveErrors < 5) { |
| setTimeout(() => this.safeRestart(), 5000); |
| } |
| } |
| } |
| |
| async startListening() { |
| if (!this.isSupported) { |
| this.showUnsupportedError(); |
| return; |
| } |
| |
| if (this.isDisabled) { |
| console.log('语音识别已禁用,需要手动重启'); |
| return; |
| } |
| |
| |
| if (this.isStarting) { |
| console.log('语音识别正在启动中,等待完成...'); |
| |
| let attempts = 0; |
| while (this.isStarting && attempts < 10) { |
| await new Promise(resolve => setTimeout(resolve, 500)); |
| attempts++; |
| } |
| if (this.isStarting) { |
| console.error('启动超时,强制清理'); |
| this.forceCleanup(); |
| } |
| return; |
| } |
| |
| if (this.isListening) { |
| console.log('语音识别已在运行中,跳过启动'); |
| return; |
| } |
| |
| |
| if (this.isStopping) { |
| console.log('等待停止完成后重新启动...'); |
| let stopAttempts = 0; |
| while (this.isStopping && stopAttempts < 6) { |
| await new Promise(resolve => setTimeout(resolve, 500)); |
| stopAttempts++; |
| } |
| if (this.isStopping) { |
| console.log('停止超时,强制清理'); |
| this.forceCleanup(); |
| } |
| } |
| |
| this.isStarting = true; |
| console.log('开始启动语音识别...'); |
| |
| try { |
| |
| await this.ensureFullyStoppedAsync(); |
| |
| |
| await new Promise(resolve => setTimeout(resolve, 800)); |
| |
| |
| if (this.isDisabled) { |
| console.log('启动被取消:语音识别已禁用'); |
| return; |
| } |
| |
| if (this.isListening) { |
| console.log('启动被取消:语音识别已在运行'); |
| return; |
| } |
| |
| |
| console.log('尝试启动语音识别...'); |
| this.recognition.start(); |
| console.log('语音识别启动命令已发送'); |
| |
| } catch (error) { |
| console.error('启动语音识别失败:', error); |
| |
| |
| if (error.toString().includes('already started')) { |
| console.log('检测到重复启动错误,清理状态'); |
| this.forceCleanup(); |
| } else { |
| this.handleError('start-failed'); |
| } |
| } finally { |
| |
| setTimeout(() => { |
| this.isStarting = false; |
| }, 1000); |
| } |
| } |
| |
| |
| async ensureFullyStoppedAsync() { |
| if (!this.recognition) return; |
| |
| return new Promise((resolve) => { |
| if (!this.isListening) { |
| resolve(); |
| return; |
| } |
| |
| this.isStopping = true; |
| |
| |
| const onStopComplete = () => { |
| this.recognition.removeEventListener('end', onStopComplete); |
| this.isStopping = false; |
| this.isListening = false; |
| console.log('语音识别已完全停止'); |
| resolve(); |
| }; |
| |
| this.recognition.addEventListener('end', onStopComplete); |
| |
| |
| try { |
| this.recognition.abort(); |
| } catch (error) { |
| console.log('停止时出现错误:', error); |
| |
| setTimeout(() => { |
| this.isStopping = false; |
| this.isListening = false; |
| resolve(); |
| }, 1000); |
| } |
| |
| |
| setTimeout(() => { |
| if (this.isStopping) { |
| console.log('停止超时,强制完成'); |
| this.recognition.removeEventListener('end', onStopComplete); |
| this.isStopping = false; |
| this.isListening = false; |
| resolve(); |
| } |
| }, 3000); |
| }); |
| } |
| |
| async stopListening() { |
| if (this.recognition && this.isListening) { |
| console.log('停止语音识别...'); |
| |
| if (this.restartTimer) { |
| clearTimeout(this.restartTimer); |
| this.restartTimer = null; |
| } |
| |
| await this.ensureFullyStoppedAsync(); |
| } |
| } |
| |
| |
| async restart() { |
| console.log('手动重启语音识别系统...'); |
| |
| try { |
| |
| this.showNotification('正在重启语音识别...', 'info'); |
| |
| |
| this.resetErrorState(); |
| |
| |
| this.forceCleanup(); |
| |
| |
| await new Promise(resolve => setTimeout(resolve, 1500)); |
| |
| |
| if (!this.isSupported) { |
| this.showNotification('当前浏览器不支持语音识别', 'error'); |
| return; |
| } |
| |
| |
| this.setupRecognition(); |
| await new Promise(resolve => setTimeout(resolve, 500)); |
| |
| |
| if (!this.isDisabled) { |
| console.log('执行重启...'); |
| await this.startListening(); |
| this.showNotification('语音识别已成功重启!', 'info'); |
| } else { |
| this.showNotification('语音识别重启完成,但仍处于禁用状态', 'warning'); |
| } |
| |
| } catch (error) { |
| console.error('重启失败:', error); |
| this.showNotification('重启失败,建议刷新页面重试', 'error'); |
| |
| |
| this.resetErrorState(); |
| } |
| } |
| |
| |
| async manualActivate() { |
| console.log('手动激活语音识别'); |
| this.resetErrorState(); |
| |
| if (this.isSupported) { |
| await this.startListening(); |
| this.showNotification('语音识别已激活', 'info'); |
| } else { |
| this.showNotification('当前浏览器不支持语音识别', 'error'); |
| } |
| } |
| |
| |
| disable() { |
| console.log('禁用语音识别'); |
| this.isDisabled = true; |
| |
| if (this.recognition && this.isListening) { |
| this.recognition.abort(); |
| } |
| |
| this.showNotification('语音识别已禁用', 'warning'); |
| } |
| |
| |
| async enable() { |
| console.log('启用语音识别'); |
| this.resetErrorState(); |
| |
| if (this.isSupported) { |
| this.showNotification('语音识别已启用', 'info'); |
| await this.startListening(); |
| } |
| } |
| |
| updateStatus(status) { |
| if (this.onStatusChange) { |
| this.onStatusChange(status); |
| } |
| } |
| |
| showVoiceInput(text, confidence = 0, isInterim = false) { |
| const voiceDisplay = document.getElementById('voiceInputDisplay'); |
| const speechText = document.getElementById('speechText'); |
| const speechConfidence = document.getElementById('speechConfidence'); |
| const speechBubble = voiceDisplay.querySelector('.speech-bubble'); |
| |
| if (!voiceDisplay || !speechText) return; |
| |
| |
| voiceDisplay.style.display = 'block'; |
| |
| |
| speechText.textContent = text; |
| |
| |
| if (confidence > 0) { |
| speechConfidence.textContent = `置信度: ${Math.round(confidence * 100)}%`; |
| } else { |
| speechConfidence.textContent = isInterim ? '正在识别...' : ''; |
| } |
| |
| |
| speechBubble.className = 'speech-bubble'; |
| if (isInterim) { |
| speechBubble.classList.add('listening'); |
| } else { |
| speechBubble.classList.add('recognized'); |
| } |
| |
| |
| if (this.hideTimer) { |
| clearTimeout(this.hideTimer); |
| } |
| |
| |
| if (isInterim) { |
| this.hideTimer = setTimeout(() => { |
| this.hideVoiceInput(); |
| }, 3000); |
| } |
| } |
| |
| hideVoiceInput() { |
| const voiceDisplay = document.getElementById('voiceInputDisplay'); |
| if (voiceDisplay) { |
| voiceDisplay.style.display = 'none'; |
| } |
| |
| if (this.hideTimer) { |
| clearTimeout(this.hideTimer); |
| this.hideTimer = null; |
| } |
| } |
| |
| showPermissionError() { |
| this.showNotification('需要麦克风权限才能使用语音功能', 'error'); |
| } |
| |
| showNetworkError() { |
| this.showNotification('网络连接错误,语音识别暂时不可用', 'error'); |
| } |
| |
| showUnsupportedError() { |
| this.showNotification('您的浏览器不支持语音识别功能', 'error'); |
| } |
| |
| showNotification(message, type = 'info') { |
| |
| const notification = document.createElement('div'); |
| notification.className = `voice-notification ${type}`; |
| notification.textContent = message; |
| |
| |
| Object.assign(notification.style, { |
| position: 'fixed', |
| top: '20px', |
| right: '20px', |
| background: type === 'error' ? '#e74c3c' : '#4ecdc4', |
| color: 'white', |
| padding: '15px 20px', |
| borderRadius: '10px', |
| boxShadow: '0 4px 15px rgba(0,0,0,0.2)', |
| zIndex: '10000', |
| fontSize: '14px', |
| maxWidth: '300px', |
| animation: 'slideInRight 0.3s ease' |
| }); |
| |
| document.body.appendChild(notification); |
| |
| |
| setTimeout(() => { |
| notification.style.animation = 'slideOutRight 0.3s ease'; |
| setTimeout(() => { |
| if (notification.parentNode) { |
| notification.parentNode.removeChild(notification); |
| } |
| }, 300); |
| }, 5000); |
| } |
| |
| |
| test() { |
| if (!this.isSupported) { |
| console.log('语音识别不支持'); |
| return false; |
| } |
| |
| console.log('语音识别测试开始...'); |
| |
| |
| const originalOnResult = this.onResult; |
| this.onResult = (transcript) => { |
| console.log('测试结果:', transcript); |
| this.onResult = originalOnResult; |
| }; |
| |
| return true; |
| } |
| |
| |
| getSupportedLanguages() { |
| return [ |
| { code: 'en-US', name: 'English (US)' }, |
| { code: 'zh-CN', name: '中文 (普通话)' }, |
| { code: 'ja-JP', name: '日本語' }, |
| { code: 'ko-KR', name: '한국어' }, |
| { code: 'fr-FR', name: 'Français' }, |
| { code: 'de-DE', name: 'Deutsch' }, |
| { code: 'es-ES', name: 'Español' }, |
| { code: 'it-IT', name: 'Italiano' } |
| ]; |
| } |
| |
| |
| setLanguage(langCode) { |
| if (this.recognition) { |
| this.recognition.lang = langCode; |
| console.log(`语音识别语言切换为: ${langCode}`); |
| } |
| } |
| |
| |
| getStatus() { |
| return { |
| isSupported: this.isSupported, |
| isListening: this.isListening, |
| isDisabled: this.isDisabled, |
| isStarting: this.isStarting, |
| isStopping: this.isStopping, |
| consecutiveErrors: this.consecutiveErrors, |
| hasRestartTimer: !!this.restartTimer, |
| currentLanguage: this.recognition ? this.recognition.lang : null, |
| doubleClickRecoveryEnabled: this.doubleClickRecoveryAdded, |
| healthStatus: this.getHealthStatus() |
| }; |
| } |
| |
| |
| getHealthStatus() { |
| if (!this.isSupported) return 'unsupported'; |
| if (this.isDisabled) return 'disabled'; |
| if (this.consecutiveErrors >= 5) return 'critical'; |
| if (this.consecutiveErrors >= 3) return 'warning'; |
| if (this.isListening) return 'healthy'; |
| if (this.isStarting) return 'starting'; |
| return 'ready'; |
| } |
| |
| |
| async quickFix() { |
| console.log('执行快速诊断和修复...'); |
| |
| const status = this.getHealthStatus(); |
| console.log('当前健康状况:', status); |
| |
| switch (status) { |
| case 'unsupported': |
| this.showNotification('浏览器不支持语音识别功能', 'error'); |
| return false; |
| |
| case 'disabled': |
| case 'critical': |
| case 'warning': |
| console.log('检测到问题,执行完整重启...'); |
| await this.restart(); |
| return true; |
| |
| case 'starting': |
| console.log('正在启动中,等待完成...'); |
| return true; |
| |
| case 'ready': |
| console.log('系统就绪,尝试启动...'); |
| await this.startListening(); |
| return true; |
| |
| case 'healthy': |
| console.log('系统运行正常'); |
| return true; |
| |
| default: |
| console.log('未知状态,执行重启...'); |
| await this.restart(); |
| return true; |
| } |
| } |
| |
| |
| cleanup() { |
| console.log('清理语音识别资源...'); |
| |
| |
| if (this.restartTimer) { |
| clearTimeout(this.restartTimer); |
| this.restartTimer = null; |
| } |
| |
| if (this.hideTimer) { |
| clearTimeout(this.hideTimer); |
| this.hideTimer = null; |
| } |
| |
| |
| if (this.recognition) { |
| try { |
| this.recognition.abort(); |
| } catch (error) { |
| console.log('清理时停止识别出错:', error); |
| } |
| } |
| |
| |
| this.isListening = false; |
| this.isStarting = false; |
| this.isStopping = false; |
| this.isDisabled = true; |
| |
| console.log('语音识别资源清理完成'); |
| } |
| } |
|
|
| |
| const style = document.createElement('style'); |
| style.textContent = ` |
| @keyframes slideInRight { |
| from { |
| transform: translateX(100%); |
| opacity: 0; |
| } |
| to { |
| transform: translateX(0); |
| opacity: 1; |
| } |
| } |
| |
| @keyframes slideOutRight { |
| from { |
| transform: translateX(0); |
| opacity: 1; |
| } |
| to { |
| transform: translateX(100%); |
| opacity: 0; |
| } |
| } |
| |
| .voice-notification { |
| font-family: 'Comic Neue', cursive; |
| font-weight: bold; |
| word-wrap: break-word; |
| } |
| `; |
| document.head.appendChild(style); |