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 time
import xml.etree.ElementTree as ET
import base64
from datetime import datetime
from time import mktime
from urllib.parse import urlencode
@@ -163,10 +164,11 @@ class XunFeiAudioEvaluator:
# 查找read_chapter节点
read_chapter = root.find('.//read_chapter')
if read_chapter is not None:
# 保持字段名一致使用completeness_score
self.evaluation_results = {
'accuracy_score': float(read_chapter.get('accuracy_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)),
'total_score': float(read_chapter.get('total_score', 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('accuracy_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('word_count', 0)}\n"
summary += f"是否被拒绝: {'' if self.evaluation_results.get('is_rejected', False) else ''}\n"
@@ -269,8 +272,8 @@ if __name__ == '__main__':
appid = XF_APPID
api_secret = XF_APISECRET
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")

View File

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

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>在线录音机</title>
<title>英语朗读评测</title>
<style>
* {
margin: 0;
@@ -183,6 +183,78 @@
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 {
width: 100%;
margin-bottom: 15px;
@@ -416,6 +488,33 @@
</div>
<div class="progress-text" id="progressText">上传进度: 0%</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>
<script>
@@ -456,6 +555,13 @@
this.uploadProgress = document.getElementById('uploadProgress');
this.progressFill = document.getElementById('progressFill');
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() {
@@ -493,7 +599,6 @@
}
});
// 设置音频上下文用于可视化
this.setupAudioContext(stream);
const mimeType = this.getMimeType();
@@ -523,19 +628,12 @@
this.updateButtons();
};
this.mediaRecorder.start(1000); // 每秒触发一次dataavailable事件
this.mediaRecorder.start(1000);
} catch (error) {
console.error('录音错误:', error);
this.updateStatus('无法访问麦克风,请检查权限设置', 'error');
if (error.name === 'NotAllowedError') {
alert('请允许访问麦克风权限才能录音');
} else if (error.name === 'NotFoundError') {
alert('未找到麦克风设备');
} else {
alert('录音初始化失败: ' + error.message);
}
alert('录音初始化失败: ' + error.message);
}
}
@@ -560,7 +658,6 @@
if (!this.isRecording) return;
this.animationId = requestAnimationFrame(draw);
this.analyser.getByteFrequencyData(this.dataArray);
this.canvasCtx.fillStyle = 'rgba(15, 23, 42, 0.7)';
@@ -573,7 +670,6 @@
for (let i = 0; i < this.dataArray.length; i++) {
barHeight = (this.dataArray[i] / 255) * this.canvas.height * 0.8;
// 创建渐变
const gradient = this.canvasCtx.createLinearGradient(0, this.canvas.height - barHeight, 0, this.canvas.height);
gradient.addColorStop(0, '#5eead4');
gradient.addColorStop(1, '#0ea5e9');
@@ -594,7 +690,6 @@
this.animationId = null;
}
// 清空画布
this.canvasCtx.fillStyle = 'rgba(15, 23, 42, 0.7)';
this.canvasCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
@@ -609,7 +704,6 @@
const mimeType = supportedTypes[format];
// 检查浏览器是否支持选定的格式
if (!MediaRecorder.isTypeSupported(mimeType)) {
console.warn(`${format} 格式不被支持,回退到 WebM`);
return 'audio/webm;codecs=opus';
@@ -636,7 +730,6 @@
const url = URL.createObjectURL(this.recordedBlob);
this.audioPlayer.src = url;
// 显示录音信息
const duration = Math.round((Date.now() - this.startTime) / 1000);
const sizeInKB = Math.round(this.recordedBlob.size / 1024);
@@ -645,7 +738,7 @@
this.audioContainer.classList.add('show');
this.updateStatus(`录音完成!时长: ${duration}秒, 大小: ${sizeInKB}KB`, 'success');
this.updateButtons(); // 添加这一行来更新按钮状态
this.updateButtons();
}
playRecording() {
@@ -662,31 +755,19 @@
this.uploadProgress.classList.add('show');
const formData = new FormData();
// 根据选择的格式确定文件扩展名
const format = this.audioFormat.value;
const extensions = {
'webm': 'webm',
'mp4': 'mp4',
'wav': 'wav'
};
const extensions = {'webm': 'webm', 'mp4': 'mp4', 'wav': 'wav'};
const fileName = `recording_${Date.now()}.${extensions[format]}`;
formData.append('audio', this.recordedBlob, fileName);
formData.append('format', format);
formData.append('duration', this.duration.textContent);
formData.append('fileSize', this.fileSize.textContent);
try {
// 模拟上传进度
this.simulateUploadProgress();
const response = await fetch('/api/xunFei/save-audio', {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json',
}
headers: {'Accept': 'application/json'}
});
if (!response.ok) {
@@ -694,32 +775,52 @@
}
const result = await response.json();
this.uploadProgress.classList.remove('show');
this.updateStatus('上传成功!', 'success');
console.log('上传结果:', result);
// 这里可以添加成功后的处理逻辑
if (result.audioUrl) {
alert(`录音上传成功!\n文件URL: ${result.audioUrl}\n文件名: ${fileName}`);
if (result.success) {
this.updateStatus('评测完成!', 'success');
this.displayEvaluationResults(result.evaluation);
console.log('评测结果:', result.evaluation);
} else {
alert('录音上传成功!');
throw new Error(result.error || '未知错误');
}
} catch (error) {
console.error('上传错误:', error);
this.uploadProgress.classList.remove('show');
this.updateStatus('上传失败: ' + error.message, 'error');
alert('上传失败: ' + error.message);
}
}
// 更详细的错误处理
if (error.name === 'TypeError' && error.message.includes('fetch')) {
alert('网络连接失败,请检查网络设置');
} else if (error.message.includes('Failed to fetch')) {
alert('无法连接到服务器请检查API地址是否正确');
} else {
alert('上传失败: ' + error.message);
}
displayEvaluationResults(evaluation) {
if (!evaluation) return;
// 显示评分卡片
this.totalScore.textContent = evaluation.total_score ? evaluation.total_score.toFixed(1) : '--';
this.accuracyScore.textContent = evaluation.accuracy_score ? evaluation.accuracy_score.toFixed(1) : '--';
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(() => {
progress += Math.random() * 15;
if (progress >= 90) {
progress = 90; // 等待实际上传完成
progress = 90;
clearInterval(interval);
return;
}
@@ -772,7 +873,6 @@
this.status.className = 'status ' + type;
}
// 清理资源
cleanup() {
this.stopTimer();
this.stopVisualization();
@@ -791,38 +891,8 @@
}
}
// 初始化录音器
const recorder = new AudioRecorder();
// 页面卸载时清理资源
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('🔧 请确保已允许麦克风权限');
window.addEventListener('beforeunload', () => recorder.cleanup());
</script>
</body>
</html>