美文网首页
中文分词

中文分词

作者: 呼噜噜睡 | 来源:发表于2024-07-16 13:38 被阅读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();
      }
      }
    })
    

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

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

    好了,拜。

    相关文章

      网友评论

          本文标题:中文分词

          本文链接:https://www.haomeiwen.com/subject/soekhjtx.html