This commit is contained in:
2025-09-05 21:01:11 +08:00
parent b6afe426d8
commit 851fc17339
7 changed files with 297 additions and 307 deletions

View File

@@ -12,6 +12,9 @@ from urllib.parse import urlencode
from wsgiref.handlers import format_date_time from wsgiref.handlers import format_date_time
import websocket import websocket
from Config.Config import XF_APPID, XF_APISECRET, XF_APIKEY
class XunFeiAudioEvaluator: class XunFeiAudioEvaluator:
"""讯飞语音评测类""" """讯飞语音评测类"""
@@ -251,9 +254,12 @@ class XunFeiAudioEvaluator:
# 使用示例 # 使用示例
if __name__ == '__main__': if __name__ == '__main__':
# 配置参数 # 配置参数
appid = "5b83f8d6" # appid = "5b83f8d6"
api_secret = "604fa6cb9c5ab664a0d153fe0ccc6802" # api_secret = "604fa6cb9c5ab664a0d153fe0ccc6802"
api_key = "5beb887923204000bfcb402046bb05a6" # api_key = "5beb887923204000bfcb402046bb05a6"
appid = XF_APPID
api_secret = XF_APISECRET
api_key = XF_APIKEY
audio_file = "./1.mp3" audio_file = "./1.mp3"
# 创建评测器实例 # 创建评测器实例

View File

@@ -2,6 +2,7 @@ import logging
import os import os
import uuid import uuid
import time import time
import asyncio # 添加此行
from fastapi import APIRouter, HTTPException, BackgroundTasks, Query, UploadFile, File, Form from fastapi import APIRouter, HTTPException, BackgroundTasks, Query, UploadFile, File, Form
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional from typing import Optional
@@ -37,14 +38,15 @@ class AudioEvaluationResponse(BaseModel):
# 科大讯飞API配置需要根据实际情况配置 # 科大讯飞API配置需要根据实际情况配置
XUNFEI_CONFIG = { XUNFEI_CONFIG = {
"appid": XF_APPID, "appid": XF_APPID,
"api_key": XF_APISECRET, # 修复参数名颠倒问题
"api_secret": XF_APIKEY "api_key": XF_APIKEY,
"api_secret": XF_APISECRET
} }
@router.post("/evaluate-audio", response_model=AudioEvaluationResponse) @router.post("/evaluate-audio", response_model=AudioEvaluationResponse)
async def evaluate_audio( async def evaluate_audio( # 添加async关键字
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
language: str = Form("chinese", description="评测语言: chineseenglish"), language: str = Form(..., description="语言类型: chinese/english"),
text: str = Form(..., description="评测文本内容"), text: str = Form(..., description="评测文本内容"),
group: str = Form("adult", description="群体类型: adult, youth, pupil"), group: str = Form("adult", description="群体类型: adult, youth, pupil"),
check_type: str = Form("common", description="检错严格程度: easy, common, hard"), check_type: str = Form("common", description="检错严格程度: easy, common, hard"),
@@ -58,6 +60,13 @@ async def evaluate_audio(
if language not in ["chinese", "english"]: if language not in ["chinese", "english"]:
raise HTTPException(status_code=400, detail="language参数必须是'chinese''english'") raise HTTPException(status_code=400, detail="language参数必须是'chinese''english'")
# 新增参数验证
if check_type not in ["easy", "common", "hard"]:
raise HTTPException(status_code=400, detail="check_type参数必须是'easy''common''hard'")
if grade not in ["junior", "middle", "senior"]:
raise HTTPException(status_code=400, detail="grade参数必须是'junior''middle''senior'")
# 验证群体参数 # 验证群体参数
if group not in ["adult", "youth", "pupil"]: if group not in ["adult", "youth", "pupil"]:
raise HTTPException(status_code=400, detail="group参数必须是'adult', 'youth''pupil'") raise HTTPException(status_code=400, detail="group参数必须是'adult', 'youth''pupil'")
@@ -71,6 +80,7 @@ async def evaluate_audio(
# 创建评测器实例 # 创建评测器实例
evaluator = XunFeiAudioEvaluator( evaluator = XunFeiAudioEvaluator(
appid=XUNFEI_CONFIG["appid"], appid=XUNFEI_CONFIG["appid"],
# 与AudioEvaluator示例用法保持一致
api_key=XUNFEI_CONFIG["api_key"], api_key=XUNFEI_CONFIG["api_key"],
api_secret=XUNFEI_CONFIG["api_secret"], api_secret=XUNFEI_CONFIG["api_secret"],
audio_file=temp_audio_path audio_file=temp_audio_path
@@ -96,7 +106,11 @@ async def evaluate_audio(
} }
# 运行评测 # 运行评测
results, eval_time = evaluator.run_evaluation() from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(max_workers=4)
results, eval_time = await asyncio.get_event_loop().run_in_executor(
executor, evaluator.run_evaluation
)
# 清理临时文件 # 清理临时文件
os.unlink(temp_audio_path) os.unlink(temp_audio_path)

View File

@@ -4,85 +4,7 @@
<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> <link rel="stylesheet" href="css/audio_evaluation.css">
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
select, input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
.btn {
background-color: #007bff;
color: white;
padding: 12px 24px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin-right: 10px;
}
.btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.btn-record {
background-color: #dc3545;
}
.btn-stop {
background-color: #28a745;
}
.status {
margin-top: 20px;
padding: 10px;
border-radius: 5px;
background-color: #e9ecef;
}
.result {
margin-top: 20px;
padding: 15px;
border: 1px solid #dee2e6;
border-radius: 5px;
background-color: #f8f9fa;
}
.error {
color: #dc3545;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
</style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
@@ -114,225 +36,6 @@
</div> </div>
</div> </div>
<script> <script src="js/audio_evaluation.js"></script>
let mediaRecorder;
let audioChunks = [];
let audioBlob;
// 获取DOM元素
const languageSelect = document.getElementById('language');
const textInput = document.getElementById('text');
const recordBtn = document.getElementById('recordBtn');
const stopBtn = document.getElementById('stopBtn');
// 删除提交按钮引用
const statusDiv = document.getElementById('status');
const resultDiv = document.getElementById('result');
const resultContent = document.getElementById('resultContent');
// 语言切换时自动填充示例文本
languageSelect.addEventListener('change', () => {
if (languageSelect.value === 'chinese') {
textInput.value = '窗前明月光,疑是地上霜。';
} else {
textInput.value = 'Nice to meet you.';
}
});
// 页面加载时自动填充中文示例
window.addEventListener('load', () => {
textInput.value = '窗前明月光,疑是地上霜。';
});
// 开始录音
recordBtn.addEventListener('click', async () => {
try {
statusDiv.textContent = '正在获取麦克风权限...';
statusDiv.className = 'status';
// ==== 插入WebSocket认证代码 ====
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.host}/ws/audio-evaluation`;
const ws = new WebSocket(wsUrl);
// WebSocket事件处理
ws.onopen = () => {
console.log('WebSocket连接已建立');
statusDiv.textContent = 'WebSocket连接已建立准备录音...';
};
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
statusDiv.textContent = 'WebSocket连接失败请刷新页面重试';
statusDiv.className = 'status error';
};
// ==== 插入结束 ====
// 使用更明确的提示并添加详细的错误处理
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
.catch(err => {
// 根据不同的错误类型提供更具体的提示
if (err.name === 'NotAllowedError') {
throw new Error('用户拒绝了麦克风访问权限。请在浏览器设置中允许麦克风访问,或刷新页面重试。');
} else if (err.name === 'NotFoundError') {
throw new Error('未检测到麦克风设备,请确保您的设备已正确连接麦克风。');
} else if (err.name === 'NotReadableError') {
throw new Error('麦克风设备正在被其他程序占用,请关闭其他可能使用麦克风的程序后重试。');
} else {
throw err;
}
});
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = (event) => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = () => {
audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
statusDiv.textContent = '录音完成,正在自动提交评测...';
// 添加WebSocket关闭逻辑
if (ws) ws.close(1000, '录音已完成');
submitEvaluation();
};
// 添加录音最大时长限制60秒
setTimeout(() => {
if (mediaRecorder && mediaRecorder.state === 'recording') {
alert('已达到最大录音时长60秒');
mediaRecorder.stop();
mediaRecorder.stream.getTracks().forEach(track => track.stop());
recordBtn.disabled = false;
stopBtn.disabled = true;
}
}, 60000);
mediaRecorder.start();
statusDiv.textContent = '正在录音...最多60秒';
recordBtn.disabled = true;
stopBtn.disabled = false;
} catch (error) {
console.error('录音失败:', error);
// 使用更明显的错误提示方式
alert('录音失败: ' + error.message);
statusDiv.textContent = '录音失败: ' + error.message;
statusDiv.className = 'status error';
// 确保按钮状态正确重置
recordBtn.disabled = false;
stopBtn.disabled = true;
}
});
// 停止录音
stopBtn.addEventListener('click', () => {
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
mediaRecorder.stream.getTracks().forEach(track => track.stop());
recordBtn.disabled = false;
stopBtn.disabled = true;
}
});
// 将提交评测逻辑提取为独立函数
async function submitEvaluation() {
if (!audioBlob) {
statusDiv.textContent = '请先完成录音';
statusDiv.className = 'status error';
return;
}
if (!textInput.value.trim()) {
statusDiv.textContent = '请输入评测文本内容';
statusDiv.className = 'status error';
return;
}
try {
statusDiv.textContent = '正在提交评测...';
statusDiv.className = 'status';
const formData = new FormData();
formData.append('audio_file', audioBlob, 'recording.webm');
formData.append('language', languageSelect.value);
formData.append('text', textInput.value.trim());
formData.append('group', 'adult');
formData.append('check_type', 'common');
formData.append('grade', 'middle');
const response = await fetch('/api/xunFei/evaluate-audio', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.status === 'success') {
displayResults(result.results);
statusDiv.textContent = '评测成功完成';
statusDiv.className = 'status success';
} else {
throw new Error(result.error_message || '评测失败');
}
} catch (error) {
console.error('评测失败:', error);
statusDiv.textContent = '评测失败: ' + error.message;
statusDiv.className = 'status error';
}
}
// 显示评测结果
function displayResults(results) {
resultDiv.style.display = 'block';
if (!results) {
resultContent.innerHTML = '<p>暂无评测结果</p>';
return;
}
let html = '<div class="result-summary">';
// 显示总分
if (results.total_score !== undefined) {
html += `<p><strong>总分:</strong> ${results.total_score.toFixed(4)} / 5.0</p>`;
}
// 显示各项分数
if (results.accuracy_score !== undefined) {
html += `<p><strong>准确度:</strong> ${results.accuracy_score.toFixed(4)}</p>`;
}
if (results.fluency_score !== undefined) {
html += `<p><strong>流利度:</strong> ${results.fluency_score.toFixed(4)}</p>`;
}
if (results.completeness_score !== undefined) {
html += `<p><strong>完整度:</strong> ${results.completeness_score.toFixed(4)}</p>`;
}
html += '</div>';
// 显示单词级评分(如果有)
if (results.words && results.words.length > 0) {
html += '<div class="word-scores"><h4>单词评分:</h4><ul>';
results.words.forEach(word => {
html += `<li>${word.content}: ${word.score.toFixed(4)}</li>`;
});
html += '</ul></div>';
}
resultContent.innerHTML = html;
}
// 页面卸载时关闭连接
window.addEventListener('beforeunload', () => {
if (ws) ws.close(1001, '页面即将关闭');
});
</script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,77 @@
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
select, input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
.btn {
background-color: #007bff;
color: white;
padding: 12px 24px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin-right: 10px;
}
.btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.btn-record {
background-color: #dc3545;
}
.btn-stop {
background-color: #28a745;
}
.status {
margin-top: 20px;
padding: 10px;
border-radius: 5px;
background-color: #e9ecef;
}
.result {
margin-top: 20px;
padding: 15px;
border: 1px solid #dee2e6;
border-radius: 5px;
background-color: #f8f9fa;
}
.error {
color: #dc3545;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}

View File

@@ -0,0 +1,190 @@
let mediaRecorder;
let audioChunks = [];
let audioBlob;
// 获取DOM元素
const languageSelect = document.getElementById('language');
const textInput = document.getElementById('text');
const recordBtn = document.getElementById('recordBtn');
const stopBtn = document.getElementById('stopBtn');
const statusDiv = document.getElementById('status');
const resultDiv = document.getElementById('result');
const resultContent = document.getElementById('resultContent');
// 语言切换时自动填充示例文本
languageSelect.addEventListener('change', () => {
if (languageSelect.value === 'chinese') {
textInput.value = '窗前明月光,疑是地上霜。';
} else {
textInput.value = 'Nice to meet you.';
}
});
// 页面加载时自动填充中文示例
window.addEventListener('load', () => {
textInput.value = '窗前明月光,疑是地上霜。';
stopBtn.disabled = true;
});
// 开始录音
recordBtn.addEventListener('click', async () => {
try {
statusDiv.textContent = '正在获取麦克风权限...';
statusDiv.className = 'status';
// 获取麦克风权限并处理可能的错误
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
.catch(err => {
if (err.name === 'NotAllowedError') {
throw new Error('用户拒绝了麦克风访问权限。请在浏览器设置中允许麦克风访问,或刷新页面重试。');
} else if (err.name === 'NotFoundError') {
throw new Error('未检测到麦克风设备,请确保您的设备已正确连接麦克风。');
} else if (err.name === 'NotReadableError') {
throw new Error('麦克风设备正在被其他程序占用,请关闭其他可能使用麦克风的程序后重试。');
} else {
throw err;
}
});
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = (event) => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = () => {
audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
statusDiv.textContent = '录音完成,正在自动提交评测...';
submitEvaluation();
// 停止所有音轨以释放麦克风
stream.getTracks().forEach(track => track.stop());
};
// 设置60秒自动停止录音
const maxRecordingTime = 60000; // 60秒
const timeoutId = setTimeout(() => {
if (mediaRecorder && mediaRecorder.state === 'recording') {
alert('已达到最大录音时长60秒');
mediaRecorder.stop();
recordBtn.disabled = false;
stopBtn.disabled = true;
}
}, maxRecordingTime);
mediaRecorder.start();
statusDiv.textContent = '正在录音...最多60秒';
recordBtn.disabled = true;
stopBtn.disabled = false;
} catch (error) {
console.error('录音初始化失败:', error);
alert('录音失败: ' + error.message);
statusDiv.textContent = '录音失败: ' + error.message;
statusDiv.className = 'status error';
recordBtn.disabled = false;
stopBtn.disabled = true;
}
});
// 停止录音
stopBtn.addEventListener('click', () => {
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
recordBtn.disabled = false;
stopBtn.disabled = true;
}
});
// 提交评测
async function submitEvaluation() {
if (!audioBlob) {
statusDiv.textContent = '请先完成录音';
statusDiv.className = 'status error';
return;
}
if (!textInput.value.trim()) {
statusDiv.textContent = '请输入评测文本内容';
statusDiv.className = 'status error';
return;
}
try {
statusDiv.textContent = '正在提交评测...';
statusDiv.className = 'status';
const formData = new FormData();
formData.append('audio_file', audioBlob, 'recording.webm');
formData.append('language', languageSelect.value);
formData.append('text', textInput.value.trim());
formData.append('group', 'adult');
formData.append('check_type', 'common');
formData.append('grade', 'middle');
const response = await fetch('/api/xunFei/evaluate-audio', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`服务器错误: HTTP状态码 ${response.status}`);
}
const result = await response.json();
if (result.status === 'success') {
displayResults(result.results);
statusDiv.textContent = '评测成功完成';
statusDiv.className = 'status success';
} else {
throw new Error(result.error_message || '评测失败,服务器返回未知错误');
}
} catch (error) {
console.error('评测提交失败:', error);
statusDiv.textContent = '评测失败: ' + error.message;
statusDiv.className = 'status error';
}
}
// 显示评测结果
function displayResults(results) {
resultDiv.style.display = 'block';
if (!results) {
resultContent.innerHTML = '<p>暂无评测结果</p>';
return;
}
let html = '<div class="result-summary">';
// 显示总分
if (results.total_score !== undefined) {
html += `<p><strong>总分:</strong> ${results.total_score.toFixed(4)} / 5.0</p>`;
}
// 显示各项评分
if (results.accuracy_score !== undefined) {
html += `<p><strong>准确度:</strong> ${results.accuracy_score.toFixed(4)}</p>`;
}
if (results.fluency_score !== undefined) {
html += `<p><strong>流利度:</strong> ${results.fluency_score.toFixed(4)}</p>`;
}
if (results.completeness_score !== undefined) {
html += `<p><strong>完整度:</strong> ${results.completeness_score.toFixed(4)}</p>`;
}
html += '</div>';
// 显示单词级评分
if (results.words && results.words.length > 0) {
html += '<div class="word-scores"><h4>单词评分:</h4><ul>';
results.words.forEach(word => {
html += `<li>${word.content}: ${word.score.toFixed(4)}</li>`;
});
html += '</ul></div>';
}
resultContent.innerHTML = html;
}