881 lines
29 KiB
HTML
881 lines
29 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.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="text-container">
|
|
<h3 style="color: #5eead4; margin-bottom: 10px;">📝 请朗读以下文本</h3>
|
|
<textarea id="readingText" rows="4" style="width: 100%; padding: 12px; border-radius: 8px; background: rgba(15, 23, 42, 0.7); border: 1px solid rgba(94, 234, 212, 0.2); color: #e2e8f0; font-size: 16px; resize: vertical; font-family: inherit;">Hello everyone! Nice to meet you. Today is a beautiful day. I am learning English pronunciation with this tool.</textarea>
|
|
</div>
|
|
<div class="visualizer">
|
|
<canvas id="visualizerCanvas"></canvas>
|
|
</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 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>
|
|
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 = "WebM";
|
|
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');
|
|
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() {
|
|
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());
|
|
}
|
|
|
|
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);
|
|
|
|
} catch (error) {
|
|
console.error('录音错误:', error);
|
|
this.updateStatus('无法访问麦克风,请检查权限设置', 'error');
|
|
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 = "webm";
|
|
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');
|
|
this.updateButtons();
|
|
}
|
|
|
|
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 = "webm";
|
|
const extensions = {'webm': 'webm', 'mp4': 'mp4', 'wav': 'wav'};
|
|
const fileName = `recording_${Date.now()}.${extensions[format]}`;
|
|
|
|
formData.append('audio', this.recordedBlob, fileName);
|
|
// 添加朗读文本到表单数据
|
|
const readingText = document.getElementById('readingText').value;
|
|
formData.append('txt', readingText);
|
|
try {
|
|
this.simulateUploadProgress();
|
|
|
|
const response = await fetch('/api/xunFeiEn/save-audio', {
|
|
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');
|
|
|
|
if (result.success) {
|
|
this.updateStatus('评测完成!', 'success');
|
|
this.displayEvaluationResults(result.evaluation);
|
|
console.log('评测结果:', result.evaluation);
|
|
} else {
|
|
throw new Error(result.error || '未知错误');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('上传错误:', error);
|
|
this.uploadProgress.classList.remove('show');
|
|
this.updateStatus('上传失败: ' + error.message, 'error');
|
|
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>';
|
|
}
|
|
}
|
|
|
|
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());
|
|
</script>
|
|
</body>
|
|
</html> |