刚结束了腾讯云BI的体验活动,在文章提到了SaSS、PaSS的概念,腾讯云BI是一个SaSS,而今天要写的腾讯云语音识别就是一个PaSS,平台即服务,用户只需要调用接口就能实现语音识别的功能,而语音识别所需要的算法、计算资源都是PaSS来分配。
曾惊叹王者荣耀的语音转文字,轻声细语的说一句cpdd,它真的就能转换成字母‘CPDD’。
今天才发现,这居然是腾讯云语音识别提供的服务。所以,今天就带着一个极大的兴趣,来体验一下腾讯云语音识别服务。
登录腾讯云语音识别的首页,可以看到一些服务的简介和套餐优惠。
腾讯云语音识别(Automatic Speech Recognition,ASR)是将语音转成文字的 PaaS 产品,能够为企业提供极具性价比的语音识别服务。被微信、王者荣耀、腾讯视频等大量内部业务使用,外部亦服务于呼叫中心录音转写、会议实时转写、语音输入法、数字人、互动直播、课堂内容分析等多个业务场景,产品具备丰富的行业落地经验。
活动期间腾讯云语音提供了新用户专享资源包,其中包括一句话识别调用、实时语音识别、录音文件识别、语音流异步识别,可以尽情体验语音识别的各种功能。
我也是在新用户专享资源包的基础上完成此次的开发,如果想要更多的体验,可以选择其他的套餐。
腾讯云语音识别一共有五个服务:录音文件识别、实时语音识别、录音文件识别极速版、一句话识别和语音流异步识别。
关于体验语音转文字的场景,我构思了好久,最终还是觉得即时通讯是语音转文字绝佳的体验场景,加上之前想要开发一个ChatGPT的微信小程序, 所以决定将语音转文字服务集成到ChatGPT中去,无需通过输入文字就能和ChatGPT交流。
主要是用一句话识别接口来完成语音识别。在一句话识别API中,可以识别URL指向的语音文件和base64格式的语音数据。我们使用base64来进行语音数据交互,来实现语音识别。
分析asr提供的API调用demo,我们在Java中构造请求及其参数。
我们发现整个请求的参数分为两个部分:公共参数和接口调用参数。公共参数放在了request的header部分,我们通过阅读API文档,前面的X-TC开头的参数用作标识不同服务,而负责服务鉴权的Authorization需要调用签名方法v3来生成。
签名方法v3文档中,实现了多种语言的签名方法。新建TencentCloudAPITC3类,将Java版本的v3复制进去。
接着开始重构TencentCloudAPITC3,在签名方法v3中,需要SecretId和SecretKey,以及host、version等参数。原代码中是从环境变量中获取,这里使用Value注解绑定application.properties中的配置。
接着就是将源代码中的main方法,重构成generateAPITC3方法,并将一些变量放到形参中调用。
最后返回一个包含请求header所有参数的Map
但是上面这种鉴权的方式不仅麻烦,而且是很麻烦。用Java、python、rest client搞了一个下午、报了一下午的错误,我直接放弃,直接使用腾讯官方的SDK来调用,所以说撤回上面的签名方法v3的实现,直接使用SDK。
使用maven引入依赖:
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.830</version>
</dependency>
新建一个类,借助tencentcloudapi的SDK能力,实现asrSentenceRecognitionRequest() 方法来完成asr的请求,其中对于url还是Data识别,都做了一个二选一的判断处理。
public SentenceRecognitionResponse asrSentenceRecognitionRequest(String url, String data, String voiceFormat, String engSerViceType) throws TencentCloudSDKException {
Credential cred = new Credential(SECRET_ID, SECRET_KEY);
AsrClient asrClient = new AsrClient(cred, "ap-shanghai");
SentenceRecognitionRequest request = new SentenceRecognitionRequest();
if (url != null && url.length() !=0) {
request.setUrl(url);
request.setSourceType(0L);
} else if (data != null) {
request.setData(data);
request.setSourceType(1L);
}
request.setVoiceFormat(voiceFormat);
request.setEngSerViceType(engSerViceType);
SentenceRecognitionResponse sentenceRecognitionResponse = asrClient.SentenceRecognition(request);
return sentenceRecognitionResponse;
}
Credential管理密钥,AsrClient来封装asr所有的接口,一句话识别接口请求对应的是SentenceRecognitionRequest类,在此类中通过setter传入请求参数。
发起请求之后,会返回SentenceRecognitionResponse的对象,包含的一句话识别接口返回Json的字段,这样我们就不用再定义实体类去转换json了。
从注释以及API文档给出的样例中,Result识别结果字段是是我们需要的。同样,对比前面自己实现签名方法v3,你会发现什么timestamp、action都无需自己声明定义,整个请求对于用户来说是透明化的。
接着就是controller的实现,对于controller,首先定义入参类AsrWordRequest,一共四个字段,分别是:EngSerViceType、VoiceFormat、Data、url。
配图
EngSerViceType、VoiceFormat分别代表着翻译语言和语音文件格式,Data和url二者选一传入,Data是base64的语音文件,url表示服务器上的语音文件资源。
然后我们就开发controller,我们只需要使用请求参数调用asrSentenceRecognitionRequest方法,完成asr一句话语音接口的请求,然后获取返回值。
@RequestMapping(value = "/sentenceRecognition", method = RequestMethod.POST)
@CrossOrigin(origins = "*", maxAge = 3600)
public Map<String, Object> sentenceRecognition(@RequestBody AsrWordRequest params) {
Map<String, Object> result = new HashMap<>();
SentenceRecognitionResponse sentenceRecognitionResponse;
try {
sentenceRecognitionResponse = tencentAPIAsrCredential.asrSentenceRecognitionRequest(params.getUrl(), params.getData(), params.getVoiceFormat(), params.getEngSerViceType());
} catch (TencentCloudSDKException e) {
result.put("code", 1);
result.put("message", e.getMessage());
return result;
}
result.put("code", 0);
result.put("message", sentenceRecognitionResponse.getResult());
return result;
}
在定义controller类时,使用 @RestController 注解让接口返回json格式的数据。这里我一共定义了两个字段:code和message。conde为0,message为正常请求asr返回的翻译结果数据,code为1,message是asr返回的异常信息。
在测试时,我语音识别的Data表示的base64的语音文件,但是从网上下载的又有问题。但是我灵机一动,腾讯云产品除了有ASR语音识别,还有TTS语音合成。于是我就领取了一个免费的语音合成资源包。
然后在API Explorer中输入TEXT“你好,阿柒!”,调用基础语音合成接口,将文本转换成wav语音文件。
接口响应结果返回的Audio就是base64的语音文件,我使用Rest Client进行接口测试,直接将Audio内容直接复制到Data参数上。
POST http://localhost:8080/asr/sentenceRecognition
Accept: */*
Content-Type: application/json; charset=utf-8
{
"VoiceFormat": "wav",
"Data": "Audio base64语音数据",
"EngSerViceType": "16k_zh",
"url": ""
}
运行返回结果:
使用TTS生成语音文件,成功被ASR一句话识别接口识别。
在上面测试示例中,如果不传入EngSerViceType,接口返回的code为1,message就是ASR返回的异常信息。
在,腾讯元器接入小程序的文章中
在右下角预留了麦克风,我们先从麦克风的一些功能入手。我想做的效果就是:长按麦克风录音的时候,麦克风变化的效果,松开手就录音结束。
<view class="input">
<uni-icons type="chat" size="30"></uni-icons>
<view class="chat-input uni-column">
<input class="uni-input" v-model="inputMessage" maxlength="200" placeholder="你有什么想知道的?"
@confirm="sendMessages" />
</view>
<uni-icons class='mic' :class="isRecording ? 'recording-icon' : ''" type="mic" size="30"
@touchstart="startRecording" @touchend="stopRecording" @touchmove="handleTouchMove"></uni-icons>
</view>
上面的代码中,主要是对mic这个uni-icons做了一些变动。当isRecording为true时,使用三目运算绑定了一个recording-icon的class,在css中定义recording-icon实现麦克风的录音效果。
首先是定义了isRecording变量,用来标识是否录音,以此来实现录音时的一些效果。
const isRecording = ref(false);
然后就是当isRecording时,实现mic图标的动画效果。
.recording-icon {
animation: pulse 1s infinite;
z-index: 100;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
使用 @keyframes定义了一个pulse动画,实际上就是利用opacity属性改变mic图标的透明度。
麦克风图标绑定了touchstart触摸事件,当按住麦克风的时候开始录音。具体逻辑在startRecording实现。
在uni-app中,结束和开始录音由RecorderManager对象控制,所以需要通过uni.getRecorderManager()获取。
let recorderManager = uni.getRecorderManager();
const startRecording = () => {
isRecording.value = true;
recorderManager.start({
format: 'wav',
});
};
这里将isRecording修改为true,表示我要开始录音了,然后使用start的options,将录音文件的格式设置为wav。同是要说的是,后面的很多效果和事件,都会在startRecording中和开始录音一起触发。
麦克风图标绑定了touchend结束触摸事件,当松开麦克风的时候停止录音,具体逻辑在stopRecording实现。
const stopRecording = () => {
if (isRecording.value) {
recorderManager.stop();
}
isRecording.value = false;
};
调用stop即可结束录音,并将isRecording修改为false。最终麦克风的动态效果结合开始、结束录音的实现如下:
当我长按麦克风的时候,我希望弹出一个灰色的遮罩层,里面有一个录音频率的条形框,松开录音的时候遮罩层就会消失。
<view class="overlay" v-if="isRecording">
<view class="canvas">
<canvas canvas-id="waveform"></canvas>
</view>
</view>
overlay的view表示遮罩层,canvas用来渲染录音频率。这里要说的是,overlay是在footer中实现的。所以overlay要作为template的子节点,也就是要和输入框、mic图标所在的view同级,这样header、main、footer、aside以及overlay都是body的子节点。
这样在使用position: absolute时,才能相当于父元素body进行定位,这时将overlay的width和height设置为100%,遮罩层就会渲染整个页面。
.overlay {
width: 100%;
height: 100%;
margin-bottom: 0px;
background: rgba(0, 0, 0, 0.5);
position: absolute;
}
.canvas {
width: 100%;
height: 50%;
position: absolute;
top: 20vh;
}
canvas {
position: absolute;
top: 55%;
left: 30%;
width: 40vw;
height: 8vh;
background-image: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);
border-radius: 20rpx;
margin: auto;
z-index: 9999;
}
这里使用css定义了canvas的样式,最终效果图如下:
gif有色差,遮罩层在实际上是灰色透明的。
然后就是实现onStop录音结束回调,实现后面的功能和逻辑。整体代码如下:
const fs = uni.getFileSystemManager()
recorderManager.onStop((res) => {
console.log('recorder stop', res);
// 第一部分
isRecording.value = false;
audioFilePath = res.tempFilePath;
uni.showToast({
icon: 'none',
title: '录音结束'
});
// 第二部分
fs.readFile({
filePath: audioFilePath,
success(res) {
console.log(res.data)
const base64Data = uni.arrayBufferToBase64(res.data);
console.log(base64Data)
// 第三部分
uni.request({
url: 'http://192.168.50.14:8080/asr/sentenceRecognition',
method: 'POST',
header: {
'Accept': '*/*',
'Content-Type': 'application/json; charset=utf-8'
},
data: {
VoiceFormat: 'wav',
Data: base64Data,
EngSerViceType: '16k_zh',
url: ''
},
success(res) {
console.log('请求成功:', res.data);
// 第四部分
chat.constructMessage(res.data.message, 1)
requestYQ();
},
fail(err) {
console.error('请求失败:', err);
}
});
},
fail(err) {
console.error('读取WAV文件失败:', err);
}
});
})
整段代码我分为四个部分:
const requestYQ = function () {
uni.request({
url: "https://yuanqi.tencent.com/openapi/v1/agent/chat/completions",
method: 'POST',
header: {
'X-Source': 'openapi',
'Content-Type': 'application/json',
'Authorization': 'Bearer pJhqvfJ1GhjzdMW',
},
data: {
"assistant_id": "v0c4Fkv",
"user_id": "rodney",
"stream": false,
"messages": chat.state.messages
},
success: (res) => {
const message = res.data['choices'][0]['message']['content']
chat.constructMessage(message, 2)
console.log(res.data);
}
})
}
chat.constructMessage是使用pinia封装的状态共享变量,具体实现可以参考腾讯元器:为了荒天帝,自学从零开发了一个微信小程序...。
基本功能都已经实现,美中不足的是预想中,录音时根据音频频率波浪线效果没有做出来,后面需要研究一下canvas和uni-app的接口。
分享一个比较有意思的,就是在测试的过程中,ASR接口返回了一个错误信息,元器最后用荒天帝的口吻给了回复。
这里是直接将语音转换的文字,渲染到对话框中,实际上是可以直接将录音文件渲染到对话框中,实现这个功能的话我需要重构两个部分:
至于录音的播放,也是需要将语音框中的语音文件filepath重新设置,绑定到语音对话框中,点击播放事件可以绑定下面的代码。
const playVoice = function () {
console.log('播放录音');
if (audioFilePath.length > 0) {
innerAudioContext.src = audioFilePath;
innerAudioContext.play();
}
}
腾讯云语音识别ASR和语音合成TTS,在准确性和实时性上的确具有优势。在整个微信小程序的开发中,只对用户方使用了ASR语音识别,有机会的话还是会将元器的回答,接入到语音合成TTS,实现ASR和TTS的完美联动。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。