224 lines
4.3 KiB
Vue
224 lines
4.3 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="speech-page">
|
|||
|
|
<view class="title">语音评测(新方案)</view>
|
|||
|
|
|
|||
|
|
<!-- 题目显示 -->
|
|||
|
|
<view class="question-box">
|
|||
|
|
<text class="question-label">请朗读:</text>
|
|||
|
|
<text class="question-text">{{ questionText }}</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 录音按钮 -->
|
|||
|
|
<view class="record-box">
|
|||
|
|
<button
|
|||
|
|
@touchstart="startRecord"
|
|||
|
|
@touchend="stopRecord"
|
|||
|
|
:disabled="isProcessing"
|
|||
|
|
class="record-btn"
|
|||
|
|
:class="{ recording: isRecording }"
|
|||
|
|
>
|
|||
|
|
{{ recordButtonText }}
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<text class="tip">{{ isRecording ? '松开结束录音' : '按住开始录音' }}</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 结果显示 -->
|
|||
|
|
<view v-if="result" class="result-box">
|
|||
|
|
<view class="result-item">
|
|||
|
|
<text class="label">识别文本:</text>
|
|||
|
|
<text class="value">{{ result.recognizedText || '-' }}</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="result-item">
|
|||
|
|
<text class="label">准确度:</text>
|
|||
|
|
<text class="value score">{{ result.score || 0 }}分</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="result-item">
|
|||
|
|
<text class="label">评价:</text>
|
|||
|
|
<text class="value">{{ result.comment || '-' }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import speechRecorder from '@/utils/speech-recorder.js'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
questionText: '你好,欢迎使用语音评测系统',
|
|||
|
|
isRecording: false,
|
|||
|
|
isProcessing: false,
|
|||
|
|
result: null
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
computed: {
|
|||
|
|
recordButtonText() {
|
|||
|
|
if (this.isProcessing) return '处理中...'
|
|||
|
|
if (this.isRecording) return '录音中...'
|
|||
|
|
return '按住录音'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onLoad() {
|
|||
|
|
// 初始化录音器
|
|||
|
|
speechRecorder.init()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
methods: {
|
|||
|
|
// 开始录音
|
|||
|
|
startRecord() {
|
|||
|
|
this.isRecording = true
|
|||
|
|
this.result = null
|
|||
|
|
|
|||
|
|
speechRecorder.start({
|
|||
|
|
duration: 60000, // 最长60秒
|
|||
|
|
sampleRate: 16000,
|
|||
|
|
format: 'mp3'
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 停止录音并评测
|
|||
|
|
async stopRecord() {
|
|||
|
|
if (!this.isRecording) return
|
|||
|
|
|
|||
|
|
this.isRecording = false
|
|||
|
|
this.isProcessing = true
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 停止录音
|
|||
|
|
const filePath = await speechRecorder.stop()
|
|||
|
|
|
|||
|
|
// 上传并识别
|
|||
|
|
const result = await speechRecorder.uploadAndRecognize(filePath, {
|
|||
|
|
referenceText: this.questionText,
|
|||
|
|
mode: 'evaluation' // 评测模式
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
this.result = result
|
|||
|
|
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '评测完成',
|
|||
|
|
icon: 'success'
|
|||
|
|
})
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('语音评测失败', error)
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '评测失败:' + error.message,
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
} finally {
|
|||
|
|
this.isProcessing = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.speech-page {
|
|||
|
|
padding: 40rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.title {
|
|||
|
|
font-size: 36rpx;
|
|||
|
|
font-weight: bold;
|
|||
|
|
text-align: center;
|
|||
|
|
margin-bottom: 40rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.question-box {
|
|||
|
|
background: #f5f5f5;
|
|||
|
|
padding: 30rpx;
|
|||
|
|
border-radius: 12rpx;
|
|||
|
|
margin-bottom: 40rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.question-label {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #666;
|
|||
|
|
display: block;
|
|||
|
|
margin-bottom: 10rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.question-text {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
color: #333;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.record-box {
|
|||
|
|
text-align: center;
|
|||
|
|
margin: 60rpx 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.record-btn {
|
|||
|
|
width: 300rpx;
|
|||
|
|
height: 300rpx;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|||
|
|
color: white;
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
border: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.record-btn.recording {
|
|||
|
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|||
|
|
animation: pulse 1s infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.record-btn:active {
|
|||
|
|
opacity: 0.8;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes pulse {
|
|||
|
|
0%, 100% { transform: scale(1); }
|
|||
|
|
50% { transform: scale(1.05); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tip {
|
|||
|
|
display: block;
|
|||
|
|
margin-top: 20rpx;
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.result-box {
|
|||
|
|
background: #fff;
|
|||
|
|
border: 1rpx solid #e0e0e0;
|
|||
|
|
border-radius: 12rpx;
|
|||
|
|
padding: 30rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.result-item {
|
|||
|
|
display: flex;
|
|||
|
|
margin-bottom: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.result-item:last-child {
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.label {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #666;
|
|||
|
|
width: 160rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.value {
|
|||
|
|
flex: 1;
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.value.score {
|
|||
|
|
color: #f5576c;
|
|||
|
|
font-size: 36rpx;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
</style>
|