上一篇微信小程序实现和AI语音对话功能
1.目的:之前项目实现跟ai语音对话,因为API语音结果生成缓慢,返给前端大概在10s左右,所以领导要求使用websokect,实时接受后端反的片段音频,前端播放。这样生成的时间就会快很多。
2.代码片段
import {
timeExChange,
copyText,
openSetting,
startMonitoringNetwork,
} from '../../../utils/utils'
const websocketModule = require('../../../utils/websocket.js');
let Wxml2canvas = require('wxml2canvas/index.js');
const app = getApp();
//引入插件:微信同声传译
const plugin = requirePlugin('WechatSI');
//获取全局唯一的语音识别管理器recordRecoManager
const manager = plugin.getRecordRecognitionManager();
// const VUE_APP_API_URL_MSG = "wss://地址/";
let isOpen = false; // 使用布尔值来表示连接状态
let socketTask = null;
let reconnectAttempts = 0;
let maxReconnectAttempts = 10; // 最大重连次数
let reconnectTimer = null;
let reconnectInterval = 1000; //重置重连间隔
Page({
/**
* 页面的初始数据
*/
innerAudioContext: null,
data: {
requestTask:null,
audioCtx: null,
// 存放从后端接收到的语音地址数组
voiceUrls: [],
isplay: true,
onstops: true, //默认执行onStop
isFlag: false, //是否点击录音到获取结果之间状态
authsetting: false, //是否获取授权
url: "地址",
openid: null,
islongPress: false, //是否长按
loginShow: false, //登录弹窗默认关闭
annimationFlag: false, //logo动画默认关闭
touchstart: false, //默认没有按下
toView: null,
scrollTop: 0,
src: '', //语音地址
resultobj: {
result: "",
tempFilePath: ""
},
msgText: 1, //1默认初始化 2对话进行中 3结束对话 4对话出现问题
flag: 1,
haveflag: false, //防止重复点击
recordState: false, //麦克风默认关闭状态
msglist: [], //聊天记录
showModal: false, //历时消息记录
heartBeatIntervalId: null,
},
/**
* 生命周期函数--监听页面加载
*/
onLoad() {
// 关闭主页按钮
wx.hideHomeButton();
wx.setNavigationBarTitle({
title: "AI"
})
// 获取语音授权
this.getSeeting(1);
//识别语音
this.initRecord();
const authset = wx.getStorageSync('AUTHSETTING');
if (!authset) { //没有获取录音权限
// 重新获取录音权限
this.getSeeting(1);
}
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
console.log("进l1onReady")
//创建内部 audio 上下文 InnerAudioContext 对象。
// this.innerAudioContext = wx.createInnerAudioContext();
// this.innerAudioContext.src = '';
// this.innerAudioContext.onError(function (res) {
// console.log(res);
// wx.showToast({
// title: '语音播放失败',
// icon: 'none',
// })
// })
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
console.log("进onshow");
this.initData();
reconnectAttempts = 0;
this.networkStatus()
},
//检测网络情况
networkStatus() {
this.getNetStatus().then(res => {
console.log('有网', res);
// 判断用户是否登录
this.isLogin();
}).catch(res => {
console.log("没网", res)
wx.showToast({
title: '请检查您的网络连接',
icon: 'none',
duration: 3000,
})
wx.onNetworkStatusChange(function (res) {
if (res.isConnected) {
uni.showToast({
title: '网络已恢复',
icon: 'none',
duration: 1000,
})
} else {
this.networkStatus();
}
})
})
},
/**
* 获取当前网络状态
* @returns {Promise} - 返回一个 Promise 对象
* - 成功时:Promise.resolve(res),其中 res 包含网络状态信息
* - 失败时:Promise.reject(res),如果网络类型为 'none',表示没有网络连接
*/
getNetStatus() {
return new Promise((resolve, reject) => {
wx.getNetworkType({
success(res) {
const networkType = res.networkType;
console.log("网络状态信息", res);
if (networkType === 'none') {
reject(res); // 网络类型为 'none' 时,拒绝 Promise
} else {
resolve(res); // 网络状态正常时,解析 Promise
}
},
fail(error) {
reject(error); // 请求失败时,拒绝 Promise
}
});
});
},
// 数据初始化
initData() {
this.setData({
requestTask:null,
islongPress: false,
onstops: true,
isFlag: false,
haveflag: false,
msgText: 1, //初始化
touchstart: false, //按钮恢复初始状态
annimationFlag: false,
haveflag: false,
'resultobj.tempFilePath': "",
isplay: false,
recordState: false,
resultText: "",
src: "",
heartBeatIntervalId: null,
touchstart: false,
voiceUrls: [],
})
},
// 链接sokect
connectWebSocket() {
let that = this;
if (socketTask) {
// 如果已有 WebSocket 连接,则关闭它
socketTask.close();
}
socketTask = wx.connectSocket({
url: VUE_APP_API_URL_MSG + wx.getStorageSync('OPENID'),
header: {
'content-type': 'application/json'
},
});
socketTask.onOpen(function (res) {
console.log('WebSocket连接成功', socketTask);
wx.hideLoading();
isOpen = true; // 更新连接状态
// 连接成功后的处理 关闭重连
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
reconnectAttempts = 0; // 连接成功后重置重连尝试次数
reconnectInterval = 1000; // 连接成功后重置重连间隔
that.startHeartbeat(); // 启动心跳机制
});
socketTask.onMessage(function (res) {
console.log("获取消息成功", res);
if (res.data == "ping") {
return false
}
if (that.data.isplay) {
wx.hideLoading();
//设置语音
that.setData({
isFlag: false,
})
const result = res.data;
// console.log("处理过后的数据", result);
that.data.voiceUrls.push(result);
// 判断是不是第一次收到消息且目前没有播放===才调用播放音频方法
if (that.data.voiceUrls.length == 1 && !that.data.audioCtx) {
console.log("第一次调用")
that.setData({
msgText: 2, //正在对话`
annimationFlag: true,
haveflag: true
})
that.playNextVoice();
}
} else {
wx.hideLoading();
// wx.showToast({
// title: res.data,
// icon: 'none',
// duration: 2000
// })
that.setData({
msgText: 1,
voiceUrls: []
})
}
});
socketTask.onClose(function (res) {
console.log('WebSocket连接已关闭========', res);
that.socketClose();
// isOpen = false; // 更新连接状态
// socketTask = null; // 清除 WebSocket 实例
that.stopHeartbeat(); // 停止心跳机制
// that.reconnectWebSocket(); // 尝试重连
// wx.showToast({
// title: "通话连接失败,刷新页面进行重新连接",
// icon: 'none',
// duration: 2000
// })
});
socketTask.onError(function (res) {
console.log('WebSocket连接错误:', res);
isOpen = false; // 更新连接状态
that.stopHeartbeat(); // 停止心跳机制
that.reconnectWebSocket(); // 尝试重连
});
},
// 启动心跳机制
startHeartbeat() {
let that = this;
if (isOpen) {
that.data.heartBeatIntervalId = setInterval(() => {
if (isOpen) {
console.log('发送心跳消息');
let data = {
msg: "ping",
openId: wx.getStorageSync('OPENID'),
}
socketTask.send({
data: JSON.stringify(data),
success(res) {
console.log('心跳消息发送成功');
}
});
} else {
clearInterval(that.data.heartBeatIntervalId);
}
}, 60000); // 每 10 秒发送一次心跳消息
}
},
// 停止心跳机制
stopHeartbeat() {
if (this.data.heartBeatIntervalId) {
console.log("停止心跳机制");
clearInterval(this.data.heartBeatIntervalId);
this.data.heartBeatIntervalId = null; // 清除定时器 ID
this.setData({
msgText: 1, //初始化
annimationFlag: false,
haveflag: false,
touchstart: false
})
}
},
// 尝试重新连接 WebSocket
reconnectWebSocket() {
if (!reconnectTimer) { // 确保没有重复的重连定时器
reconnectTimer = setTimeout(() => {
console.log('尝试重新连接 WebSocket');
this.connectWebSocket(); // 重新初始化 WebSocket
reconnectTimer = null; // 清除重连定时器 ID
}, 6000); // 重连间隔时间(毫秒)
}
},
// 播放下一个音频
playNextVoice() {
if (this.data.voiceUrls.length > 0) {
const url = this.data.voiceUrls.shift();
this.playVoice(url);
} else {
this.setData({
audioCtx: null,
msgText: 1,
annimationFlag: false,
haveflag: false,
src: ""
});
}
},
// 播放音频
playVoice(url) {
if (!this.data.audioCtx) {
this.data.audioCtx = wx.createInnerAudioContext();
this.data.audioCtx.onError((res) => {
console.log("音频播放错误", res);
this.setData({
msgText: 1,
annimationFlag: false,
haveflag: false,
touchstart: false
});
});
this.data.audioCtx.onEnded(() => {
this.playNextVoice();
});
}
this.data.audioCtx.src = url;
this.data.audioCtx.play();
},
// 预加载音频
preloadVoices(voiceUrls) {
voiceUrls.forEach(url => {
const audio = wx.createInnerAudioContext();
audio.src = url;
audio.preload = 'auto'; // 设置为自动预加载
audio.onCanplay(() => {
console.log(`Audio ${url} 预加载`);
});
audio.onError((res) => {
console.error(`错误预加载 ${url}`, res);
});
});
},
// 初始化播放音频
playVoices(voiceUrls) {
this.preloadVoices(voiceUrls); // 预加载音频
this.setData({
voiceUrls
}); // 更新音频列表
this.playNextVoice(); // 开始播放音频
},
playVoice(url) {
if (!this.data.audioCtx) {
this.data.audioCtx = wx.createInnerAudioContext();
this.data.audioCtx.onError((res) => {
console.log("音频播放错误", res);
this.setData({
msgText: 1,
annimationFlag: false,
haveflag: false,
touchstart: false
});
});
this.data.audioCtx.onEnded(() => {
this.playNextVoice();
});
}
this.data.audioCtx.src = url;
this.data.audioCtx.play();
},
// 关闭当前正在播放的语音
stopVoice() {
if (this.data.audioCtx) {
this.data.audioCtx.stop(); // 停止播放
this.data.audioCtx.destroy(); // 销毁音频上下文,释放资源
this.setData({
voiceUrls: [],
audioCtx: null,
msgText: 1, //初始化
annimationFlag: false,
haveflag: false,
src: ""
});
}
},
// 发送数据
wsSend(question) {
// 参数
let obj = {
// question: this.data.resultobj.result,
// question: "请给我写300字的有关妈妈的作文",
question: question,
openId: wx.getStorageSync('OPENID'),
}
console.log("消息链接上了ma", socketTask.readyState)
socketTask.send({
data: JSON.stringify(obj),
success(res) {
// wx.hideLoading();
console.log('发送数据帧成功', res);
},
fail(res) {
wx.hideLoading();
console.log('发送数据帧失败', res);
}
})
},
// 重连websokect
attemptReconnect() {
if (reconnectAttempts >= maxReconnectAttempts) {
console.error('已达最大重连次数,停止重连');
return;
}
reconnectAttempts++;
reconnectInterval *= 2; // 指数退避,每次重连间隔加倍
setTimeout(() => {
console.log(`尝试重新连接,间隔 ${reconnectInterval} 毫秒`);
this.connectWebSocket();
}, reconnectInterval);
},
// 关闭sokect type==1直接关闭,type==2,关闭后重连
socketClose(type) {
console.log("关闭sokect", socketTask)
if (socketTask) {
socketTask.close(); // 关闭 WebSocket 连接
socketTask = null; // 清除 WebSocket 实例
isOpen = false; // 更新连接状态
reconnectTimer = null;
reconnectAttempts = 0;
reconnectInterval = 1000;
this.setData({
heartBeatIntervalId: null
})
let thas = this;
if (type == 2) {
// setTimeout(res=>{
console.log("重连");
thas.reconnectWebSocket()
// },500)
}
}
},
// 判断用户是否登录
isLogin() {
// 获取本地存储中的 OPENID
const openid = wx.getStorageSync('OPENID');
console.log("openid是", openid);
if (openid) {
// 如果本地存储中存在 OPENID,则说明用户已经登录过,可以直接使用 OPENID 进行后续操作
this.setData({
loginShow: false
})
console.log("连接 WebSocket")
// 连接 WebSocket
wx.showLoading({
title: '对话连接中...',
icon: 'none',
mask: true
})
// this.isSokectApi();
this.connectWebSocket();
// 这里可以进行其他操作,比如直接跳转到主页面
} else {
console.log("如果本地存储中没有 OPENID,则需要调用登录接口获取 OPENID");
// 如果本地存储中没有 OPENID,则需要调用登录接口获取 OPENID
this.setData({
loginShow: true,
openid: openid
})
}
},
// 判断是否连接sokect
isSokectApi() {
wx.request({
url: this.data.url + '/cyjgVoice/sessionTest',
method: 'POST',
data: {
openId: wx.getStorageSync('OPENID')
},
success: res1 => {
console.log("是否链接呢111111", res1)
if (res1.data.code == 200) { //有连接
} else {
this.connectWebSocket();
// wx.showToast({
// title: res1.data.msg,
// icon: 'none',
// duration: 2000
// })
}
}
})
},
onChildEvent(event) {
console.log("进来嘛", wx.getStorageSync('OPENID'))
this.setData({
loginShow: event.detail.login_show
})
// 连接 WebSocket
this.connectWebSocket();
},
closeSend(msg) {
let data = {
msg: msg,
openId: wx.getStorageSync('OPENID'),
}
socketTask.send({
data: JSON.stringify(data),
success(res) {
console.log('消息发送了,关闭');
}
});
},
//暂停语音
backIndex() {
let that = this;
// 终止请求
that.closeSend("关闭语音");
that.cancelRequest();
that.stopVoice();
reconnectAttempts = 0;
reconnectTimer = null;
that.setData({
voiceUrls: [],
audioCtx: null,
msgText: 1, //初始化
touchstart: false, //按钮恢复初始状态
annimationFlag: false,
haveflag: false,
'resultobj.tempFilePath': "",
isplay: false,
resultText: "",
src: ""
})
// 如何判断当前是语音录制识别状态
console.log("暂停语音", that.data.isFlag)
if (that.data.isFlag) {
that.setData({
onstops: false, //是否执行onStop
})
wx.showLoading({
title: '关闭中...',
icon: 'none',
mask: true
})
// 停止识别
manager.stop();
}
wx.vibrateShort({
type: 'heavy',
fail: function (err) {
console.error('短振动失败', err)
}
})
},
// 播放语音
yuyinPlay: function (e) {
console.log("播放1", e);
if (this.data.src == '' && this.data.isplay) {
console.log("播放2", this.data.src, '222', this.data.isplay);
return;
}
this.setData({
msgText: 2, //正在对话`
annimationFlag: true,
haveflag: true
})
this.innerAudioContext.src = this.data.src; // 设置音频地址
let _this = this;
this.innerAudioContext.onError(function (res) {
console.log("报错吗", res)
_this.setData({
msgText: 1, //初始化
annimationFlag: false,
haveflag: false,
touchstart: false
})
// wx.showToast({
// title: '语音播放失败',
// icon: 'none',
// })
})
this.innerAudioContext.onTimeUpdate(() => {
// console.log('音频播放进度更新', this.data.src);
// console.log('音频进度', this.innerAudioContext.src);
});
this.innerAudioContext.onEnded(() => { // 添加播放结束的回调
console.log("播放结束")
this.setData({
msgText: 1, //初始化
annimationFlag: false,
haveflag: false,
src: ""
})
// 在这里执行播放完毕后的操作,比如关闭语音
this.innerAudioContext.stop(); // 使用 stop 方法停止音频并重置播放状态
});
this.innerAudioContext.play(); // 播放音频
},
//识别语音 -- 初始化
initRecord() {
const that = this;
// 有新的识别内容返回,则会调用此事件
manager.onRecognize = function (res) {
console.log("有新的识别内容返回,则会调用此事件")
}
// 正常开始录音识别时会调用此事件
manager.onStart = function (res) {
console.log("成功开始录音识别", res)
that.setData({
// annimationFlag:true
})
}
//识别结束事件
manager.onStop = function (res) {
if (!that.data.isplay) {
wx.showToast({
title: "听不清楚,请再说一遍!",
icon: 'success',
image: '/assets/image/no_voice.png',
duration: 1000,
success: function (res) {
that.setData({
haveflag: false,
isFlag: false
})
},
fail: function (res) {
console.log(res);
}
});
return false
}
if (res.result == '') {
wx.hideLoading();
// wx.showToast({
// title: '听不清楚,请重新说一遍!',
// icon: 'none',
// duration: 2000
// })
// that.setData({
// msgText: 1, //初始化
// haveflag: false,
// isFlag: false,
// })
that.showRecordEmptyTip()
return;
} else {
// wx.showLoading({
// title: '正在思考...',
// icon: 'none',
// })
console.log("获取翻译", res.result)
that.setData({
resultobj: {
result: res.result,
tempFilePath: res.tempFilePath,
},
msgText: 2, //正在对话
// annimationFlag: true
})
// 调用接口
that.resultTextApi();
// 给websoket发送数据
// that.wsSend();
}
}
// 识别错误事件
manager.onError = function (res) {
console.log("error msg", res);
wx.hideLoading();
wx.showToast({
icon: "none",
title: '请重新开始~'
})
that.setData({
haveflag: false,
msgText: 1,
annimationFlag: false,
isFlag: false, //当前录制语音识别状态
touchstart: false
})
}
},
// 根据wx.getSetting判断用户是否打开了录音权限,如果没有打开,则通过wx.authorize,向用户打开授权请求,如果用户拒绝了,就给用户打开授权设置页面。
getSeeting(type) {
// wx.showLoading({
// title: '获取录音权限',
// icon: 'none',
// mask: true
// })
const _this = this
wx.getSetting({ //获取用户当前设置
success: res => {
// wx.hideLoading();
// console.log('获取权限', res);
if (res.authSetting['scope.record']) { //查看是否授权了录音设置
// console.log('获取权限1111');
const authset = wx.setStorageSync('AUTHSETTING', true);
_this.setData({
authsetting: true
})
if (type == 2) {
wx.showToast({
title: '获取录音权限成功,点击重新开始!',
icon: 'none',
duration: 2000
})
}
} else {
// 用户还没有授权,向 用户发起授权请求
wx.authorize({ //提前向用户发起授权请求,调用后会立刻弹窗询问用户是否同意授权小程序使用某项功能或获取用户的某些数据,但不会实际调用对应接口
scope: 'scope.record',
success() { //用户同意授权摄像头
// console.log("同意授权");
// wx.showToast({
// title: '获取录音权限成功',
// icon: 'none',
// duration: 2000
// })
},
fail() { //用户不同意授权摄像头
openSetting()
}
})
}
},
fail() {
// console.log('获取用户授权信息失败');
wx.showToast({
title: '获取权限失败',
icon: 'none',
duration: 2000
})
}
})
},
resultTextApi(){
console.log("进来1111111")
//调用接口
const formdata = {
question: this.data.resultobj.result,
openId: wx.getStorageSync('OPENID'),
};
this.data.requestTask = wx.request({
url: this.data.url + '/cyjgVoice/ans',
method: 'POST',
//请求头需要改为stream模式
header: {
'content-type': 'application/x-www-form-urlencoded' // 或者 'application/json' 如果你的后端能接受JSON格式
},
data: formdata, // 直接使用formdata对象
success: res => {
console.log("语音1111", res);
if (res.data.code == 500) {
wx.showToast({
title: "未识别成功,请再说一遍",
icon: 'none'
})
wx.hideLoading();
//数据初始化
this.initData();
}
}
})
},
// 如果需要在某些条件下中止请求
cancelRequest() {
// 调用请求任务对象的 abort 方法来中止请求
if (this.data.requestTask && typeof this.data.requestTask.abort === 'function') {
this.data.requestTask.abort();
console.log("请求已被取消");
}
},
//语音 --按住说话
touchStart(e) {
// 判断是否获取录音权限
// if (!(this.data.authsetting)) {
// // console.log("获取语音权限")
// this.getSeeting(2);
// return
// }
this.setData({
resultobj: {
result: "",
tempFilePath: "",
}
})
console.log('按住说话', isOpen);
if (!isOpen) { //代表websokect未连接上
wx.showToast({
title: '正在建立语音通信,稍后请重试',
icon: 'none',
duration: 3000
})
return false
}
this.closeSend("ping");
if (this.data.haveflag) { //true 请先结束语音
console.log("没关闭haveflag")
wx.showToast({
title: '请先关闭语音!',
icon: 'none',
duration: 2000
})
return false
}
// 当前正在识别语音,还没结束上一次识别,请先关闭再进行录音
if (this.data.isFlag) { //true 请先结束语音
console.log("没关闭isFlag")
wx.showToast({
title: '请先关闭语音!',
icon: 'none',
duration: 2000
})
return false
}
// this.innerAudioContext.src = "";
// this.innerAudioContext.stop(); //暂停音频
wx.vibrateShort({
type: 'heavy',
fail: function (err) {
console.error('短振动失败', err);
}
})
this.setData({
islongPress: true,
isplay: true
})
var flag = Number(e.currentTarget.dataset.flag)
this.setData({
voiceUrls: [], //清除语音列表
recordState: true, //录音状态
flag: flag,
touchstart: true, //按下
msgText: 2, //初始化状态
})
// 语音开始识别
manager.start({
lang: 'zh_CN', // 识别的语言
})
},
//语音 --松开结束
touchEnd(e) {
if (!(this.data.islongPress)) { //如果是长按执行下面内容
console.log('松开1')
return false
}
wx.showLoading({
title: '正在思考...',
icon: 'none',
})
if (this.data.haveflag) { //true 请先结束语音
console.log('松开2');
this.setData({
touchstart: false,
})
wx.hideLoading();
// wx.showToast({
// title: '请先关闭语音111!',
// icon: 'none',
// duration: 2000
// })
return false
}
console.log('松开结束3');
this.setData({
touchstart: false,
recordState: false,
islongPress: false, //长按初始状态
isFlag: true, //判断从松手到识别录音期间状态
haveflag: true
})
// 语音结束识别
manager.stop();
},
// base64转mp3音频
base64ChangeVideo(base64Data) {
const audioPath = wx.env.USER_DATA_PATH + '/ordernew.mp3'
const fs = wx.getFileSystemManager();
let that = this;
fs.writeFile({
filePath: audioPath,
data: base64Data,
encoding: 'base64',
success(res) {
that.setData({
src: audioPath
})
that.yuyinPlay();
},
})
},
// 打开弹窗
openContent() {
if ((this.data.msgText == 1 && !(this.data.annimationFlag)) || (this.data.msgText == 2 && this.data.annimationFlag)) { //true 请先结束语音
// wx.showToast({
// title: '请先关闭对话!',
// icon: 'none',
// duration: 2000
// })
this.getChartQuery();
} else {
return false
}
wx.vibrateShort({
type: 'heavy',
fail: function (err) {
// console.error('短振动失败', err)
}
})
},
// 关闭弹窗
closeModal() {
this.setData({
showModal: false
})
},
showRecordEmptyTip() {
console.log("请说话1")
this.setData({
msgText: 1, //初始化
haveflag: false,
isFlag: false,
})
wx.showToast({
title: "听不清楚,请再说一遍!",
icon: 'success',
image: '/assets/image/no_voice.png',
duration: 1000,
success: function (res) {},
fail: function (res) {
console.log(res);
}
});
},
// 页面销毁功能处理函数
outPageData() {
if (socketTask) {
// websokect通知后端关闭
this.closeSend("关闭语音");
reconnectAttempts = 0;
this.stopVoice();
this.socketClose();
reconnectTimer = null;
// 终止请求
this.cancelRequest();
}
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
this.outPageData()
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
this.outPageData()
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {}
})