This commit is contained in:
2025-09-06 08:38:04 +08:00
parent 9bf1128f4a
commit ada13af11e
9 changed files with 169 additions and 92 deletions

View File

@@ -6,6 +6,7 @@ import json
import ssl import ssl
import time import time
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import base64
from datetime import datetime from datetime import datetime
from time import mktime from time import mktime
from urllib.parse import urlencode from urllib.parse import urlencode
@@ -163,10 +164,11 @@ class XunFeiAudioEvaluator:
# 查找read_chapter节点 # 查找read_chapter节点
read_chapter = root.find('.//read_chapter') read_chapter = root.find('.//read_chapter')
if read_chapter is not None: if read_chapter is not None:
# 保持字段名一致使用completeness_score
self.evaluation_results = { self.evaluation_results = {
'accuracy_score': float(read_chapter.get('accuracy_score', 0)), 'accuracy_score': float(read_chapter.get('accuracy_score', 0)),
'fluency_score': float(read_chapter.get('fluency_score', 0)), 'fluency_score': float(read_chapter.get('fluency_score', 0)),
'completeness_score': float(read_chapter.get('integrity_score', 0)), # 修复字段名 'completeness_score': float(read_chapter.get('integrity_score', 0)),
'standard_score': float(read_chapter.get('standard_score', 0)), 'standard_score': float(read_chapter.get('standard_score', 0)),
'total_score': float(read_chapter.get('total_score', 0)), 'total_score': float(read_chapter.get('total_score', 0)),
'word_count': int(read_chapter.get('word_count', 0)), 'word_count': int(read_chapter.get('word_count', 0)),
@@ -206,7 +208,8 @@ class XunFeiAudioEvaluator:
summary += f"总得分: {self.evaluation_results.get('total_score', 0):.4f}\n" summary += f"总得分: {self.evaluation_results.get('total_score', 0):.4f}\n"
summary += f"准确度得分: {self.evaluation_results.get('accuracy_score', 0):.4f}\n" summary += f"准确度得分: {self.evaluation_results.get('accuracy_score', 0):.4f}\n"
summary += f"流畅度得分: {self.evaluation_results.get('fluency_score', 0):.4f}\n" summary += f"流畅度得分: {self.evaluation_results.get('fluency_score', 0):.4f}\n"
summary += f"完整度得分: {self.evaluation_results.get('integrity_score', 0):.4f}\n" # 修复这里使用completeness_score
summary += f"完整度得分: {self.evaluation_results.get('completeness_score', 0):.4f}\n"
summary += f"标准度得分: {self.evaluation_results.get('standard_score', 0):.4f}\n" summary += f"标准度得分: {self.evaluation_results.get('standard_score', 0):.4f}\n"
summary += f"单词数量: {self.evaluation_results.get('word_count', 0)}\n" summary += f"单词数量: {self.evaluation_results.get('word_count', 0)}\n"
summary += f"是否被拒绝: {'' if self.evaluation_results.get('is_rejected', False) else ''}\n" summary += f"是否被拒绝: {'' if self.evaluation_results.get('is_rejected', False) else ''}\n"
@@ -269,8 +272,8 @@ if __name__ == '__main__':
appid = XF_APPID appid = XF_APPID
api_secret = XF_APISECRET api_secret = XF_APISECRET
api_key = XF_APIKEY api_key = XF_APIKEY
audio_file = "./1.mp3" #audio_file = "./1.mp3"
audio_file=r'D:\dsWork\dsProject\dsLightRag\static\audio\audio_1f0a8c47db2d4f9ba7674055a352cab8.wav'
# 创建评测器实例 # 创建评测器实例
evaluator = XunFeiAudioEvaluator(appid, api_key, api_secret, audio_file, "english") evaluator = XunFeiAudioEvaluator(appid, api_key, api_secret, audio_file, "english")

View File

@@ -4,6 +4,7 @@ import uuid
import tempfile import tempfile
import shutil import shutil
import sys import sys
import logging
from fastapi import APIRouter, UploadFile, File from fastapi import APIRouter, UploadFile, File
# 配置日志 # 配置日志
@@ -30,7 +31,13 @@ async def save_audio(audio: UploadFile = File(...)):
content = await audio.read() content = await audio.read()
with open(temp_file, "wb") as f: with open(temp_file, "wb") as f:
f.write(content) f.write(content)
# 3. 保存到正式目录
file_name = f"audio_{uuid.uuid4().hex}.wav"
file_path = os.path.join(UPLOAD_DIR, file_name)
shutil.copy2(temp_file, file_path)
logger.info(f"已保存文件到: {file_path}")
# 2. 讯飞评分 # 2. 讯飞评分
from KeDaXunFei.XunFeiAudioEvaluator import XunFeiAudioEvaluator from KeDaXunFei.XunFeiAudioEvaluator import XunFeiAudioEvaluator
evaluator = XunFeiAudioEvaluator( evaluator = XunFeiAudioEvaluator(
@@ -41,11 +48,8 @@ async def save_audio(audio: UploadFile = File(...)):
language="english" language="english"
) )
results, eval_time = evaluator.run_evaluation() results, eval_time = evaluator.run_evaluation()
print(evaluator.get_evaluation_summary())
# 3. 保存到正式目录
file_name = f"audio_{uuid.uuid4().hex}.wav"
file_path = os.path.join(UPLOAD_DIR, file_name)
shutil.copy2(temp_file, file_path)
return { return {
"success": True, "success": True,

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>在线录音机</title> <title>英语朗读评测</title>
<style> <style>
* { * {
margin: 0; margin: 0;
@@ -183,6 +183,78 @@
display: block; display: block;
} }
.evaluation-container {
margin-top: 20px;
padding: 20px;
background: rgba(30, 41, 59, 0.5);
border-radius: 10px;
display: none;
border: 1px solid rgba(94, 234, 212, 0.1);
}
.evaluation-container.show {
display: block;
}
.score-card {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.score-item {
text-align: center;
padding: 15px;
background: rgba(15, 23, 42, 0.7);
border-radius: 10px;
border: 1px solid rgba(94, 234, 212, 0.2);
}
.score-value {
font-size: 24px;
font-weight: bold;
color: #5eead4;
display: block;
}
.score-label {
font-size: 12px;
color: #cbd5e1;
margin-top: 5px;
}
.words-list {
max-height: 200px;
overflow-y: auto;
background: rgba(15, 23, 42, 0.7);
border-radius: 10px;
padding: 15px;
margin-top: 15px;
}
.word-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(94, 234, 212, 0.1);
}
.word-item:last-child {
border-bottom: none;
}
.word-content {
font-weight: 500;
color: #e2e8f0;
}
.word-score {
color: #5eead4;
font-weight: bold;
}
audio { audio {
width: 100%; width: 100%;
margin-bottom: 15px; margin-bottom: 15px;
@@ -416,6 +488,33 @@
</div> </div>
<div class="progress-text" id="progressText">上传进度: 0%</div> <div class="progress-text" id="progressText">上传进度: 0%</div>
</div> </div>
<!-- 评分结果显示区域 -->
<div class="evaluation-container" id="evaluationContainer">
<h3 style="color: #5eead4; margin-bottom: 15px;">📊 评测结果</h3>
<div class="score-card">
<div class="score-item">
<span class="score-value" id="totalScore">--</span>
<span class="score-label">总分</span>
</div>
<div class="score-item">
<span class="score-value" id="accuracyScore">--</span>
<span class="score-label">准确度</span>
</div>
<div class="score-item">
<span class="score-value" id="fluencyScore">--</span>
<span class="score-label">流利度</span>
</div>
<div class="score-item">
<span class="score-value" id="completenessScore">--</span>
<span class="score-label">完整度</span>
</div>
</div>
<div class="words-list" id="wordsList">
<h4 style="color: #5eead4; margin-bottom: 10px;">单词评分</h4>
<div id="wordsContent">等待评测结果...</div>
</div>
</div>
</div> </div>
<script> <script>
@@ -456,6 +555,13 @@
this.uploadProgress = document.getElementById('uploadProgress'); this.uploadProgress = document.getElementById('uploadProgress');
this.progressFill = document.getElementById('progressFill'); this.progressFill = document.getElementById('progressFill');
this.progressText = document.getElementById('progressText'); this.progressText = document.getElementById('progressText');
this.evaluationContainer = document.getElementById('evaluationContainer');
this.totalScore = document.getElementById('totalScore');
this.accuracyScore = document.getElementById('accuracyScore');
this.fluencyScore = document.getElementById('fluencyScore');
this.completenessScore = document.getElementById('completenessScore');
this.wordsList = document.getElementById('wordsList');
this.wordsContent = document.getElementById('wordsContent');
} }
setupCanvas() { setupCanvas() {
@@ -493,7 +599,6 @@
} }
}); });
// 设置音频上下文用于可视化
this.setupAudioContext(stream); this.setupAudioContext(stream);
const mimeType = this.getMimeType(); const mimeType = this.getMimeType();
@@ -523,19 +628,12 @@
this.updateButtons(); this.updateButtons();
}; };
this.mediaRecorder.start(1000); // 每秒触发一次dataavailable事件 this.mediaRecorder.start(1000);
} catch (error) { } catch (error) {
console.error('录音错误:', error); console.error('录音错误:', error);
this.updateStatus('无法访问麦克风,请检查权限设置', 'error'); this.updateStatus('无法访问麦克风,请检查权限设置', 'error');
alert('录音初始化失败: ' + error.message);
if (error.name === 'NotAllowedError') {
alert('请允许访问麦克风权限才能录音');
} else if (error.name === 'NotFoundError') {
alert('未找到麦克风设备');
} else {
alert('录音初始化失败: ' + error.message);
}
} }
} }
@@ -560,7 +658,6 @@
if (!this.isRecording) return; if (!this.isRecording) return;
this.animationId = requestAnimationFrame(draw); this.animationId = requestAnimationFrame(draw);
this.analyser.getByteFrequencyData(this.dataArray); this.analyser.getByteFrequencyData(this.dataArray);
this.canvasCtx.fillStyle = 'rgba(15, 23, 42, 0.7)'; this.canvasCtx.fillStyle = 'rgba(15, 23, 42, 0.7)';
@@ -573,7 +670,6 @@
for (let i = 0; i < this.dataArray.length; i++) { for (let i = 0; i < this.dataArray.length; i++) {
barHeight = (this.dataArray[i] / 255) * this.canvas.height * 0.8; barHeight = (this.dataArray[i] / 255) * this.canvas.height * 0.8;
// 创建渐变
const gradient = this.canvasCtx.createLinearGradient(0, this.canvas.height - barHeight, 0, this.canvas.height); const gradient = this.canvasCtx.createLinearGradient(0, this.canvas.height - barHeight, 0, this.canvas.height);
gradient.addColorStop(0, '#5eead4'); gradient.addColorStop(0, '#5eead4');
gradient.addColorStop(1, '#0ea5e9'); gradient.addColorStop(1, '#0ea5e9');
@@ -594,7 +690,6 @@
this.animationId = null; this.animationId = null;
} }
// 清空画布
this.canvasCtx.fillStyle = 'rgba(15, 23, 42, 0.7)'; this.canvasCtx.fillStyle = 'rgba(15, 23, 42, 0.7)';
this.canvasCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.canvasCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
} }
@@ -609,7 +704,6 @@
const mimeType = supportedTypes[format]; const mimeType = supportedTypes[format];
// 检查浏览器是否支持选定的格式
if (!MediaRecorder.isTypeSupported(mimeType)) { if (!MediaRecorder.isTypeSupported(mimeType)) {
console.warn(`${format} 格式不被支持,回退到 WebM`); console.warn(`${format} 格式不被支持,回退到 WebM`);
return 'audio/webm;codecs=opus'; return 'audio/webm;codecs=opus';
@@ -636,7 +730,6 @@
const url = URL.createObjectURL(this.recordedBlob); const url = URL.createObjectURL(this.recordedBlob);
this.audioPlayer.src = url; this.audioPlayer.src = url;
// 显示录音信息
const duration = Math.round((Date.now() - this.startTime) / 1000); const duration = Math.round((Date.now() - this.startTime) / 1000);
const sizeInKB = Math.round(this.recordedBlob.size / 1024); const sizeInKB = Math.round(this.recordedBlob.size / 1024);
@@ -645,7 +738,7 @@
this.audioContainer.classList.add('show'); this.audioContainer.classList.add('show');
this.updateStatus(`录音完成!时长: ${duration}秒, 大小: ${sizeInKB}KB`, 'success'); this.updateStatus(`录音完成!时长: ${duration}秒, 大小: ${sizeInKB}KB`, 'success');
this.updateButtons(); // 添加这一行来更新按钮状态 this.updateButtons();
} }
playRecording() { playRecording() {
@@ -662,31 +755,19 @@
this.uploadProgress.classList.add('show'); this.uploadProgress.classList.add('show');
const formData = new FormData(); const formData = new FormData();
// 根据选择的格式确定文件扩展名
const format = this.audioFormat.value; const format = this.audioFormat.value;
const extensions = { const extensions = {'webm': 'webm', 'mp4': 'mp4', 'wav': 'wav'};
'webm': 'webm',
'mp4': 'mp4',
'wav': 'wav'
};
const fileName = `recording_${Date.now()}.${extensions[format]}`; const fileName = `recording_${Date.now()}.${extensions[format]}`;
formData.append('audio', this.recordedBlob, fileName); formData.append('audio', this.recordedBlob, fileName);
formData.append('format', format);
formData.append('duration', this.duration.textContent);
formData.append('fileSize', this.fileSize.textContent);
try { try {
// 模拟上传进度
this.simulateUploadProgress(); this.simulateUploadProgress();
const response = await fetch('/api/xunFei/save-audio', { const response = await fetch('/api/xunFei/save-audio', {
method: 'POST', method: 'POST',
body: formData, body: formData,
headers: { headers: {'Accept': 'application/json'}
'Accept': 'application/json',
}
}); });
if (!response.ok) { if (!response.ok) {
@@ -694,32 +775,52 @@
} }
const result = await response.json(); const result = await response.json();
this.uploadProgress.classList.remove('show'); this.uploadProgress.classList.remove('show');
this.updateStatus('上传成功!', 'success');
if (result.success) {
console.log('上传结果:', result); this.updateStatus('评测完成!', 'success');
this.displayEvaluationResults(result.evaluation);
// 这里可以添加成功后的处理逻辑 console.log('评测结果:', result.evaluation);
if (result.audioUrl) {
alert(`录音上传成功!\n文件URL: ${result.audioUrl}\n文件名: ${fileName}`);
} else { } else {
alert('录音上传成功!'); throw new Error(result.error || '未知错误');
} }
} catch (error) { } catch (error) {
console.error('上传错误:', error); console.error('上传错误:', error);
this.uploadProgress.classList.remove('show'); this.uploadProgress.classList.remove('show');
this.updateStatus('上传失败: ' + error.message, 'error'); this.updateStatus('上传失败: ' + error.message, 'error');
alert('上传失败: ' + error.message);
}
}
// 更详细的错误处理 displayEvaluationResults(evaluation) {
if (error.name === 'TypeError' && error.message.includes('fetch')) { if (!evaluation) return;
alert('网络连接失败,请检查网络设置');
} else if (error.message.includes('Failed to fetch')) { // 显示评分卡片
alert('无法连接到服务器请检查API地址是否正确'); this.totalScore.textContent = evaluation.total_score ? evaluation.total_score.toFixed(1) : '--';
} else { this.accuracyScore.textContent = evaluation.accuracy_score ? evaluation.accuracy_score.toFixed(1) : '--';
alert('上传失败: ' + error.message); this.fluencyScore.textContent = evaluation.fluency_score ? evaluation.fluency_score.toFixed(1) : '--';
} this.completenessScore.textContent = evaluation.completeness_score ? evaluation.completeness_score.toFixed(1) : '--';
this.evaluationContainer.classList.add('show');
// 显示单词评分
if (evaluation.words && evaluation.words.length > 0) {
let wordsHTML = '';
evaluation.words.forEach((word, index) => {
const score = word.total_score ? word.total_score.toFixed(1) : '0.0';
const color = score >= 80 ? '#10b981' : score >= 60 ? '#f59e0b' : '#ef4444';
wordsHTML += `
<div class="word-item">
<span class="word-content">${word.content || `单词${index + 1}`}</span>
<span class="word-score" style="color: ${color}">${score}</span>
</div>
`;
});
this.wordsContent.innerHTML = wordsHTML;
} else {
this.wordsContent.innerHTML = '<p style="color: #cbd5e1; text-align: center;">无单词评分数据</p>';
} }
} }
@@ -728,7 +829,7 @@
const interval = setInterval(() => { const interval = setInterval(() => {
progress += Math.random() * 15; progress += Math.random() * 15;
if (progress >= 90) { if (progress >= 90) {
progress = 90; // 等待实际上传完成 progress = 90;
clearInterval(interval); clearInterval(interval);
return; return;
} }
@@ -772,7 +873,6 @@
this.status.className = 'status ' + type; this.status.className = 'status ' + type;
} }
// 清理资源
cleanup() { cleanup() {
this.stopTimer(); this.stopTimer();
this.stopVisualization(); this.stopVisualization();
@@ -791,38 +891,8 @@
} }
} }
// 初始化录音器
const recorder = new AudioRecorder(); const recorder = new AudioRecorder();
window.addEventListener('beforeunload', () => recorder.cleanup());
// 页面卸载时清理资源
window.addEventListener('beforeunload', () => {
recorder.cleanup();
});
// 处理页面可见性变化
document.addEventListener('visibilitychange', () => {
if (document.hidden && recorder.isRecording) {
console.log('页面隐藏,录音继续进行...');
}
});
// 添加键盘快捷键支持
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
if (!recorder.isRecording && !recorder.recordBtn.disabled) {
recorder.startRecording();
} else if (recorder.isRecording) {
recorder.stopRecording();
}
}
});
// 添加提示信息
console.log('🎙️ 在线录音机已加载完成!');
console.log('💡 快捷键:空格键 - 开始/停止录音');
console.log('📋 支持的格式WebM (推荐), MP4, WAV');
console.log('🔧 请确保已允许麦克风权限');
</script> </script>
</body> </body>
</html> </html>