Files
dsProject/dsLightRag/static/XunFei/audio_evaluation.html
2025-09-05 22:07:01 +08:00

823 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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;
}
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="format-selector">
<label for="audioFormat">选择音频格式:</label>
<select id="audioFormat">
<option value="wav">WAV</option>
</select>
</div>
<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 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>
<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>