上一篇手机录入语音翻译为文字里面讲到了如何从手机端录入语音,发往后端。其实这只是完成了工作的一半,既然我有了语音输入,你是不是应该给我返回语音消息,这样咱们才能愉快的玩耍呀。要不然,你就是个寂寞boy,对说的就是你。
好的,假设后端已经获取到了语音,翻译成了中文文字。我们就要对这个中文文字进行处理。在处理之前,有个问题,就是中文是有多音字的,其次是语音翻译接口未必那么准。如果翻译错了,那么后续的处理,势必很困难。因此呢,加入你用的是百度语音翻译接口,你可以先上传词库,也就是你的所有可能的话的单词、句子上传上去,这样百度翻译的时候就能准确一点。
即使这样做了,依旧会存在翻译错误和不准确的情况。这就需要用到相似度,进行匹配了。也就是那这个翻译的句子,跟这个所有可能的句子列表进行相似度匹配,找到一个最相似的,并且这个相似度超过某个阈值,我们就说,我们就认为这句话本来是这个意思,就可以按照后续逻辑进行处理了。否则,我们认为无法识别这句话的意图,进行报错提示。比如“宝,你说的太难了,我正在学习,请给我一点时间好吗,我一定...”。
下面具体解释一下几种不同的相似度匹配思路:
部分词的相似度匹配:
比如对于报表中心来说,用户输入“查看XX项目利润报表”
那么就要去匹配到底是哪张报表,假设关键字“查看”翻译无误,已经顺利提取出来了,
同时“XX项目”也通过某种方式识别和提取出来了,
那就只剩下“利润报表”这个关键词了,如果根据精确匹配无法找到,
那么就需要根据这个词进行相似性匹配了。
这里只是用到了部分的关键词“XX项目”进行相似度匹配,
因此叫做部分词的相似度匹配。
整句话的相似度匹配:
有时候上面的依旧是无法识别到的,比如“查看”这个开头的关键字,
由于用户发音不准或者翻译有误,将其翻译为“查到” “看到”,
但是后面确又翻译的准确无误。其实很容易就能看出来用户是想要干什么,
但是代码只能无情的将其判定为“未知的语音指令”,这很让人苦恼和心痛。
因此这时候就需要将整句话进行拼音相似度匹配,以期从中可以做到最大范围的匹配。
但是数据库中并没有整句话的词库,有的只是报表的名称?那该怎么办呢?
因此可以在项目启动的时候,一次性查出所有报表名称,
然后用代码穷举各种前缀性的词,后缀性的词,进行排列组合。
然后拿着翻译的话跟所有饿排列组合句子去进行相似度匹配。
假如你选定了某种相似度匹配的思路,那么问题来了,怎样相似度匹配呢?一种做法是利用已经有的工具库,进行分词处理,或者某种算法,计算余弦夹角或者什么距离,这一块我不太懂,需要去研究不同的工具库以及训练词库。
另一种做法就是将文本转为拼音,然后比较拼音的相似度。因为一般的业务性话题和选词范围大致固定,因此我选用拼音相似度来做匹配。
嗯,首先将中文文本转为拼音吧,先引入pom:
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
</dependency>
工具类PinYinUtil:
public static String chineseTextToPinYin(String chineseText,String separator,Boolean unConvertIsKeep) {
try {
if(unConvertIsKeep == null) {
unConvertIsKeep = true;//遇到不能识别的 默认保留
}
if(StringUtil.isEmpty(separator)){
separator = "";
}
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
//拼音小写
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
//不带声调
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
//要转换的中文,格式,转换之后的拼音的分隔符,遇到不能转换的是否保留 wo,shi,zhong,guo,ren,,hello
return PinyinHelper.toHanYuPinyinString(chineseText, format, separator, unConvertIsKeep);
}catch (Exception e){
throw new RuntimeException(e);
}
}
相似度工具类TextSimilarityUtil:
/**
* 计算两个中文文本的相似度 相似度越高 值越大 最大为1
* @param text1
* @param text2
* @return
*/
public static double getSemblanceByPinyin(String text1, String text2){
String pinyinTexts1 = PinYinUtil.chineseTextToPinYin(text1,",",null);
String pinyinTexts2 = PinYinUtil.chineseTextToPinYin(text2,",",null);
int d[][]; // 矩阵
int n = pinyinTexts1.length();
int m = pinyinTexts2.length();
int i; // 遍历str的
int j; // 遍历target的
char ch1; // str的
char ch2; // target的
int temp; // 记录相同字符,在某个矩阵位置值的增量,不是0就是1
if (n == 0 || m == 0) {
return 0;
}
d = new int[n + 1][m + 1];
for (i = 0; i <= n; i++) { // 初始化第一列
d[i][0] = i;
}
for (j = 0; j <= m; j++) { // 初始化第一行
d[0][j] = j;
}
for (i = 1; i <= n; i++) { // 遍历str
ch1 = pinyinTexts1.charAt(i - 1);
// 去匹配target
for (j = 1; j <= m; j++) {
ch2 = pinyinTexts2.charAt(j - 1);
if (ch1 == ch2 || ch1 == ch2 + 32 || ch1 + 32 == ch2) {
temp = 0;
} else {
temp = 1;
}
// 左边+1,上边+1, 左上角+temp取最小
d[i][j] = Math.min(Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1), d[i - 1][j - 1] + temp);
}
}
return 1 - (double) d[n][m] / Math.max(pinyinTexts1.length(), pinyinTexts2.length());
}
/**
* 根据文本的拼音相似度 从list中选择一个跟text最相似的返回
* @param text
* @param list
* @return
*/
public static String getSemblanceByPinyin(String text, List<String> list){
if(CollectionUtils.isEmpty(list)){
return null;
}
if(StringUtil.isEmpty(text)){
return null;
}
String bestMatch = null;
double maxSimilarity = 0d;
for(int i = 0 ; i < list.size(); i++){
String compareText = list.get(i);
double similarity = getSemblanceByPinyin(text,compareText);
if(similarity > maxSimilarity){
maxSimilarity = similarity;
bestMatch = compareText;
}
if(maxSimilarity == 1d){//如果完全相似 直接返回 不再继续比较了
return bestMatch;
}
}
return bestMatch;
}
这样我们就可以处理后续的逻辑了,比如根据关键词或者其余的什么来分隔和提取,做操作。这一步我们先略去,假如我们已经处理好了,这个时候需要返回给前端语音,让微信端去播放。
首先我们需要将中文转为语音,我们用百度语音接口来做:
/**
* 文字转为语音 wav格式
* @param text
* @param accessToken
* @param cuid
* @return
*/
public static byte[] text2Audio(String text,String accessToken,String cuid){
ParamCheckUtil.stringEmpty(text,"文本不能为空");
ParamCheckUtil.isTrue(text.length() > 500,"文本过长");
ParamCheckUtil.stringEmpty(accessToken,"accessToken不能为空");
ParamCheckUtil.stringEmpty(cuid,"cuid不能为空");
// 发音人选择, 基础音库:0为度小美,1为度小宇,3为度逍遥,4为度丫丫,
// 精品音库:5为度小娇,103为度米朵,106为度博文,110为度小童,111为度小萌,默认为度小美
int per = 0;
// 语速,取值0-15,默认为5中语速
int spd = 5;
// 音调,取值0-15,默认为5中语调
int pit = 5;
// 音量,取值0-9,默认为5中音量
int vol = 5;
// 下载的文件格式, 3:mp3(default) 4:pcm-16k 5:pcm-8k 6. wav
int aue = 6;
try {
// 此处2次urlencode, 确保特殊字符被正确编码
String params = "tex=" + URLEncoder.encode(URLEncoder.encode(text, "utf-8"), "utf-8");
params += "&per=" + per;
params += "&spd=" + spd;
params += "&pit=" + pit;
params += "&vol=" + vol;
params += "&cuid=" + cuid;
params += "&tok=" + accessToken;
params += "&aue=" + aue;
params += "&lan=zh&ctp=1";
HttpURLConnection conn = (HttpURLConnection) new URL(TEXT_2_AUDIO_URL).openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setConnectTimeout(5000);
PrintWriter printWriter = new PrintWriter(conn.getOutputStream());
printWriter.write(params);
printWriter.close();
String contentType = conn.getContentType();
if (contentType.contains("audio/")) {
byte[] bytes = getResponseBytes(conn);
ParamCheckUtil.isTrue(bytes == null || bytes.length == 0,"语音为空");
return bytes;
} else {
System.err.println("ERROR: content-type= " + contentType);
String res = getResponseString(conn);
log.error(res);
throw new RuntimeException(res);
}
}catch (Exception e){
throw new RuntimeException(e);
}
}
百度接口返回的是语音字节,我们先把字节保存到文件,后续nginx配置映射,返回给微信端地址就好了:
// 字节数组写出到文件 需要字节数组的数据源,以及文件的路径
public static void byteArrayToFile(byte[] src, String filePath,String fileName) {
File dir = new File(filePath);
dir.mkdirs();
File dest = new File(filePath,fileName);//输出图片的目的地,这里是文件写出的路径
ParamCheckUtil.isTrue(dest.exists(),"文件已存在");
ByteArrayInputStream is = null; //字节数组的流,先让它写到程序 src是数据源
OutputStream os = null;
try {
dest.createNewFile();
is = new ByteArrayInputStream(src);
os = new FileOutputStream(dest);
byte[] flush=new byte[5];
int len = -1;
while((len = is.read(flush))!= -1){//这里是写入程序
os.write(flush,0,len);//这一步是将程序写入到文件 这里一定要记住文件流一定要释放
}
os.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(is!=null) {
is.close();
}
if(os!=null) {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
nginx映射静态文件的配置我就略过了,现在将url地址返回到微信小程序端,js的逻辑处理如下:
//
const wxUtil = require('../../utils/wxutil.js');
const recorderManager = wx.getRecorderManager();
const innerAudioContext = wx.createInnerAudioContext({"useWebAudioImplement": true});
recorderManager.onStop((res) => {
var tempFilePath = res.tempFilePath;//音频文件地址
const fs = wx.getFileSystemManager();
fs.readFile({//读取文件并转为ArrayBuffer
filePath: tempFilePath,
success(res) {
wx.showLoading({
title: '正在语音识别中...',
});
const base64Data = wx.arrayBufferToBase64(res.data);
var fileSize = res.data.byteLength ;
var paramJson = {
format: 'pcm',
sampleRate: 16000,
encodeBitRate: 48000,
data: base64Data
};
wxUtil.post('v1/wx/audioupload', paramJson, wxUtil.audio, { isShowLoading: true }, (result) =>{
console.log( result);
innerAudioContext.src = "https://xxx" + result.data;
innerAudioContext.onPlay(() => {
console.log('onPlay')
});
innerAudioContext.onError((res) => {
console.log(res);
console.log(res.errMsg);
console.log(res.errCode)
});
innerAudioContext.onCanplay(()=>{
console.log('canplay');
});
innerAudioContext.play();
});
}
})
});
Page({
data: {
},
onLoad() {
},
//语音识别
handleTouchStart: function(e){
//录音参数
const options = {
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
format: 'pcm'
}
//开启录音
recorderManager.start(options);
wx.showLoading({
title: '正在录音中...',
});
},
handleTouchEnd: function(e){
recorderManager.stop();
}
}
})
这样微信就可以播放语音了。
现在,你可以对着手机说话,然后微信给你播放语音,比如“宝,我想你了”,“小可爱,我也想你了”。
好了,拜。