中文分词

2024-07-16  本文已影响0人  呼噜噜睡

上一篇手机录入语音翻译为文字里面讲到了如何从手机端录入语音,发往后端。其实这只是完成了工作的一半,既然我有了语音输入,你是不是应该给我返回语音消息,这样咱们才能愉快的玩耍呀。要不然,你就是个寂寞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();
  }
  }
})

这样微信就可以播放语音了。

现在,你可以对着手机说话,然后微信给你播放语音,比如“宝,我想你了”,“小可爱,我也想你了”。

好了,拜。

上一篇下一篇

猜你喜欢

热点阅读