<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>英语发音评分系统</title> </head> <body> <div class="container"> <h2>英语发音评分系统</h2> <div class="sentence-selector"> <h3>选择练习句子</h3> <select id="sentenceSelect"> <option value="0">Hello, how are you today?</option> <option value="1">The weather is beautiful outside.</option> <option value="2">I love learning English every day.</option> <option value="3">Technology makes our lives easier.</option> <option value="4">Reading books expands our knowledge.</option> <option value="5">Practice makes perfect in everything.</option> </select> </div> <div class="practice-area"> <div class="target-sentence"> <h3>目标句子</h3> <p id="targetText">Hello, how are you today?</p> </div> <div class="recording-controls"> <button id="recordBtn" class="record-button"> <span class="btn-text">开始录音</span> <span class="recording-indicator"></span> </button> <p class="instruction">点击按钮开始录音,再次点击结束录音</p> </div> <div class="result-panel" id="resultPanel"> <h3>评分结果</h3> <div class="scores"> <div class="score-item"> <span class="score-label">流利度</span> <div class="score-bar"> <div class="score-fill" id="fluencyBar"></div> </div> <span class="score-value" id="fluencyScore">0</span> </div> <div class="score-item"> <span class="score-label">准确度</span> <div class="score-bar"> <div class="score-fill" id="accuracyBar"></div> </div> <span class="score-value" id="accuracyScore">0</span> </div> <div class="score-item total"> <span class="score-label">总得分</span> <div class="score-bar"> <div class="score-fill" id="totalBar"></div> </div> <span class="score-value" id="totalScore">0</span> </div> </div> <div class="feedback"> <p><strong>识别结果:</strong> <span id="recognizedText">--</span></p> <p><strong>建议:</strong> <span id="feedback">--</span></p> </div> </div> </div> <div class="history-panel"> <h3>练习历史</h3> <div id="historyList"></div> </div> </div> </body> </html>
* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Arial', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; } .container { max-width: 800px; margin: 0 auto; background: white; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); padding: 15px; } h2 { text-align: center; color: #333; margin-bottom: 30px; } .sentence-selector { margin-bottom: 30px; } .sentence-selector h3 { margin-bottom: 10px; color: #555; } #sentenceSelect { width: 100%; padding: 12px; font-size: 16px; border: 2px solid #ddd; border-radius: 8px; background: white; } .practice-area { text-align: center; margin-bottom: 30px; } .target-sentence { background: #f8f9fa; padding: 20px; border-radius: 10px; margin-bottom: 30px; border-left: 4px solid #667eea; } .target-sentence h3 { color: #555; margin-bottom: 10px; } #targetText { font-size: 1.4em; color: #333; font-weight: 500; line-height: 1.6; } .recording-controls { margin-bottom: 30px; } .record-button { width: 120px; height: 120px; border-radius: 50%; border: none; background: linear-gradient(45deg, #4CAF50, #45a049); color: white; font-size: 14px; font-weight: bold; cursor: pointer; transition: background 0.3s ease; position: relative; overflow: hidden; user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; } .record-button:hover:not(.recording):not(.disabled) { background: linear-gradient(45deg, #45a049, #4CAF50); } .record-button.recording { background: linear-gradient(45deg, #ff4757, #ff3838); } .record-button.disabled { background: #ccc; cursor: not-allowed; opacity: 0.6; } .instruction { margin-top: 15px; color: #666; font-size: 14px; } .recording-timer { margin-top: 10px; font-size: 16px; font-weight: bold; color: #ff4757; opacity: 0; transition: opacity 0.3s; } .recording-timer.active { opacity: 1; } .result-panel { background: #f8f9fa; padding: 15px; border-radius: 10px; margin-top: 20px; } .result-panel h3 { color: #333; margin-bottom: 20px; } .scores { margin-bottom: 20px; } .score-item { display: flex; align-items: center; margin-bottom: 15px; gap: 15px; } .score-item.total { padding-top: 15px; border-top: 2px solid #ddd; font-weight: bold; } .score-label { min-width: 80px; text-align: left; font-weight: 500; color: #555; } .score-bar { flex: 1; height: 20px; background: #e9ecef; border-radius: 10px; overflow: hidden; position: relative; } .score-fill { height: 100%; background: linear-gradient(90deg, #28a745, #20c997); border-radius: 10px; transition: width 0.8s ease; width: 0%; } .score-item.total .score-fill { background: linear-gradient(90deg, #667eea, #764ba2); } .score-value { min-width: 40px; text-align: right; font-weight: bold; color: #333; } .feedback { text-align: left; background: white; padding: 15px; border-radius: 8px; border-left: 4px solid #17a2b8; } .feedback p { margin-bottom: 8px; line-height: 1.5; } .history-panel { background: #f8f9fa; padding: 15px; border-radius: 10px; } .history-panel h3 { color: #333; margin-bottom: 15px; } .history-item { background: white; padding: 15px; margin-bottom: 10px; border-radius: 8px; border-left: 4px solid #6c757d; } .history-item .sentence { font-weight: bold; color: #333; margin-bottom: 5px; } .history-item .scores { font-size: 14px; color: #666; } .history-item .timestamp { font-size: 12px; color: #999; margin-top: 5px; }
class PronunciationScorer { constructor() { this.recognition = null; this.isRecording = false; this.startTime = null; this.endTime = null; this.manualStop = false; this.initializeSpeechRecognition(); } initializeSpeechRecognition() { if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { throw new Error('浏览器不支持语音识别功能'); } this.recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)(); this.recognition.continuous = true; // 改为连续识别 this.recognition.interimResults = true; // 启用中间结果 this.recognition.lang = 'en-US'; this.recognition.maxAlternatives = 1; } async startRecording() { return new Promise((resolve, reject) => { if (this.isRecording) { reject(new Error('正在录音中')); return; } this.isRecording = true; this.manualStop = false; this.startTime = Date.now(); let finalTranscript = ''; let interimTranscript = ''; let confidence = 0; this.recognition.onresult = (event) => { interimTranscript = ''; for (let i = event.resultIndex; i < event.results.length; i++) { const transcript = event.results[i][0].transcript; if (event.results[i].isFinal) { finalTranscript += transcript; confidence = Math.max(confidence, event.results[i][0].confidence); } else { interimTranscript += transcript; } } // 如果有最终结果且是手动停止,则解析结果 if (this.manualStop && finalTranscript.trim()) { this.endTime = Date.now(); resolve({ transcript: finalTranscript.trim(), confidence: confidence || 0.8, duration: this.endTime - this.startTime }); } }; this.recognition.onerror = (event) => { this.isRecording = false; if (event.error === 'no-speech') { reject(new Error('未检测到语音输入,请重试')); } else if (event.error === 'audio-capture') { reject(new Error('无法访问麦克风,请检查权限')); } else if (event.error === 'not-allowed') { reject(new Error('麦克风权限被拒绝')); } else { reject(new Error(`语音识别错误: ${event.error}`)); } }; this.recognition.onend = () => { this.isRecording = false; // 如果是手动停止且有结果,处理结果 if (this.manualStop) { if (finalTranscript.trim()) { // 结果已在 onresult 中处理 return; } else { // 没有识别到内容 this.endTime = Date.now(); resolve({ transcript: '', confidence: 0, duration: this.endTime - this.startTime }); } } else { // 非手动停止,可能是因为超时或其他原因 if (finalTranscript.trim()) { this.endTime = Date.now(); resolve({ transcript: finalTranscript.trim(), confidence: confidence || 0.8, duration: this.endTime - this.startTime }); } else { reject(new Error('录音意外结束,请重试')); } } }; try { this.recognition.start(); } catch (error) { this.isRecording = false; reject(error); } }); } stopRecording() { if (this.recognition && this.isRecording) { this.manualStop = true; this.recognition.stop(); } } analyzePronunciation(targetText, recognizedText, confidence, duration) { // 1. 计算准确度 const accuracy = this.calculateAccuracy(targetText, recognizedText); // 2. 计算流利度 const fluency = this.calculateFluency(targetText, duration, confidence); // 3. 计算总得分 const totalScore = this.calculateTotalScore(accuracy, fluency, confidence); // 4. 生成反馈 const feedback = this.generateFeedback(accuracy, fluency, totalScore); return { accuracy: Math.round(accuracy), fluency: Math.round(fluency), totalScore: Math.round(totalScore), feedback, recognizedText, targetText, confidence: Math.round(confidence * 100), duration }; } calculateAccuracy(target, recognized) { const targetWords = this.normalizeText(target).split(' '); const recognizedWords = this.normalizeText(recognized).split(' '); // 计算单词级别的准确度 const wordAccuracy = this.calculateWordAccuracy(targetWords, recognizedWords); // 计算字符级别的相似度 const charSimilarity = this.calculateStringSimilarity(target, recognized); // 综合评分 (70% 单词准确度 + 30% 字符相似度) return (wordAccuracy * 0.7 + charSimilarity * 0.3) * 100; } calculateWordAccuracy(targetWords, recognizedWords) { if (targetWords.length === 0) return 0; let matchCount = 0; const maxLength = Math.max(targetWords.length, recognizedWords.length); // 使用动态规划算法计算最佳匹配 for (let i = 0; i < Math.min(targetWords.length, recognizedWords.length); i++) { if (this.wordsMatch(targetWords[i], recognizedWords[i])) { matchCount++; } } // 考虑长度差异的惩罚 const lengthPenalty = Math.abs(targetWords.length - recognizedWords.length) * 0.1; const accuracy = (matchCount / targetWords.length) - lengthPenalty; return Math.max(0, Math.min(1, accuracy)); } wordsMatch(word1, word2) { // 完全匹配 if (word1 === word2) return true; // 音似匹配 (简化版) const similarity = this.calculateStringSimilarity(word1, word2); return similarity > 0.8; } calculateFluency(targetText, duration, confidence) { const targetWords = this.normalizeText(targetText).split(' '); const expectedDuration = this.calculateExpectedDuration(targetWords); // 计算语速得分 (理想语速 vs 实际语速) const speedScore = this.calculateSpeedScore(duration, expectedDuration); // 计算置信度得分 const confidenceScore = confidence * 100; // 综合流利度得分 (60% 语速 + 40% 置信度) return speedScore * 0.6 + confidenceScore * 0.4; } calculateExpectedDuration(words) { // 平均每个单词 600ms,加上停顿时间 const baseTime = words.length * 600; const pauseTime = (words.length - 1) * 100; return baseTime + pauseTime; } calculateSpeedScore(actualDuration, expectedDuration) { const ratio = actualDuration / expectedDuration; // 理想语速范围 0.8-1.2倍 if (ratio >= 0.8 && ratio <= 1.2) { return 100; } else if (ratio >= 0.6 && ratio <= 1.5) { return 80; } else if (ratio >= 0.4 && ratio <= 2.0) { return 60; } else { return 40; } } calculateTotalScore(accuracy, fluency, confidence) { // 权重: 准确度 50%, 流利度 30%, 置信度 20% return (accuracy * 0.5) + (fluency * 0.3) + (confidence * 100 * 0.2); } generateFeedback(accuracy, fluency, totalScore) { let feedback = []; if (accuracy < 60) { feedback.push("发音准确度需要提高,建议多练习单词发音"); } else if (accuracy < 80) { feedback.push("发音基本准确,继续练习可以更好"); } else { feedback.push("发音很准确,保持下去!"); } if (fluency < 60) { feedback.push("语速需要调整,建议放慢语速确保清晰度"); } else if (fluency < 80) { feedback.push("流利度不错,可以尝试更自然的语调"); } else { feedback.push("说得很流利!"); } if (totalScore >= 85) { feedback.push("总体表现优秀!"); } else if (totalScore >= 70) { feedback.push("总体表现良好,继续努力!"); } else { feedback.push("继续练习,你会越来越好的!"); } return feedback.join(" "); } normalizeText(text) { return text.toLowerCase() .replace(/[^\w\s]/gi, '') .replace(/\s+/g, ' ') .trim(); } calculateStringSimilarity(str1, str2) { const longer = str1.length > str2.length ? str1 : str2; const shorter = str1.length > str2.length ? str2 : str1; if (longer.length === 0) return 1.0; const editDistance = this.levenshteinDistance(longer, shorter); return (longer.length - editDistance) / longer.length; } levenshteinDistance(str1, str2) { const matrix = []; for (let i = 0; i <= str2.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= str1.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { if (str2.charAt(i - 1) === str1.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } return matrix[str2.length][str1.length]; } } // filepath: app.js class PronunciationApp { constructor() { this.sentences = [ "Hello, how are you today?", "The weather is beautiful outside.", "I love learning English every day.", "Technology makes our lives easier.", "Reading books expands our knowledge.", "Practice makes perfect in everything." ]; this.currentSentence = 0; this.scorer = new PronunciationScorer(); this.history = this.loadHistory(); this.isProcessing = false; this.recordingTimer = null; this.recordingStartTime = null; this.initializeElements(); this.bindEvents(); this.updateUI(); } initializeElements() { this.sentenceSelect = document.getElementById('sentenceSelect'); this.targetText = document.getElementById('targetText'); this.recordBtn = document.getElementById('recordBtn'); this.resultPanel = document.getElementById('resultPanel'); this.fluencyBar = document.getElementById('fluencyBar'); this.accuracyBar = document.getElementById('accuracyBar'); this.totalBar = document.getElementById('totalBar'); this.fluencyScore = document.getElementById('fluencyScore'); this.accuracyScore = document.getElementById('accuracyScore'); this.totalScore = document.getElementById('totalScore'); this.recognizedText = document.getElementById('recognizedText'); this.feedback = document.getElementById('feedback'); this.historyList = document.getElementById('historyList'); // 添加录音计时器元素 const timerElement = document.createElement('div'); timerElement.id = 'recordingTimer'; timerElement.className = 'recording-timer'; timerElement.textContent = '00:00'; this.recordBtn.parentNode.appendChild(timerElement); this.recordingTimerElement = timerElement; } bindEvents() { // 句子选择 this.sentenceSelect.addEventListener('change', (e) => { this.currentSentence = parseInt(e.target.value); this.updateUI(); }); // 录音按钮点击事件 this.recordBtn.addEventListener('click', (e) => { e.preventDefault(); this.toggleRecording(); }); // 键盘快捷键 - 空格键切换录音状态 document.addEventListener('keydown', (e) => { if (e.code === 'Space' && !e.repeat) { e.preventDefault(); if (!this.isProcessing) { this.toggleRecording(); } } }); // 防止右键菜单 this.recordBtn.addEventListener('contextmenu', (e) => { e.preventDefault(); }); } updateUI() { this.targetText.textContent = this.sentences[this.currentSentence]; this.updateHistory(); } toggleRecording() { if (this.isProcessing) return; if (this.scorer.isRecording) { this.stopRecording(); } else { this.startRecording(); } } async startRecording() { if (this.scorer.isRecording || this.isProcessing) return; try { this.recordBtn.classList.add('recording'); this.recordBtn.classList.remove('disabled'); this.recordBtn.querySelector('.btn-text').textContent = '点击停止'; // 显示录音计时器 this.recordingTimerElement.classList.add('active'); this.recordingStartTime = Date.now(); this.startTimer(); // 显示录音状态提示 this.showRecordingStatus('正在录音中...'); const result = await this.scorer.startRecording(); this.processRecordingResult(result); } catch (error) { console.error('录音错误:', error); this.showError('录音失败: ' + error.message); this.resetRecordButton(); } finally { this.hideRecordingStatus(); } } startTimer() { this.recordingTimer = setInterval(() => { const elapsed = Date.now() - this.recordingStartTime; const seconds = Math.floor(elapsed / 1000); const minutes = Math.floor(seconds / 60); const displaySeconds = seconds % 60; this.recordingTimerElement.textContent = `${minutes.toString().padStart(2, '0')}:${displaySeconds.toString().padStart(2, '0')}`; // 录音超过60秒自动停止 if (seconds >= 60) { this.stopRecording(); this.showError('录音时间过长,已自动停止'); } }, 100); } showRecordingStatus(message) { if (!this.statusDiv) { this.statusDiv = document.createElement('div'); this.statusDiv.style.cssText = ` position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%); background: rgba(0, 0, 0, 0.8); color: white; padding: 15px 25px; border-radius: 10px; font-size: 16px; z-index: 1001; text-align: center; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); max-width: 300px; `; document.body.appendChild(this.statusDiv); } this.statusDiv.textContent = message; this.statusDiv.style.display = 'block'; } hideRecordingStatus() { if (this.statusDiv) { this.statusDiv.style.display = 'none'; } } stopRecording() { if (this.scorer.isRecording) { this.scorer.stopRecording(); this.isProcessing = true; this.recordBtn.classList.add('disabled'); this.recordBtn.querySelector('.btn-text').textContent = '处理中...'; this.showRecordingStatus('正在处理录音...'); // 停止计时器 this.stopTimer(); } } stopTimer() { if (this.recordingTimer) { clearInterval(this.recordingTimer); this.recordingTimer = null; } this.recordingTimerElement.classList.remove('active'); this.recordingTimerElement.textContent = '00:00'; } resetRecordButton() { this.recordBtn.classList.remove('recording', 'disabled'); this.recordBtn.querySelector('.btn-text').textContent = '开始录音'; this.isProcessing = false; this.stopTimer(); } processRecordingResult(result) { const targetText = this.sentences[this.currentSentence]; const analysis = this.scorer.analyzePronunciation( targetText, result.transcript, result.confidence, result.duration ); this.displayResults(analysis); this.saveToHistory(analysis); // 重置按钮状态 this.resetRecordButton(); } displayResults(analysis) { // 显示结果面板 this.resultPanel.style.display = 'block'; // 更新得分条 this.animateScoreBar(this.fluencyBar, analysis.fluency); this.animateScoreBar(this.accuracyBar, analysis.accuracy); this.animateScoreBar(this.totalBar, analysis.totalScore); // 更新得分数字 this.animateScoreValue(this.fluencyScore, analysis.fluency); this.animateScoreValue(this.accuracyScore, analysis.accuracy); this.animateScoreValue(this.totalScore, analysis.totalScore); // 更新文本信息 this.recognizedText.textContent = analysis.recognizedText || '未识别到内容'; this.feedback.textContent = analysis.feedback; // 滚动到结果区域 this.resultPanel.scrollIntoView({ behavior: 'smooth' }); } animateScoreBar(element, score) { setTimeout(() => { element.style.width = score + '%'; // 根据得分设置颜色 if (score >= 80) { element.style.background = 'linear-gradient(90deg, #28a745, #20c997)'; } else if (score >= 60) { element.style.background = 'linear-gradient(90deg, #ffc107, #ffb300)'; } else { element.style.background = 'linear-gradient(90deg, #dc3545, #c82333)'; } }, 100); } animateScoreValue(element, score) { let current = 0; const increment = score / 30; const timer = setInterval(() => { current += increment; if (current >= score) { current = score; clearInterval(timer); } element.textContent = Math.round(current); }, 50); } saveToHistory(analysis) { const historyItem = { sentence: analysis.targetText, recognized: analysis.recognizedText, accuracy: analysis.accuracy, fluency: analysis.fluency, totalScore: analysis.totalScore, timestamp: new Date().toLocaleString('zh-CN') }; this.history.unshift(historyItem); // 只保留最近20条记录 if (this.history.length > 20) { this.history = this.history.slice(0, 20); } localStorage.setItem('pronunciation-history', JSON.stringify(this.history)); this.updateHistory(); } loadHistory() { const saved = localStorage.getItem('pronunciation-history'); return saved ? JSON.parse(saved) : []; } updateHistory() { this.historyList.innerHTML = ''; if (this.history.length === 0) { this.historyList.innerHTML = '<p style="color: #666; text-align: center;">暂无练习记录</p>'; return; } this.history.forEach((item, index) => { const historyItem = document.createElement('div'); historyItem.className = 'history-item'; historyItem.innerHTML = ` <div class="sentence">${item.sentence}</div> <div class="scores"> 准确度: ${item.accuracy}分 | 流利度: ${item.fluency}分 | 总分: ${item.totalScore}分 </div> <div class="timestamp">${item.timestamp}</div> `; this.historyList.appendChild(historyItem); }); } showError(message) { // 重置按钮状态 this.resetRecordButton(); // 创建更友好的错误提示 const errorDiv = document.createElement('div'); errorDiv.style.cssText = ` position: fixed; top: 20px; right: 20px; background: #ff4757; color: white; padding: 15px 20px; border-radius: 8px; font-size: 14px; z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.2); animation: slideIn 0.3s ease; max-width: 300px; `; errorDiv.innerHTML = ` <div style="display: flex; align-items: center; gap: 10px;"> <span>⚠️</span> <span>${message}</span> </div> `; document.body.appendChild(errorDiv); // 3秒后自动消失 setTimeout(() => { if (document.body.contains(errorDiv)) { errorDiv.style.animation = 'slideOut 0.3s ease'; setTimeout(() => { if (document.body.contains(errorDiv)) { document.body.removeChild(errorDiv); } }, 300); } }, 3000); // 添加滑入滑出动画 if (!document.querySelector('#error-animations')) { const style = document.createElement('style'); style.id = 'error-animations'; style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } `; document.head.appendChild(style); } } } // 初始化应用 document.addEventListener('DOMContentLoaded', () => { try { new PronunciationApp(); } catch (error) { console.error('应用初始化失败:', error); document.body.innerHTML = ` <div style="text-align: center; padding: 50px;"> <h2>抱歉,您的浏览器不支持语音识别功能</h2> <p>请使用Chrome、Edge或Safari浏览器</p> </div> `; } });