上一篇手機(jī)錄入語(yǔ)音翻譯為文字里面講到了如何從手機(jī)端錄入語(yǔ)音,發(fā)往后端。其實(shí)這只是完成了工作的一半,既然我有了語(yǔ)音輸入,你是不是應(yīng)該給我返回語(yǔ)音消息,這樣咱們才能愉快的玩耍呀。要不然,你就是個(gè)寂寞boy,對(duì)說(shuō)的就是你。
好的,假設(shè)后端已經(jīng)獲取到了語(yǔ)音,翻譯成了中文文字。我們就要對(duì)這個(gè)中文文字進(jìn)行處理。在處理之前,有個(gè)問(wèn)題,就是中文是有多音字的,其次是語(yǔ)音翻譯接口未必那么準(zhǔn)。如果翻譯錯(cuò)了,那么后續(xù)的處理,勢(shì)必很困難。因此呢,加入你用的是百度語(yǔ)音翻譯接口,你可以先上傳詞庫(kù),也就是你的所有可能的話的單詞、句子上傳上去,這樣百度翻譯的時(shí)候就能準(zhǔn)確一點(diǎn)。
即使這樣做了,依舊會(huì)存在翻譯錯(cuò)誤和不準(zhǔn)確的情況。這就需要用到相似度,進(jìn)行匹配了。也就是那這個(gè)翻譯的句子,跟這個(gè)所有可能的句子列表進(jìn)行相似度匹配,找到一個(gè)最相似的,并且這個(gè)相似度超過(guò)某個(gè)閾值,我們就說(shuō),我們就認(rèn)為這句話本來(lái)是這個(gè)意思,就可以按照后續(xù)邏輯進(jìn)行處理了。否則,我們認(rèn)為無(wú)法識(shí)別這句話的意圖,進(jìn)行報(bào)錯(cuò)提示。比如“寶,你說(shuō)的太難了,我正在學(xué)習(xí),請(qǐng)給我一點(diǎn)時(shí)間好嗎,我一定...”。
下面具體解釋一下幾種不同的相似度匹配思路:
部分詞的相似度匹配:
比如對(duì)于報(bào)表中心來(lái)說(shuō),用戶輸入“查看XX項(xiàng)目利潤(rùn)報(bào)表”
那么就要去匹配到底是哪張報(bào)表,假設(shè)關(guān)鍵字“查看”翻譯無(wú)誤,已經(jīng)順利提取出來(lái)了,
同時(shí)“XX項(xiàng)目”也通過(guò)某種方式識(shí)別和提取出來(lái)了,
那就只剩下“利潤(rùn)報(bào)表”這個(gè)關(guān)鍵詞了,如果根據(jù)精確匹配無(wú)法找到,
那么就需要根據(jù)這個(gè)詞進(jìn)行相似性匹配了。
這里只是用到了部分的關(guān)鍵詞“XX項(xiàng)目”進(jìn)行相似度匹配,
因此叫做部分詞的相似度匹配。
整句話的相似度匹配:
有時(shí)候上面的依舊是無(wú)法識(shí)別到的,比如“查看”這個(gè)開頭的關(guān)鍵字,
由于用戶發(fā)音不準(zhǔn)或者翻譯有誤,將其翻譯為“查到” “看到”,
但是后面確又翻譯的準(zhǔn)確無(wú)誤。其實(shí)很容易就能看出來(lái)用戶是想要干什么,
但是代碼只能無(wú)情的將其判定為“未知的語(yǔ)音指令”,這很讓人苦惱和心痛。
因此這時(shí)候就需要將整句話進(jìn)行拼音相似度匹配,以期從中可以做到最大范圍的匹配。
但是數(shù)據(jù)庫(kù)中并沒有整句話的詞庫(kù),有的只是報(bào)表的名稱?那該怎么辦呢?
因此可以在項(xiàng)目啟動(dòng)的時(shí)候,一次性查出所有報(bào)表名稱,
然后用代碼窮舉各種前綴性的詞,后綴性的詞,進(jìn)行排列組合。
然后拿著翻譯的話跟所有餓排列組合句子去進(jìn)行相似度匹配。
假如你選定了某種相似度匹配的思路,那么問(wèn)題來(lái)了,怎樣相似度匹配呢?一種做法是利用已經(jīng)有的工具庫(kù),進(jìn)行分詞處理,或者某種算法,計(jì)算余弦夾角或者什么距離,這一塊我不太懂,需要去研究不同的工具庫(kù)以及訓(xùn)練詞庫(kù)。
另一種做法就是將文本轉(zhuǎn)為拼音,然后比較拼音的相似度。因?yàn)橐话愕臉I(yè)務(wù)性話題和選詞范圍大致固定,因此我選用拼音相似度來(lái)做匹配。
嗯,首先將中文文本轉(zhuǎn)為拼音吧,先引入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;//遇到不能識(shí)別的 默認(rèn)保留
}
if(StringUtil.isEmpty(separator)){
separator = "";
}
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
//拼音小寫
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
//不帶聲調(diào)
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
//要轉(zhuǎn)換的中文,格式,轉(zhuǎn)換之后的拼音的分隔符,遇到不能轉(zhuǎn)換的是否保留 wo,shi,zhong,guo,ren,,hello
return PinyinHelper.toHanYuPinyinString(chineseText, format, separator, unConvertIsKeep);
}catch (Exception e){
throw new RuntimeException(e);
}
}
相似度工具類TextSimilarityUtil:
/**
* 計(jì)算兩個(gè)中文文本的相似度 相似度越高 值越大 最大為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; // 記錄相同字符,在某個(gè)矩陣位置值的增量,不是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());
}
/**
* 根據(jù)文本的拼音相似度 從list中選擇一個(gè)跟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){//如果完全相似 直接返回 不再繼續(xù)比較了
return bestMatch;
}
}
return bestMatch;
}
這樣我們就可以處理后續(xù)的邏輯了,比如根據(jù)關(guān)鍵詞或者其余的什么來(lái)分隔和提取,做操作。這一步我們先略去,假如我們已經(jīng)處理好了,這個(gè)時(shí)候需要返回給前端語(yǔ)音,讓微信端去播放。
首先我們需要將中文轉(zhuǎn)為語(yǔ)音,我們用百度語(yǔ)音接口來(lái)做:
/**
* 文字轉(zhuǎn)為語(yǔ)音 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,"文本過(guò)長(zhǎng)");
ParamCheckUtil.stringEmpty(accessToken,"accessToken不能為空");
ParamCheckUtil.stringEmpty(cuid,"cuid不能為空");
// 發(fā)音人選擇, 基礎(chǔ)音庫(kù):0為度小美,1為度小宇,3為度逍遙,4為度丫丫,
// 精品音庫(kù):5為度小嬌,103為度米朵,106為度博文,110為度小童,111為度小萌,默認(rèn)為度小美
int per = 0;
// 語(yǔ)速,取值0-15,默認(rèn)為5中語(yǔ)速
int spd = 5;
// 音調(diào),取值0-15,默認(rèn)為5中語(yǔ)調(diào)
int pit = 5;
// 音量,取值0-9,默認(rèn)為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,"語(yǔ)音為空");
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);
}
}
百度接口返回的是語(yǔ)音字節(jié),我們先把字節(jié)保存到文件,后續(xù)nginx配置映射,返回給微信端地址就好了:
// 字節(jié)數(shù)組寫出到文件 需要字節(jié)數(shù)組的數(shù)據(jù)源,以及文件的路徑
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; //字節(jié)數(shù)組的流,先讓它寫到程序 src是數(shù)據(jù)源
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);//這一步是將程序?qū)懭氲轿募? 這里一定要記住文件流一定要釋放
}
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映射靜態(tài)文件的配置我就略過(guò)了,現(xiàn)在將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({//讀取文件并轉(zhuǎn)為ArrayBuffer
filePath: tempFilePath,
success(res) {
wx.showLoading({
title: '正在語(yǔ)音識(shí)別中...',
});
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() {
},
//語(yǔ)音識(shí)別
handleTouchStart: function(e){
//錄音參數(shù)
const options = {
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
format: 'pcm'
}
//開啟錄音
recorderManager.start(options);
wx.showLoading({
title: '正在錄音中...',
});
},
handleTouchEnd: function(e){
recorderManager.stop();
}
}
})
這樣微信就可以播放語(yǔ)音了。
現(xiàn)在,你可以對(duì)著手機(jī)說(shuō)話,然后微信給你播放語(yǔ)音,比如“寶,我想你了”,“小可愛,我也想你了”。
好了,拜。