This commit is contained in:
2025-09-05 22:01:55 +08:00
parent ae78c41f86
commit 4028e54629
7 changed files with 903 additions and 228 deletions

View File

@@ -3,34 +3,823 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>语音评测系统</title>
<link rel="stylesheet" href="css/audio_evaluation.css">
<title>在线录音机</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #0f172a, #1e293b);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
color: #e2e8f0;
}
.container {
background: rgba(15, 23, 42, 0.7);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(94, 234, 212, 0.1);
padding: 40px;
max-width: 600px;
width: 100%;
border: 1px solid rgba(94, 234, 212, 0.1);
}
h1 {
text-align: center;
color: #5eead4;
margin-bottom: 30px;
font-size: 2.5em;
font-weight: 700;
letter-spacing: 1px;
text-shadow: 0 0 10px rgba(94, 234, 212, 0.3);
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 30px;
flex-wrap: wrap;
}
button {
padding: 12px 24px;
border: none;
border-radius: 50px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
button::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transform: translateX(-100%);
transition: 0.5s;
}
button:hover::before {
transform: translateX(100%);
}
.btn-record {
background: linear-gradient(45deg, #ef4444, #dc2626);
color: white;
}
.btn-record:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(220, 38, 38, 0.3);
}
.btn-stop {
background: linear-gradient(45deg, #f59e0b, #d97706);
color: white;
}
.btn-stop:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(217, 119, 6, 0.3);
}
.btn-play {
background: linear-gradient(45deg, #10b981, #059669);
color: white;
}
.btn-play:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(16, 185, 129, 0.3);
}
.btn-upload {
background: linear-gradient(45deg, #3b82f6, #2563eb);
color: white;
}
.btn-upload:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(37, 99, 235, 0.3);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
box-shadow: none;
}
.status {
text-align: center;
margin-bottom: 20px;
padding: 15px;
border-radius: 10px;
font-weight: 500;
background: rgba(30, 41, 59, 0.5);
border: 1px solid rgba(94, 234, 212, 0.1);
}
.status.recording {
background: rgba(220, 38, 38, 0.15);
color: #fecaca;
border: 1px solid rgba(220, 38, 38, 0.3);
}
.status.stopped {
background: rgba(217, 119, 6, 0.15);
color: #fde68a;
border: 1px solid rgba(217, 119, 6, 0.3);
}
.status.uploading {
background: rgba(37, 99, 235, 0.15);
color: #bfdbfe;
border: 1px solid rgba(37, 99, 235, 0.3);
}
.status.success {
background: rgba(16, 185, 129, 0.15);
color: #a7f3d0;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.status.error {
background: rgba(239, 68, 68, 0.15);
color: #fecaca;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.audio-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);
}
.audio-container.show {
display: block;
}
audio {
width: 100%;
margin-bottom: 15px;
}
.format-selector {
margin-bottom: 20px;
text-align: center;
}
.format-selector label {
font-weight: 500;
color: #cbd5e1;
margin-right: 10px;
}
.format-selector select {
padding: 8px 15px;
border: 2px solid rgba(94, 234, 212, 0.3);
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
background: rgba(15, 23, 42, 0.7);
color: #e2e8f0;
}
.format-selector select:focus {
outline: none;
border-color: #5eead4;
box-shadow: 0 0 0 3px rgba(94, 234, 212, 0.2);
}
.timer {
font-size: 24px;
font-weight: bold;
color: #5eead4;
text-align: center;
margin-bottom: 20px;
font-family: 'Courier New', monospace;
text-shadow: 0 0 5px rgba(94, 234, 212, 0.5);
}
.visualizer {
width: 100%;
height: 100px;
background: rgba(15, 23, 42, 0.7);
border-radius: 10px;
margin-bottom: 20px;
position: relative;
overflow: hidden;
border: 1px solid rgba(94, 234, 212, 0.1);
}
canvas {
width: 100%;
height: 100%;
}
.upload-progress {
margin-top: 20px;
padding: 15px;
background: rgba(30, 41, 59, 0.5);
border-radius: 10px;
display: none;
border: 1px solid rgba(94, 234, 212, 0.1);
}
.upload-progress.show {
display: block;
}
.progress-bar {
width: 100%;
height: 20px;
background: rgba(15, 23, 42, 0.7);
border-radius: 10px;
overflow: hidden;
position: relative;
border: 1px solid rgba(94, 234, 212, 0.1);
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #0ea5e9, #5eead4);
width: 0%;
transition: width 0.3s ease;
position: relative;
overflow: hidden;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.progress-text {
text-align: center;
margin-top: 10px;
font-weight: 500;
color: #cbd5e1;
}
.settings {
margin-bottom: 20px;
padding: 15px;
background: rgba(30, 41, 59, 0.5);
border-radius: 10px;
border: 1px solid rgba(94, 234, 212, 0.1);
}
.settings h3 {
margin-bottom: 10px;
color: #5eead4;
font-size: 1.1em;
}
.setting-item {
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.setting-item label {
color: #cbd5e1;
}
.setting-item input[type="range"] {
width: 150px;
}
.setting-item span {
color: #5eead4;
font-weight: 500;
min-width: 40px;
text-align: right;
}
@media (max-width: 480px) {
.container {
padding: 20px;
}
h1 {
font-size: 2em;
}
button {
padding: 10px 20px;
font-size: 14px;
}
.timer {
font-size: 20px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>语音评测系统</h1>
<div class="form-group">
<label for="language">选择评测语言:</label>
<select id="language">
<option value="chinese">中文</option>
<option value="english">英文</option>
<h1>🎙️ 在线录音机</h1>
<div class="format-selector">
<label for="audioFormat">选择音频格式:</label>
<select id="audioFormat">
<option value="webm">WebM (默认)</option>
<option value="mp4">MP4</option>
<option value="wav">WAV</option>
</select>
</div>
<div class="form-group">
<button id="recordBtn" class="btn btn-record">开始录音</button>
<button id="stopBtn" class="btn btn-stop" disabled>停止录音</button>
<div class="settings">
<h3>录音设置</h3>
<div class="setting-item">
<label>麦克风音量:</label>
<div style="display: flex; align-items: center; gap: 10px;">
<input type="range" id="volumeSlider" min="0" max="100" value="50">
<span id="volumeValue">50%</span>
</div>
</div>
</div>
<div id="status" class="status">准备就绪</div>
<div class="visualizer">
<canvas id="visualizerCanvas"></canvas>
</div>
<div id="result" class="result" style="display: none;">
<h3>评测结果</h3>
<div id="resultContent"></div>
<div class="timer" id="timer">00:00</div>
<div class="status" id="status">准备就绪</div>
<div class="controls">
<button id="recordBtn" class="btn-record">
<span>🔴</span> 开始录音
</button>
<button id="stopBtn" class="btn-stop" disabled>
<span>⏹️</span> 停止录音
</button>
<button id="playBtn" class="btn-play" disabled>
<span>▶️</span> 播放
</button>
<button id="uploadBtn" class="btn-upload" disabled>
<span>📤</span> 上传到服务器
</button>
</div>
<div class="audio-container" id="audioContainer">
<audio id="audioPlayer" controls></audio>
<p style="text-align: center; color: #666; margin-top: 10px;">
录制时间: <span id="duration">0</span> 秒 |
文件大小: <span id="fileSize">0</span> KB
</p>
</div>
<div class="upload-progress" id="uploadProgress">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text" id="progressText">上传进度: 0%</div>
</div>
</div>
<script src="js/audio_evaluation.js"></script>
<script>
class AudioRecorder {
constructor() {
this.mediaRecorder = null;
this.audioChunks = [];
this.recordedBlob = null;
this.isRecording = false;
this.startTime = null;
this.timerInterval = null;
this.audioContext = null;
this.analyser = null;
this.dataArray = null;
this.canvas = document.getElementById('visualizerCanvas');
this.canvasCtx = this.canvas.getContext('2d');
this.animationId = null;
this.initializeElements();
this.setupEventListeners();
this.setupCanvas();
}
initializeElements() {
this.recordBtn = document.getElementById('recordBtn');
this.stopBtn = document.getElementById('stopBtn');
this.playBtn = document.getElementById('playBtn');
this.uploadBtn = document.getElementById('uploadBtn');
this.status = document.getElementById('status');
this.audioPlayer = document.getElementById('audioPlayer');
this.audioContainer = document.getElementById('audioContainer');
this.timer = document.getElementById('timer');
this.duration = document.getElementById('duration');
this.fileSize = document.getElementById('fileSize');
this.audioFormat = document.getElementById('audioFormat');
this.volumeSlider = document.getElementById('volumeSlider');
this.volumeValue = document.getElementById('volumeValue');
this.uploadProgress = document.getElementById('uploadProgress');
this.progressFill = document.getElementById('progressFill');
this.progressText = document.getElementById('progressText');
}
setupCanvas() {
const resizeCanvas = () => {
this.canvas.width = this.canvas.offsetWidth;
this.canvas.height = this.canvas.offsetHeight;
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
}
setupEventListeners() {
this.recordBtn.addEventListener('click', () => this.startRecording());
this.stopBtn.addEventListener('click', () => this.stopRecording());
this.playBtn.addEventListener('click', () => this.playRecording());
this.uploadBtn.addEventListener('click', () => this.uploadRecording());
this.volumeSlider.addEventListener('input', (e) => {
this.volumeValue.textContent = e.target.value + '%';
if (this.audioContext && this.audioContext.destination) {
this.audioContext.destination.volume.value = e.target.value / 100;
}
});
}
async startRecording() {
try {
this.updateStatus('正在请求麦克风权限...', 'recording');
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100
}
});
// 设置音频上下文用于可视化
this.setupAudioContext(stream);
const mimeType = this.getMimeType();
this.mediaRecorder = new MediaRecorder(stream, {
mimeType: mimeType,
audioBitsPerSecond: 128000
});
this.audioChunks = [];
this.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
this.audioChunks.push(event.data);
}
};
this.mediaRecorder.onstop = () => {
this.recordedBlob = new Blob(this.audioChunks, { type: mimeType });
this.onRecordingComplete();
};
this.mediaRecorder.onstart = () => {
this.isRecording = true;
this.startTime = Date.now();
this.startTimer();
this.startVisualization();
this.updateButtons();
};
this.mediaRecorder.start(1000); // 每秒触发一次dataavailable事件
} catch (error) {
console.error('录音错误:', error);
this.updateStatus('无法访问麦克风,请检查权限设置', 'error');
if (error.name === 'NotAllowedError') {
alert('请允许访问麦克风权限才能录音');
} else if (error.name === 'NotFoundError') {
alert('未找到麦克风设备');
} else {
alert('录音初始化失败: ' + error.message);
}
}
}
setupAudioContext(stream) {
try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
const source = this.audioContext.createMediaStreamSource(stream);
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 256;
const bufferLength = this.analyser.frequencyBinCount;
this.dataArray = new Uint8Array(bufferLength);
source.connect(this.analyser);
} catch (error) {
console.warn('音频可视化初始化失败:', error);
}
}
startVisualization() {
if (!this.analyser) return;
const draw = () => {
if (!this.isRecording) return;
this.animationId = requestAnimationFrame(draw);
this.analyser.getByteFrequencyData(this.dataArray);
this.canvasCtx.fillStyle = 'rgba(15, 23, 42, 0.7)';
this.canvasCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const barWidth = (this.canvas.width / this.dataArray.length) * 2.5;
let barHeight;
let x = 0;
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');
this.canvasCtx.fillStyle = gradient;
this.canvasCtx.fillRect(x, this.canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
};
draw();
}
stopVisualization() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
// 清空画布
this.canvasCtx.fillStyle = 'rgba(15, 23, 42, 0.7)';
this.canvasCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
getMimeType() {
const format = this.audioFormat.value;
const supportedTypes = {
'webm': 'audio/webm;codecs=opus',
'mp4': 'audio/mp4',
'wav': 'audio/wav'
};
const mimeType = supportedTypes[format];
// 检查浏览器是否支持选定的格式
if (!MediaRecorder.isTypeSupported(mimeType)) {
console.warn(`${format} 格式不被支持,回退到 WebM`);
return 'audio/webm;codecs=opus';
}
return mimeType;
}
stopRecording() {
if (this.mediaRecorder && this.isRecording) {
this.mediaRecorder.stop();
this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
this.isRecording = false;
this.stopTimer();
this.stopVisualization();
this.updateStatus('录音已停止', 'stopped');
this.updateButtons();
}
}
onRecordingComplete() {
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);
this.duration.textContent = duration;
this.fileSize.textContent = sizeInKB;
this.audioContainer.classList.add('show');
this.updateStatus(`录音完成!时长: ${duration}秒, 大小: ${sizeInKB}KB`, 'success');
}
playRecording() {
if (this.audioPlayer.src) {
this.audioPlayer.play();
this.updateStatus('正在播放录音...', 'success');
}
}
async uploadRecording() {
if (!this.recordedBlob) return;
this.updateStatus('正在上传录音...', 'uploading');
this.uploadProgress.classList.add('show');
const formData = new FormData();
// 根据选择的格式确定文件扩展名
const format = this.audioFormat.value;
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('https://your-api-endpoint.com/upload', {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json',
}
});
if (!response.ok) {
throw new Error(`上传失败: ${response.status} ${response.statusText}`);
}
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}`);
} else {
alert('录音上传成功!');
}
} catch (error) {
console.error('上传错误:', error);
this.uploadProgress.classList.remove('show');
this.updateStatus('上传失败: ' + error.message, 'error');
// 更详细的错误处理
if (error.name === 'TypeError' && error.message.includes('fetch')) {
alert('网络连接失败,请检查网络设置');
} else if (error.message.includes('Failed to fetch')) {
alert('无法连接到服务器请检查API地址是否正确');
} else {
alert('上传失败: ' + error.message);
}
}
}
simulateUploadProgress() {
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 15;
if (progress >= 90) {
progress = 90; // 等待实际上传完成
clearInterval(interval);
return;
}
this.updateProgress(progress);
}, 200);
}
updateProgress(progress) {
progress = Math.min(100, Math.max(0, progress));
this.progressFill.style.width = progress + '%';
this.progressText.textContent = `上传进度: ${Math.round(progress)}%`;
}
startTimer() {
this.timerInterval = setInterval(() => {
if (this.startTime) {
const elapsed = Date.now() - this.startTime;
const minutes = Math.floor(elapsed / 60000);
const seconds = Math.floor((elapsed % 60000) / 1000);
this.timer.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
}, 100);
}
stopTimer() {
if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = null;
}
}
updateButtons() {
this.recordBtn.disabled = this.isRecording;
this.stopBtn.disabled = !this.isRecording;
this.playBtn.disabled = !this.recordedBlob || this.isRecording;
this.uploadBtn.disabled = !this.recordedBlob || this.isRecording;
}
updateStatus(message, type = '') {
this.status.textContent = message;
this.status.className = 'status ' + type;
}
// 清理资源
cleanup() {
this.stopTimer();
this.stopVisualization();
if (this.mediaRecorder) {
this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
if (this.audioContext) {
this.audioContext.close();
}
if (this.audioPlayer.src) {
URL.revokeObjectURL(this.audioPlayer.src);
}
}
}
// 初始化录音器
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('🔧 请确保已允许麦克风权限');
</script>
</body>
</html>
</html>

View File

@@ -1,205 +0,0 @@
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');
// 开始录音
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;
}
});
// 检测浏览器支持的录制格式
const getSupportedMimeType = () => {
const options = [
'audio/webm; codecs=opus',
'audio/webm',
'audio/mp4',
'' // 空字符串表示使用浏览器默认格式
];
for (const option of options) {
if (MediaRecorder.isTypeSupported(option)) return option;
}
return '';
};
// 使用检测到的格式初始化
mediaRecorder = new MediaRecorder(stream, {
mimeType: getSupportedMimeType(),
audioBitsPerSecond: 16000
});
// 同时修正Blob类型确保前后一致
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) {
showError("评测结果格式错误");
return;
}
if (results.fluency_score !== undefined) {
html += `<p><strong>流利度:</strong> ${results.fluency_score.toFixed(4)}</p>`;
} else {
html += `<p><strong>流利度:</strong> 未获取</p>`; // 添加默认值
}
if (results.completeness_score !== undefined) {
html += `<p><strong>完整度:</strong> ${results.completeness_score.toFixed(4)}</p>`;
} else {
html += `<p><strong>完整度:</strong> 未获取</p>`; // 添加默认值
}
html += '</div>';
// 显示单词级评分
if (results.words && results.words.length > 0) {
html += '<div class="word-scores"><h4>单词评分:</h4><ul>';
results.words.forEach(word => {
// 为单词评分添加空值检查
const score = word.score !== undefined ? word.score.toFixed(4) : '无';
html += `<li>${word.content}: ${score}</li>`;
});
html += '</ul></div>';
}
resultContent.innerHTML = html;
}