'commit'
This commit is contained in:
@@ -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"
|
||||||
|
|
||||||
# 创建评测器实例
|
# 创建评测器实例
|
||||||
|
Binary file not shown.
@@ -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="评测语言: chinese 或 english"),
|
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)
|
||||||
|
Binary file not shown.
@@ -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>
|
||||||
|
77
dsLightRag/static/XunFei/css/audio_evaluation.css
Normal file
77
dsLightRag/static/XunFei/css/audio_evaluation.css
Normal 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;
|
||||||
|
}
|
190
dsLightRag/static/XunFei/js/audio_evaluation.js
Normal file
190
dsLightRag/static/XunFei/js/audio_evaluation.js
Normal 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;
|
||||||
|
}
|
Reference in New Issue
Block a user