引言

在与实现了语音合成、语义分析、机器翻译等算法的后端交互时,页面可以设计成更为人性化、亲切的方式。我们采用类似于聊天对话的实现,效果如下:

  • 智能客服(输入文本,返回引擎处理后的文本结果)

  • 语音合成(输入文本,返回文本以及合成的音频)



    如上图所示,返回文本后,再返回合成出的音频。

    音频按钮嵌在对话气泡中,可以点击播放。

  • 语音识别(在页面录制语音发送,页面实时展示识别出的文本结果)

实现功能及技术要点

1、基于WebSocket实现对话流

页面与后端的交互是实时互动的,所以采用WebSocket协议,而不是HTTP请求,这样后端推送回的消息可以实时显示在页面上。

WebSocket的返回是队列的、无序的,在后续处理中我们也需要注意这一点,在后文中会说到。

2、调用设备麦克风进行音频录制和转码加头,基于WebAudio、WaveSurferJS等实现音频处理和绘制

3、基于Vue的响应式页面实现

4、CSS3 + Canvas + JS 交互效果优化

  • 录制音频CSS动画效果
  • 聊天记录自动滚动

    下面给出部分实现代码。

集成WebSocket

我们的聊天组件是页面侧边打开的抽屉(el-drawer),Vue组件会在打开时创建,关闭时销毁。在组件中引入WebSocket,并管理它的开、关、消息接收和发送,使它的生命周期与组件一致(打开窗口时创建ws连接,关闭窗口时关闭连接,避免与后台连接过多。)

created(){
if (typeof WebSocket === 'undefined') {
alert('您的浏览器不支持socket')
} else {
// 实例化socket
this.socket = new WebSocket(this.socketServerPath)
// 监听socket连接
this.socket.onopen = this.open
// 监听socket错误信息
this.socket.onerror = this.error
// 监听socket消息
this.socket.onmessage = this.onMessage
this.socket.onclose = this.close
}
}
destroyed(){
this.socket.close()
}

如上,将WebSocket的事件绑定到JS方法中,可以在对应方法中实现对数据的接收和发送。

打开浏览器控制台,选中指定的标签,便于对WebSocket连接进行监控和查看。

音频录制采集

从浏览器端音频和视频采集基于网页即时通信(Web Real-Time

Communication,简称WebRTC) 的API。通过WebRTCgetUserMedia实现,获取一个MediaStream对象,将该对象关联到AudioContext即可获得音频。

可参考RecorderJS的实现: https://github.com/mattdiamond/Recorderjs/blob/master/examples/example_simple_exportwav.html



if (navigator.getUserMedia) {
navigator.getUserMedia(
{ audio: true }, // 只启用音频
function(stream) {
var context = new(window.webkitAudioContext || window.AudioContext)()
var audioInput = context.createMediaStreamSource(stream)
var recorder = new Recorder(audioInput) },
function(error) {
switch (error.code || error.name) {
case 'PERMISSION_DENIED':
case 'PermissionDeniedError':
throwError('用户拒绝提供信息。')
break
case 'NOT_SUPPORTED_ERROR':
case 'NotSupportedError':
throwError('浏览器不支持硬件设备。')
break
case 'MANDATORY_UNSATISFIED_ERROR':
case 'MandatoryUnsatisfiedError':
throwError('无法发现指定的硬件设备。')
break
default:
throwError('无法打开麦克风。异常信息:' + (error.code || error.name))
break
}
}
)
} else {
throwError('当前浏览器不支持录音功能。')
}

注意: 若navigator.getUserMedia获取到的是undefined,是Chrome浏览器的安全策略导致的,需要通过https请求或配置浏览器,配置地址: chrome://flags/#unsafely-treat-insecure-origin-as-secure

浏览器采集到的音频为PCM格式(PCM (脉冲编码调制 Pulse Code Modulation)),需要对音频加头才能在页面上进行播放。注意加头时采样率、采样频率、声道数量等必须与采样时相同,不然加完头后的音频无法解码。参考查看https://github.com/mattdiamond/Recorderjs/blob/master/src/recorder.js中exportWav方法。

业务中对接的语音识别引擎为实时转写引擎,即:不是录制完成后再发送,而是一边录制一边进行编码并发送。

使用onaudioprocess方法监听语音的输入:

参考这个实现,我们可以在每次监听到有数据写入时,从buffer中获取到录制到的数据,并进行编码、压缩,再通过WebSocket发送。

Vue组件设计和业务实现

分析页面业务逻辑,将代码拆分成两个组件:

ChatDialog.vue 聊天对话框页面,根据输入类型,分为文本输入、语音输入。

ChatRecord.vue聊天记录组件,根据发送方(自己或者系统)展示向左/向右的气泡,根据内容显示文本、音频等。ChatDialogChatRecord的父组件,遍历ChatDialog中的chatList对象(Array),将chatList中的项注入到ChatRecord中。

<div class="chat-list">
<div v-for="(item,index) in chatList" :key="index" class="msg-wrapper">
<chat-record ref="chatRecord" :data="item" @showJson="showJsonDialog"></chat-record>
</div>
<div id="msg_end" style="height:0px; overflow:hidden"></div>
</div>
</div>

对于聊天记录的气泡展示,与数据类型相关性很强,ChatRecord组件只关心对数据的处理和展示,我们可以完全不用关心消息的发送、接收、音频的录制、停止录制、接受音频等逻辑,只需要根据数据来展示不同的样式即可。

这样Vue的响应式就充分获得了用武之地:无需用代码对样式展示进行控制,只需要设计合理的数据格式和样式模板,然后注入不同的数据即可。

模板页面: 使用v-if控制,修改chatList里的对象内容即可改变页面展示。

根据业务需求,将ChatRecord可能接收到的数据分为以下几类:

发送方为自己:

计时器使用JS的setInterval方法,每100ms更新一次录制时长

 this.recordTimer = setInterval(() => {
this.audioDuration = this.audioDuration + 0.1
}, 100)

停止后清空计时器:

 clearInterval(this.recordTimer)
  • 语音输入完毕,根据录制的语音,绘制波纹

    效果:

使用wavesurfer插件:

 initWaveSurfer() {
this.$nextTick(() => {
this.wavesurfer = WaveSurfer.create({
container: this.$refs.waveform,
height: 20,
waveColor: '#3d6fff',
progressColor: 'blue',
backend: 'MediaElement',
mediaControls: false,
audioRate: '1',
fillParent: false,
maxCanvasWidth: 500,
barWidth: 1,
barGap: 2,
barHeight: 5,
barMinHeight: 3,
normalize: true,
cursorColor: '#409EFF'
})
this.convertAudioToUrl(this.waveAudio).then((res) => {
this.wavesurfer.load(res) setTimeout(() => {
this.audioDuration = this.getAudioDuration()
}, 100)
})
})
}, // 将音频转化成url地址
convertAudioToUrl(audio) {
let blobUrl = ''
if (this.data.sendBy === 'self') {
blobUrl = window.URL.createObjectURL(audio)
return new Promise((resolve) => {
resolve(blobUrl)
})
} else {
return this.base64ToBlob({
b64data: audio,
contentType: 'audio/wav'
})
}
}, base64ToBlob({ b64data = '', contentType = '', sliceSize = 512 } = {}) {
return new Promise((resolve, reject) => {
// 使用 atob() 方法将数据解码
let byteCharacters = atob(b64data)
let byteArrays = []
for (
let offset = 0;
offset < byteCharacters.length;
offset += sliceSize
) {
let slice = byteCharacters.slice(offset, offset + sliceSize)
let byteNumbers = []
for (let i = 0; i < slice.length; i++) {
byteNumbers.push(slice.charCodeAt(i))
}
// 8 位无符号整数值的类型化数组。内容将初始化为 0。
// 如果无法分配请求数目的字节,则将引发异常。
byteArrays.push(new Uint8Array(byteNumbers))
}
let result = new Blob(byteArrays, {
type: contentType
})
result = Object.assign(result, {
// 这里一定要处理一下 URL.createObjectURL
preview: URL.createObjectURL(result),
name: `XXX.wav`
})
resolve(window.URL.createObjectURL(result))
})
},

发送方为系统:

  • 仅返回文本:显示文本

  • 仅返回音频(参考发送方为自己的实现)

  • 返回文本,随即返回文本对应的合成音频,显示文本和播放按钮

页面嵌入audio标签,将hidden设置为true使其不显示:

<div class="audio-player">
<svg-icon v-if="!isPlaying" icon-class='play' @click="onClickAudioPlayer" />
<svg-icon v-else icon-class='pause' @click="onClickAudioPlayer" />
<audio :src="playAudioUrl" autostart="true" hidden="true" ref="audioPlayer" />
</div>

playAudioUrl的生成参考上面生成的wavesurfer的url。

使用isPlaying参数记录当前音频的播放状态,并使用setTimeout方法,当播放了音频时长后,将播放按钮自动置为play

  onClickAudioPlayer() {
if (this.isPlaying) {
this.$refs.audioPlayer.pause()
this.isPlaying = false
} else {
// 每次点击时,开始播放,并在播放完毕将isPlaying置为false
this.$refs.audioPlayer.currentTime = 0
this.$refs.audioPlayer.play()
this.isPlaying = true setTimeout(() => {
// 将正在播放重置为false
this.isPlaying = false
}, Math.ceil(this.$refs.audioPlayer.duration) * 1000)
}
},
  • 聊天记录自动定位到最后一条:

    使用scrollIntoView()方法
  • 记录每次会话对应的记录ID(recordId):

    定义单次会话的id,并在返回的消息中回传,从而建立多条websocket返回的关联关系。

以上就是全部实现。难点主要是请求麦克风权限和对音频进行编码,在加wav头时必须保证和采样时的采样率、频率一致

Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的更多相关文章

  1. vue+websocket+express+mongodb实战项目(实时聊天)

    继上一个项目用vuejs仿网易云音乐(实现听歌以及搜索功能)后,发现上一个项目单纯用vue的model管理十分混乱,然后我去看了看vuex,打算做一个项目练练手,又不想做一个重复的项目,这次我就放弃颜 ...

  2. spring boot+vue实现H5聊天室客服功能

    spring boot+vue实现H5聊天室客服功能 h5效果图 vue效果图 功能实现 spring boot + webSocket 实现 官方地址 https://docs.spring.io/ ...

  3. vue+websocket+express+mongodb实战项目(实时聊天)(二)

    原项目地址:[ vue+websocket+express+mongodb实战项目(实时聊天)(一)][http://blog.csdn.net/blueblueskyhua/article/deta ...

  4. SpringBoot+Vue+WebSocket 实现在线聊天

    一.前言 本文将基于 SpringBoot + Vue + WebSocket 实现一个简单的在线聊天功能 页面如下: 在线体验地址:http://www.zhengqingya.com:8101 二 ...

  5. 如何利用WebSocket实现网页版聊天室

    花了将近一周的时间终于完成了利用WebSocket完成网页版聊天室这个小demo,期间还走过了一段"看似弯曲"的道路,但是我想其实也不算是弯路吧,因为你走过的路必将留下你的足迹.这 ...

  6. 安卓Native和H5页面进行交互

    安卓Native和H5页面进行交互 1.H5页面调用安卓Native界面 1)通过给webView添加JsInterface,安卓提供接口,让H5来进行调用    a)安卓写一个类,里面的方法需要用通 ...

  7. vue打包app嵌入h5,区分app进入和android,ios显示不同的下载链接

    vue打包app嵌入h5,区分app进入和android,ios显示不同的下载链接 需求:自己app打开的登录页面不显示app下载链接.其他地方打开判断android手机的跳转到android下载页链 ...

  8. 【WebSocket No.2】WebSocket和Socket实现聊天群发

    介绍: 前面写过一篇简单的websocke实现服务端.这一篇就不在说什么基础的东西主要是来用实例说话,主要是讲一下实现单聊和群组聊天和所有群发的思路设计. 直接不懂的可以看一下上一篇简单版本再来看也行 ...

  9. Vue+WebSocket+ES6+Canvas 制作「你画我猜」小游戏

    Vue+WebSocket+ES6+Canvas 制作「你画我猜」小游戏 转载 来源:jrainlau 链接:https://segmentfault.com/a/1190000005804860 项 ...

随机推荐

  1. HBase表的数据导出和导入

    1. 表数据导出 hbase org.apache.hadoop.hbase.mapreduce.Export test file:///home/hadoop/test (导入到本地) hbase ...

  2. 阿里服务器docker部署

    首先本人是购买的阿里云服务器,虽然是1g的内存,不过部署一些项目还是没问题的,学生也有一个优惠服务器,好像是70多2g内存的,还是很舒服的,学生党可以试着部署一下,下面呢我就说一下我自己部署的步骤: ...

  3. redis连接池参数动态化

    有的时候要从后端获取数据,真实的key可能在参数之上做一些修改,查了下set-misc模块,set_unescape_uri命令支持变量替换 location ~ /get_redis$ {      ...

  4. springboot完整项目,基于人人开源框架

    这是前端和数据库 下载链接只有31天有效,需要的,请联系QQ2319899766 下载链接密码: 9ksz 这个是后端代码 链接只有31天有效时间,链接失效请联系QQ2319899766提供下载链接 ...

  5. 本机浏览器无法访问linux的tomcat

    原因:Centos7的防火墙拦截了tomcat访问的8080端口  Redhat 等等linux firewall-cmd --permanent --zone=public --add-port=8 ...

  6. 论文阅读 SNAPSHOT ENSEMBLES

    引入 1. 随机梯度下降的特点 随机梯度下降法(Stochastic Gradient Descent)作为深度学习中主流使用的最优化方法, 有以下的优点: 躲避和逃离假的鞍点和局部极小点的能力 这篇 ...

  7. Spring系列之aAOP AOP是什么?+xml方式实现aop+注解方式实现aop

    Spring系列之aop aop是什么?+xml方式实现aop+注解方式实现aop 什么是AOP? AOP为Aspect Oriented Programming 的缩写,意识为面向切面的编程,是通过 ...

  8. 优雅的在React组件中注册事件

    前言 在React的开发中,我们经常需要在 window 上注册一些事件, 比如按下 Esc 关闭弹窗, 按上下键选中列表内容等等.比较常见的操作是在组件 mount 的时候去 window 上监听一 ...

  9. Markdown 1.0.1

    简介 Markdown 是由 John Gruber 于2004年开发一种轻量级标记语言,它是一个面向web作者的 text-to-HTML 转换工具.Markdown编辑器允许您使用纯文本格式编写, ...

  10. CentOS 7安装Nginx 1.10.2

    安装epel-release源并进行安装 yum install epel-release yum update(时间会有点长) yum install nginx 相关操作: systemctl s ...