语音识别可以识别录制好的MP3,但是无法实时转换,基于app录音不支持分片问题,参考组件科大讯飞实时语音识别
1.在uniapp 在app上不支持录音分片为了解决这个问题,底层使用了renderjs 使用了web相关的技术。
测试demo
<template>
<view class="content">
<view placeholder="转文字" class="inputarea">
{{ msg }}
</view>
<view class="down-ui" v-if="downed" :style="{ backgroundColor: downtime == -1 ? '#e43d33' : '#1acf3b' }">
<!-- 效果显示 -->
<view v-if="downtime == -1">
建立连接中
</view>
<view v-else>
语音倒计时
<text style="color: red;">{{ downtime }} </text>
</view>
</view>
<button class="btn-bottom" :disabled="disabled" @touchstart.stop="start" @touchend.stop="end">按下说话</button>
<yue-asr-xf ref="yueAsrRefs" :options="optionsxf" @countDown="countDown" @result="resultMsg" @onStop="onStop"
@onOpen="onOpen" @change="change"></yue-asr-xf>
</view>
</template>
<script>
export default {
data() {
const second = 60;
return {
title: 'Hello',
msg: '转文字',
optionsxf: {
receordingDuration: second,
APPID: '221111', // 请替换为实际值
API_SECRET: 'NmzMjcyMjQyMWFmZmNmM2M0YTBj', // 请替换为实际值
API_KEY: '926adea190f4a146bbc3c' // 请替换为实际值
},
downtime: -1, //默认-1
downed: false,
disabled: false,
second,
};
},
onLoad() {
// #ifdef APP
//对于android应用,基于安全权限,请在调用时动态申请危险权限
plus.android.requestPermissions(["android.permission.RECORD_AUDIO"], (e) => {}, (e) => {})
// #endif
},
methods: {
resumeUi() {
this.downed = false;
this.downtime = -1;
this.disabled = false;
this.downtime = this.second;
},
start() {
if (this.disabled) {
return;
}
console.log("开始")
this.downed = true;
this.$refs.yueAsrRefs.start();
this.disabled = true;
// 建立连接
},
end() {
console.log("结束")
this.$refs.yueAsrRefs.end();
},
countDown(e) {
console.log('countDown', e);
this.downtime = e;
},
onStop(e) {
console.log('onStop', e);
this.resumeUi();
},
onOpen(e) {
console.log('onOpen', e);
},
change(e) {
console.log('change', e);
},
resultMsg(e) {
this.msg = e
console.log('resultMsg', e);
}
}
};
</script>
<style>
.btn-bottom {
width: 100vw;
position: absolute;
bottom: 0px;
}
.inputarea {
text-align: left;
color: red;
height: 200rpx;
border: 1px solid #ccc;
margin: 10rpx;
border-radius: 10rpx;
padding: 5rpx;
overflow-y: scroll;
}
.down-ui {
height: 100px;
width: 100%;
position: absolute;
bottom: 50px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}
</style>
2.manifest.json
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />"
3.static下增加文件,将包里的dist复制过来
image.png
4.uni_modules
image.png
5.下载地址
https://p.dcloud.net.cn/plugin?id=21811&token=18363f5143222daa710fb756c3fe8cca
6.优化后的代码
e1a0b2fd46170bd72571d5de5f48f0b3.jpg
<template>
<view class="container">
<!-- 聊天消息区域 -->
<scroll-view ref="chatScroll" class="chat-area" scroll-y="true" scroll-with-animation>
<view v-for="(item, index) in messages" :key="index" class="message-item" :id="item.id"
:class="item.type === 'user' ? 'user-message' : 'ai-message'">
<image v-if="item.type === 'ai'" class="avatar" :src="aiAvatar"></image>
<view class="message-content">
{{ item.content }}
</view>
<image v-if="item.type === 'user'" class="avatar" src="/src/static/images/ai/user.png"></image>
</view>
</scroll-view>
<!-- 文本输入区域 -->
<view class="text-input-container" v-if="inputMode === 'text'">
<image class="mode-switch-btn" :src="'/static/images/ai/voice.png'" @click="inputMode = 'voice'"
mode="aspectFit" />
<input v-model="inputText" placeholder="输入消息..." @confirm="sendTextMessage" />
<button @click="sendTextMessage">发送</button>
</view>
<!-- 语音输入区域 -->
<view class="voice-input-container" v-if="inputMode === 'voice'">
<view class="voice-preview" v-if="isRecording" :class="{ canceling: isCanceling }">
<view class="voice-controls">
<text :style="{ color: isCanceling ? '#e43d33' : '#666' }">
{{ isCanceling ? '取消发送' : '松开发送' }}
</text>
<view class="slide-hint" v-show="!isCanceling">
<text>↑ 上滑取消</text>
</view>
<view class="cancel-hint" v-show="isCanceling">
<text style="color: #e43d33; font-weight: bold;">松开手指取消发送</text>
</view>
</view>
<view class="voice-text-preview">
{{ inputText || '正在聆听...' }}
</view>
</view>
<view style="display: flex;">
<image class="mode-switch-btn" :src="'/static/images/ai/keyboard.png'" @click="inputMode = 'text'"
mode="aspectFit" />
<button class="voice-btn" @touchstart="startVoiceInput" @touchend="endVoiceInput"
@touchmove="handleTouchMove">
{{ isRecording ? '松开结束' : '按住说话' }}
</button>
</view>
</view>
<yue-asr-xf ref="yueAsrRefs" :options="optionsxf" @countDown="countDown" @result="resultMsg" @onStop="onStop"
@onOpen="onOpen" @change="change"></yue-asr-xf>
</view>
</template>
<script>
export default {
data() {
const second = 60;
return {
title: 'Hello',
msg: '转文字',
messages: [], // 聊天消息列表
lastMsgId: '', // 最后一条消息ID
inputText: '', // 输入框文本
aiAvatar: '/static/tabbar/intelligence.gif', // AI头像
userAvatar: '/src/static/images/ai/user.png', // 用户头像
inputMode: 'voice', // 输入模式:voice/text
isRecording: false, // 是否正在录音
isCanceling: false, // 是否正在取消
optionsxf: {
receordingDuration: second,
APPID: 'dd27c773', // 请替换为实际值
API_SECRET: 'NmZhMjczMjcyMjQyMWFmZmNmM2M0YTBj', // 请替换为实际值
API_KEY: '926ab0bbb1fb226dea190f4a146bbc3c' // 请替换为实际值
},
downtime: -1, // 默认-1
downed: false,
disabled: false,
second,
};
},
methods: {
resumeUi() {
this.downed = false;
this.downtime = -1;
this.disabled = false;
this.downtime = this.second;
},
start() {
if (this.disabled) {
return;
}
console.log("开始")
this.downed = true;
this.$refs.yueAsrRefs.start();
this.disabled = true;
},
end() {
console.log("结束")
this.$refs.yueAsrRefs.end();
},
countDown(e) {
console.log('countDown', e);
this.downtime = e;
},
onOpen(e) {
console.log('onOpen', e);
},
change(e) {
console.log('change', e);
},
resultMsg(e) {
this.inputText = e;
console.log('resultMsg', e);
},
onStop(e) {
console.log('onStop', e);
this.resumeUi();
if (this.inputText.trim()) {
this.sendTextMessage();
}
},
sendTextMessage() {
if (!this.inputText.trim()) return;
const userMsg = {
type: 'user',
content: this.inputText,
id: 'msg_' + Date.now()
};
this.messages.push(userMsg);
this.lastMsgId = userMsg.id;
this.$nextTick(() => {
this.scrollToBottom();
});
this.inputText = '';
setTimeout(() => {
const aiMsg = {
type: 'ai',
content: '这是AI的模拟回复',
id: 'msg_' + Date.now()
};
this.messages.push(aiMsg);
this.lastMsgId = aiMsg.id;
this.$nextTick(() => {
this.scrollToBottom();
});
}, 1000);
},
scrollToBottom() {
this.$nextTick(() => {
if (this.$refs.chatScroll) {
this.$refs.chatScroll.scrollTo({
top: 999999, // 足够大的值确保滚动到底部
duration: 300
});
console.log('执行滚动')
}
});
},
endVoiceInput() {
// 确保停止录音
this.$refs.yueAsrRefs.end();
if (this.isCanceling) {
this.inputText = '';
} else if (this.inputText.trim()) {
this.sendTextMessage();
}
// 重置所有状态
this.isRecording = false;
this.isCanceling = false;
this.startY = null;
},
handleTouchMove(e) {
if (!this.isRecording) return;
const touch = e.touches[0];
if (!this.startY) {
this.startY = touch.pageY;
return;
}
const currentY = touch.pageY;
const moveDistance = this.startY - currentY;
// console.log('触摸移动距离:', moveDistance, 'startY:', this.startY, 'currentY:', currentY);
if (moveDistance > 50) {
if (!this.isCanceling) {
this.$set(this, 'isCanceling', true);
// console.log('触发取消状态');
}
} else if (this.isCanceling) {
this.$set(this, 'isCanceling', false);
// console.log('取消状态已重置');
}
},
startVoiceInput() {
this.startY = null; // 重置起始坐标
this.isRecording = true;
this.isCanceling = false;
this.$refs.yueAsrRefs.start();
},
focusInput() {
// #ifdef H5
this.$refs.input.focus();
// #endif
}
}
};
</script>
<style lang="scss">
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.chat-area {
flex: 1;
padding: 20rpx;
overflow-y: auto;
}
.message-item {
display: flex;
margin-bottom: 30rpx;
align-items: flex-start;
margin-right: 40rpx;
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
.message-content {
max-width: 70%;
padding: 20rpx;
border-radius: 10rpx;
font-size: 28rpx;
line-height: 1.5;
}
}
.user-message {
justify-content: flex-end;
.message-content {
background-color: #1989fa;
color: white;
margin-left: 20rpx;
}
}
.ai-message {
justify-content: flex-start;
.message-content {
background-color: white;
color: #333;
margin-right: 20rpx;
}
}
.text-input-container {
display: flex;
padding: 20rpx;
background-color: white;
border-top: 1rpx solid #eee;
input {
flex: 1;
padding: 20rpx;
border: 1rpx solid #ddd;
border-radius: 50rpx;
font-size: 28rpx;
margin-right: 20rpx;
}
button {
padding: 0 40rpx;
background-color: #1989fa;
color: white;
border: none;
border-radius: 50rpx;
}
}
.voice-input-container {
background-color: white;
padding: 20rpx;
border-top: 1rpx solid #eee;
.voice-preview {
background-color: #f9f9f9;
border-radius: 10rpx;
padding: 20rpx;
margin-bottom: 20rpx;
transition: all 0.3s;
&.canceling {
background-color: #ffeeee;
.voice-controls {
color: #e43d33;
}
}
.voice-controls {
text-align: center;
font-size: 28rpx;
margin-bottom: 20rpx;
.slide-hint {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
.cancel-hint {
font-size: 24rpx;
margin-top: 10rpx;
}
}
.voice-text-preview {
min-height: 80rpx;
padding: 20rpx;
background-color: white;
border-radius: 10rpx;
color: #333;
}
}
.voice-btn {
width: 100%;
background-color: #1989fa;
color: white;
border: none;
border-radius: 10rpx;
font-size: 32rpx;
text-align: center;
}
}
.mode-switch-btn {
width: 70rpx;
height: 70rpx;
margin: 10rpx;
}
</style>