<!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>
`;
}
});