2019-10-06 18:22:55 qq_40808154 阅读数 314
  • C++语音识别开篇

    本篇mark老师将教大家使用第三方库的调用来简单的实现语音识别。随着机器学习和人工智能的热闹,国内语音行业也可谓是百花齐放。 语音识别一个伟大的时代已在我们身边悄悄走来。

    5905 人正在学习 去看看 杨波

之前在网上看到一个题目使用语音控制你的浏览器,感觉挺有意思的,就想着实现一个简单的语音识别程序,这里我选择的是百度语音识别,还有好多不错的如科大讯飞等都可以使用。

语音识别过程分为三个部分:

1)录音

2)获取参数access token,有效期为一个月(开发文档)

3)上传录音文件

1.首先你需要一个注册一个百度账号,进行登录,可以直接使用注册过的百度网盘账号进行登录,登录的网址在此处https://login.bce.baidu.com/,选择语音识别,点击创建应用,为你的应用起一个名字:

 创建完成后,如下图所示,AppID,API Key,Secret Key这三个参数需要用到

 

 然后看百度相关的技术开发文档,必要的参数一定带全。

2.由于需要进行录音,这里使用到pyaudio库,需要先进行安装,但是直接使用pip install pyaudio进行安装会出现错误,推荐使用下面的命令进行安装:
sudo apt-get install portaudio19-dev python-all-dev python3-all-dev

pip install pyaudio

安装成功后,接下来开始第1部分录音,代码中都有详细的注释,直接上代码:

import pyaudio
import wave
import requests
import json
import base64
import os


#1.录音
#用Pyaudio录制音频(该库可以进行录音,播放,生成wav文件)
def audio_record(rec_time,filename):
    """
    :param rec_time: 音频录制时间
    :param filename: 输出音频文件名
    """
    CHUNK=1024#定义数据流块
    FORMAT=pyaudio.paInt16#16bit编码格式
    CHANNELS=1#单声道
    RATE=16000#16000采样频率
    #创建一个音频对象
    p=pyaudio.PyAudio()
    #创建音频数据流
    stream=p.open(format=FORMAT,#音频流wav格式
                  channels=CHANNELS,#单声道
                  rate=RATE,#采样率16000
                  input=True,#输入
                  frames_per_buffer=CHUNK)
    print('start recording...')
    frames=list()#空列表用于保存录制的音频流
    #录制音频数据
    for i in range(0,int(RATE/CHUNK*rec_time)):
        data=stream.read(CHUNK)
        frames.append(data)
    #录制完成
    print(frames)
    #停止数据流
    stream.stop_stream()
    stream.close()
    #关闭pyaudio
    p.terminate()
    print('recording done...')
    #保存音频文件
    with wave.open(filename,'wb') as f:
        f.setnchannels(CHANNELS)#设置音频声道数
        f.setsampwidth(p.get_sample_size(FORMAT))#以字节为单位返回样本宽度
        f.setframerate(RATE)#设置取样频率

第2部分,获取参数token

#2 使用appKey secretKey 访问 https://openapi.baidu.com 换取 token
def Get_token():
    #baidu_server='https://openapi.baidu.com/oauth/2.0/token'
    grant_type = 'client_credentials'
    # API KEY
    client_id = 'PrKnhUppGEsqrG8mVG2qIq8O'
    # SECRERT KEY
    client_secret = 'Al4cRfrlRGaMCCkz3kLsd4MXOoQP28iD'

    # 拼接url
    url = 'https://openapi.baidu.com/oauth/2.0/token?grant_type=client_credentials&client_id={}&client_secret={}'.format(
            client_id, client_secret)
    # 发送Post请求 获取acess_token
    req = requests.post(url)
    data_dict = json.loads(req.text)  # 将json字符串转换为python字典
    print(req.text)
    print(data_dict['access_token'])

    return data_dict['access_token']


此段代码,可以单独运行下,打印token看是否能获取到,token指为如下所示:

返回的数据:scope中含有audio_voice_assistant_get 表示有语音识别能力, 注意语音服务的调用地址是https://openapi.baidu.com/oauth/2.0/token

第3部分,上传录音(有两种方式:第一种是Json方式,第二种方式是Raw方式,详细见开发文档)

#3.上传录音文件
def BaiduYuYin(file_url,token):
    try:
        RATE='16000'
        FORMAT='wav'
        CUID='wate_play'
        DEV_PID='1536' #普通话:支持简单的英文识别

        file_url=file_url
        token = token

        #以字节格式读取文件之后进行编码
        with open(file_url,'rb') as f:
            speech=base64.b64encode(f.read()).decode('utf-8')
        size = os.path.getsize(file_url)#语音文件的字节数
        headers={'Content-Type':'application/json'}#json格式post上传本地文件
        url='https://vop.baidu.com/server_api'
        data={
            "format":FORMAT,#格式
            "rate":RATE,#取样频率,固定值16000
            "dev_pid":DEV_PID,#语音识别类型
            "speech":speech,#本地语音文件的二进制数据,需要进行base64编码
            "cuid":CUID,#用户唯一标识,用来区分用户 建议填写能区分用户的机器MAC地址或IMEI码,长度为60字符以内。
            "len":size,#语音文件的字节数
            "channel":1,#声道数,仅支持单声道,固定值为1
            "token":token,
        }
        req=requests.post(url,json.dumps(data),headers)
        data_dict=json.loads(req.text)
        print(data_dict['result'][0])
        return data_dict['result'][0][::-1]
    except:
        return '识别不清楚'

最后写一个调度函数run(),运行程序

#4.调度
def run(rec_time,file_name):
    #1.录音
    audio_record(rec_time,file_name)

    #2.获取token
    access_token=Get_token()

    #3.上传录音
    BaiduYuYin(file_name,access_token)


if __name__ == '__main__':
    #录音时间为5秒,文件名为'record1.wav'
    run(5,'record1.wav')

若想要你的电脑可以录音,需要提前打开麦克风,运行此程序后在五秒内说出一段话(可能需要大点声),控制台会打印出来,测试过之后发现准确率还可以,

2019-02-15 12:14:12 chen_pan_pan 阅读数 4693
  • C++语音识别开篇

    本篇mark老师将教大家使用第三方库的调用来简单的实现语音识别。随着机器学习和人工智能的热闹,国内语音行业也可谓是百花齐放。 语音识别一个伟大的时代已在我们身边悄悄走来。

    5905 人正在学习 去看看 杨波

小程序实现语音识别功能,通过语音的方式代替手动输入查询。经过查询微信小程序api,发现微信并没有对外提供语音识别的api,所以要另外想办法。经过多发查找资料发现了思路。

解决思路:

微信小程序提供了录音的功能,通过录音的方式,然后把录音文件传到服务器,后台服务器将语音转码,然后再调用第三方语音识别api,我这里使用的是百度的api,最后在将识别的文字返回给微信小程序。

直接上代码:

小程序端代码:

startRecord: function() {

if (this.recorderManager == null) {

this.recorderManager = wx.getRecorderManager();

this.options = {

duration: 10000,

sampleRate: 16000,

numberOfChannels: 1,

encodeBitRate: 64000,

format: 'mp3',

frameSize: 50

}

}

this.recorderManager.start(this.options);

this.recorderManager.onStop((res) => {

console.log(res)

wx.uploadFile({

url: 'https://xxxx',//将录音文件传到后台服务器

filePath: res.tempFilePath,

method:'POST',

name: 'file',

header: {

'content-type': 'multipart/form-data'

},

success: function(res) {

console.log(res);

},

fail: function() {

console.log("语音识别失败");

}

})

});

},

stopRecord: function() {

this.recorderManager.stop()

}

 

服务端代码:

注意:需要使用mp3plugin.jar包,网上可以下载到。

    // 百度语音识别
    public static final String APP_ID = "xxx";
    public static final String API_KEY = "xxx";
    public static final String SECRET_KEY = "xxx";

    /**
     * @Description TODO
     * @return
     */
    @RequestMapping(value = "speechRecognition", method = RequestMethod.POST)
    @ResponseBody
    public Object speechReco(HttpServletRequest request) {
        MultipartFile file = ((MultipartHttpServletRequest) request).getFile("file");
        try {
            byte[] pcmBytes = mp3Convertpcm(file.getInputStream());
            org.json.JSONObject resultJson = speechBdApi(pcmBytes);
            System.out.println(resultJson.toString());
            if (null != resultJson && resultJson.getInt("err_no") == 0) {
                return resultJson.getJSONArray("result").get(0).toString().split(",")[0];
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return "";
    }

    /**
     * @Description MP3转换pcm
     * @param mp3Stream
     *            原始文件流
     * @return 转换后的二进制
     * @throws Exception
     */
    public byte[] mp3Convertpcm(InputStream mp3Stream) throws Exception {
        // 原MP3文件转AudioInputStream
        BufferedInputStream zipTest=new BufferedInputStream(mp3Stream);
        //重新包装一层,不然会报错。
        AudioInputStream mp3audioStream = AudioSystem.getAudioInputStream(zipTest);
        // 将AudioInputStream MP3文件 转换为PCM AudioInputStream
        AudioInputStream pcmaudioStream = AudioSystem.getAudioInputStream(AudioFormat.Encoding.PCM_SIGNED,
                mp3audioStream);
        byte[] pcmBytes = IOUtils.toByteArray(pcmaudioStream);
        pcmaudioStream.close();
        mp3audioStream.close();
        return pcmBytes;
    }

    /**
     * @Description 调用百度语音识别API
     * @param pcmBytes
     * @return
     */
    public static org.json.JSONObject speechBdApi(byte[] pcmBytes) {
        // 初始化一个AipSpeech
        AipSpeech client = new AipSpeech(APP_ID, API_KEY, SECRET_KEY);
        // 可选:设置网络连接参数
        client.setConnectionTimeoutInMillis(2000);
        client.setSocketTimeoutInMillis(60000);
        // 调用接口
        org.json.JSONObject res = client.asr(pcmBytes, "pcm", 16000, null);
        return res;
    }

 

如果我的文章帮助到了大家,减少大家的弯路,愿意打赏的请扫下面的二维码。也可留言。

 

2019-10-11 20:42:34 Change_xiao 阅读数 96
  • C++语音识别开篇

    本篇mark老师将教大家使用第三方库的调用来简单的实现语音识别。随着机器学习和人工智能的热闹,国内语音行业也可谓是百花齐放。 语音识别一个伟大的时代已在我们身边悄悄走来。

    5905 人正在学习 去看看 杨波

微信前台录入语音PHP后台调用百度AI进行语音识别

先去百度AI下载 PHP sdk
目录如下
在这里插入图片描述
**

由于微信小程序暂且支持真机调试才会出现效果
所以真机调试的设备必须与js中发起请求的url在同一网络下

**

微信前台代码
.html

<button type='primary' bindtouchstart='startrecorderHandel' bindtouchend="sendrecorderHandel">点击录音</button>
<view>
  你说的话是:
  <view>
    {{msg}}
  </view>
</view>

.js

// 录音对象
const recorderManager = wx.getRecorderManager();

function sendRecord(src) {
  var obj = {
    url: "http://49.122.47.146/php-yuyin/yuyin.php",
    filePath: src,
    name: "recordFile",
    header: {
      'Content-Type': 'application/json'
    },
    success: function (result) {
      var data = JSON.parse(result.data);
      // msg 为最终语音识别的字符串
      var msg = data.result;
      // 获取当前页面对象
      var page = getCurrentPages()[0];
      page.setData({ msg: msg });
    },
    fail: function (err) {
      console.log(err);
    }
  };
  wx.uploadFile(obj)
}

// 结束录音的时候触发 
recorderManager.onStop((res) => {
  // 获取文件路径-提交到后台-后台发送到百度
  sendRecord(res.tempFilePath);
})

recorderManager.onError ((res) => {
  console.log("error", res);
});

Page({

  /**
   * 页面的初始数据
   */
  data: {
    msg: ""
  },
  // 按下按钮的时候触发
  startrecorderHandel() {
    // 开始录音
    recorderManager.start({
    });
  },
  // 松开按钮的时候触发-发送录音
  sendrecorderHandel() {
    // 结束录音
    recorderManager.stop();
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    wx.authorize({
      scope: 'record'
    })
  },

  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady: function () {

  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {

  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide: function () {

  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload: function () {

  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh: function () {

  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom: function () {

  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage: function () {

  }
})

PHP后台代码

<?php
/**
 * 调用百度AI识别语音
 */
require_once "aip-speech-php-sdk-1.6.0/AipSpeech.php";

$name = "./upload/".$_FILES['recordFile']['name'];
if(move_uploaded_file($_FILES['recordFile']['tmp_name'],$name)){

    $APP_ID = '你的$APP_ID';//你的$APP_ID
    $API_KEY = '你的$API_KEY';//你的$API_KEY
    $SECRET_KEY = '你的$SECRET_KEY';//你的$SECRET_KEY

    $client = new AipSpeech($APP_ID, $API_KEY, $SECRET_KEY);
    $res = $client->asr(file_get_contents($name), 'm4a', 16000, array(
        'dev_pid' => 1536,
    ));
    echo json_encode($res);
    die();
}else{
    echo "上传失败";
}

die();
2010-08-03 13:26:00 xieyan0811 阅读数 4479
  • C++语音识别开篇

    本篇mark老师将教大家使用第三方库的调用来简单的实现语音识别。随着机器学习和人工智能的热闹,国内语音行业也可谓是百花齐放。 语音识别一个伟大的时代已在我们身边悄悄走来。

    5905 人正在学习 去看看 杨波

 

 

 

1.     说明
以下例程功能为:在应用程序中使用intent来调出语言识别界面,录音并识别后将识别的字串返回给应用程序。注意:使用前需要安装语音识别程序如语音搜索。

2.     本例参考自android例程:
development/samples/ApiDemos/src/com/example/android/apis/app/VoiceRecognition.java

3.     可从此处下载可独立运行的代码:
http://download.csdn.net/source/2591401

4.     核心代码及说明

package com.android.mystt1;

 

import android.app.Activity;

import android.content.Intent;

import android.content.pm.PackageManager;

import android.content.pm.ResolveInfo;

import android.os.Bundle;

import android.speech.RecognizerIntent;

import android.view.View;

import android.view.View.OnClickListener;

import android.widget.ArrayAdapter;

import android.widget.Button;

import android.widget.ListView;

 

import java.util.ArrayList;

import java.util.List;

 

public class MyStt1Activity extends Activity implements OnClickListener {

       private static final int VOICE_RECOGNITION_REQUEST_CODE = 1234;

       private ListView mList;          // 显示识别后字串的list控件

 

       @Override

       public void onCreate(Bundle savedInstanceState) {

                super.onCreate(savedInstanceState);

                setContentView(R.layout.main);

                Button speakButton = (Button) findViewById(R.id.btn_speak); // 识别按钮

                 mList = (ListView) findViewById(R.id.list);

                PackageManager pm = getPackageManager();

                List activities = pm.queryIntentActivities(

                          new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0); //本地识别程序

//                       new Intent(RecognizerIntent.ACTION_WEB_SEARCH), 0); // 网络识别程序

                if (activities.size() != 0) {

                         speakButton.setOnClickListener(this);

                } else {                 // 若检测不到语音识别程序在本机安装,测将扭铵置灰

                         speakButton.setEnabled(false);

                         speakButton.setText("Recognizer not present");

                }

       }

 

       public void onClick(View v) {

                if (v.getId() == R.id.btn_speak) {

                         startMysttActivityActivity();

                }

       }

 

       private void startMysttActivityActivity() {          // 开始识别

                Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);

                intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,

                                   RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);

                intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Speech recognition demo");

                startActivityForResult(intent, VOICE_RECOGNITION_REQUEST_CODE);
                //
调出识别界面

    }

 

       @Override

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {

                if (requestCode == VOICE_RECOGNITION_REQUEST_CODE && resultCode == RESULT_OK) {

                         // Fill the list view with the strings the recognizer thought it could have heard

                         ArrayList matches = data.getStringArrayListExtra(

                                            RecognizerIntent.EXTRA_RESULTS);

                         mList.setAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1,

                                            matches));

                }

                // 语音识别后的回调,将识别的字串在list中显示

                super.onActivityResult(requestCode, resultCode, data);

       }

}

(转载请注明出处: http://xy0811.spaces.live.com/) 

2017-09-18 10:19:03 happycxz 阅读数 15043
  • C++语音识别开篇

    本篇mark老师将教大家使用第三方库的调用来简单的实现语音识别。随着机器学习和人工智能的热闹,国内语音行业也可谓是百花齐放。 语音识别一个伟大的时代已在我们身边悄悄走来。

    5905 人正在学习 去看看 杨波

微信小程序录音(silk和mp3)转olami语音识别和语义处理的api服务(ubuntu16.04服务器上实现)


重要的写在前面

重要事项一:
所有相关更新,我优先更新到我个人博客中,其它地方的文章属于本人或他人转发,不一定及时同步。原文链接是: http://www.happycxz.com/m/?p=32

重要事项二:
目前本文中提到的API已支持微信小程序录音文件格式:silk v3、mp3。
注:微信小程序开发工具上的录音不论是新接口还是老接口,都是webm/base64格式,虽然后辍名是silk(或mp3),但不是真正的silk v3(或mp3)格式的,打开看头部是 data:audio/webm;base64, 开头的。
为了便于调试,2017年9月份时我补逻辑给支持上了,但是:我在2017年11月份发现原有的webm/base格式又不支持了
api服务器端代码没有动过,跟踪查过,目前仅发现在2017年10月份之前用微信小程序开发工具录的文件,还是可以支持的,在10月份之后的文件,就不支持了,具体什么原因只能问老马家的人了。
结论是:调用我本文中提到的两个接口,只能用真机做测试,不能用电脑录音来测试语音识别了。

重要事项三:
想要用我这个API,务必先去cn.olami.ai申请appKey和appSecret,然后将appKey告知我,我加进支持列表方可调用,二者缺一不可。文末有将有调用此文提到的API服务的案例以及源码分享文章链接。

调用案例:“遥知之”智能小秘,欢迎扫码体验:
小程序码小

重要事项四:
欢迎转载本文,没有什么别的要求,请保留:
原文链接:http://www.happycxz.com/m/?p=32
本文所有源码对应码云链接:https://gitee.com/happycxz/silk2asr
本文所有源码对应github链接:https://github.com/happycxz/silk2asr

为什么做?

前不久刚发布了一个智能生活信息查询的小助手“遥知之”,可惜只能手动输入来玩,这一点体验很不好,因为微信小程序录音是silk格式的,现在主要的语音识别接口都不支持。

在网上搜了下相应的功能,也只有php做的开源代码实现的silk转wav的服务器代码,首先我不熟悉PHP,其次也不知道后期有没有维护,干脆自己做一个tomcat + java版的,权当学习娱乐一下。

怎么做?

准备环境

先需要有一个支持https的服务器,我目前用的服务器是阿里云秒杀的免费最低配置的服务器,预装的ubuntu16.04 LTS版,然后自己捣鼓一下,配置上了https,具体是用 nginx + let’s encrypt + tomcat来提供的https的API。这里不详细介绍,感兴趣的自己研究下。

需要一个silk解码器,网上有一牛在2015年年初曾经发贴讨论过这个话题:silk v3 编码的音频怎么转换成 wav 或 mp3 之类的?

而且此牛后面有持续研究,提供了开源的silk_v3_decoder项目,具体见:kn007大牛的silk_v3_decoder

对了,开源项目是github上的,服务器上装个git,这不用额外再说明了吧。

搭建服务步骤

下载silk-v3-decoder

基本就是在服务器上找个目录,把大牛kn007的项目下载下来。

root@alijod:/home/jod/wechat_app# mkdir download
root@alijod:/home/jod/wechat_app# cd download/
root@alijod:/home/jod/wechat_app/download# git clone https://github.com/kn007/silk-v3-decoder.git
Cloning into 'silk-v3-decoder'...
remote: Counting objects: 634, done.
remote: Total 634 (delta 0), reused 0 (delta 0), pack-reused 634
Receiving objects: 100% (634/634), 72.79 MiB | 9.50 MiB/s, done.
Resolving deltas: 100% (352/352), done.
Checking connectivity... done.
root@alijod:/home/jod/wechat_app/download# ll
total 12
drwxr-xr-x 3 root root 4096 Sep 18 10:11 ./
drwxr-xr-x 7 root root 4096 Sep 18 10:11 ../
drwxr-xr-x 5 root root 4096 Sep 18 10:11 silk-v3-decoder/
root@alijod:/home/jod/wechat_app/download# ls silk-v3-decoder/
converter_beta.sh  converter.sh  LICENSE  README.md  silk  windows

看上述目录,其实只用到了silk这个目录,和converter.sh这个脚本。silk目录中的C代码需要gcc编译,converter.sh脚本需要修改一下,后续都会提。

编译silk_v3_decoder

根据https://github.com/kn007/silk-v3-decoder上的README,用上这个工具,需要gcc和ffmpeg,gcc是在编译silk时执行make时用到的(普及一下小白),ffmpeg其实是脚本里要用的,与编译无关。事实是,ffmpeg在整个服务搭建过程确实不是必备的,后文将有针对这个额外说明,只是本人偷懒,暂时不想再深入研究了。

gcc的环境,如果没有安装,自己网搜吧,这里不扯了,直接进入正题:

root@alijod:/home/jod/wechat_app/download# cd silk-v3-decoder/silk/
root@alijod:/home/jod/wechat_app/download/silk-v3-decoder/silk# ll
total 32
drwxr-xr-x 5 root root  4096 Sep 18 10:11 ./
drwxr-xr-x 5 root root  4096 Sep 18 10:11 ../
drwxr-xr-x 2 root root  4096 Sep 18 10:11 interface/
-rw-r--r-- 1 root root  3278 Sep 18 10:11 Makefile
drwxr-xr-x 2 root root 12288 Sep 18 10:11 src/
drwxr-xr-x 2 root root  4096 Sep 18 10:11 test/
root@alijod:/home/jod/wechat_app/download/silk-v3-decoder/silk# make
…………
…………(这里是一大段编译过程日志)
…………
a - src/SKP_Silk_scale_vector.o
gcc -c -Wall -enable-threads -O3   -Iinterface -Isrc -Itest  -o test/Decoder.o test/Decoder.c
test/Decoder.c: In function ‘main’:
test/Decoder.c:187:9: warning: ignoring return value of ‘fread’, declared with attribute warn_unused_result [-Wunused-result]
         fread(header_buf, sizeof(char), 1, bitInFile);
         ^
g++  -L./ test/Decoder.o -lSKP_SILK_SDK -o decoder
root@alijod:/home/jod/wechat_app/download/silk-v3-decoder/silk# ls
decoder  interface  libSKP_SILK_SDK.a  Makefile  src  test
root@alijod:/home/jod/wechat_app/download/silk-v3-decoder/silk# 

可以看到,上面编译过程中,最后出现了一个warning,不过没关系,ls查一下,第一个“decoder”就是我们要用的binary啦,有它就证明编译成功了。

测试silk_v3_decoder功能

接下来就要验证一下编出来的这个能不能用了。
根据https://github.com/kn007/silk-v3-decoder上的README,摘下来一段:

sh converter.sh silk_v3_file/input_folder output_format/output_folder flag(format)

比如转换一个文件,使用:

sh converter.sh 33921FF3774A773BB193B6FD4AD7C33E.slk mp3

注意:其中33921FF3774A773BB193B6FD4AD7C33E.slk是要转换的文件,而mp3是最终转换后输出的格式。

参考上面那个例子就好了,脚本参数只有两个,一个是源文件相对或绝对路径,另一个是目标格式。
也就是说上述命令会将33921FF3774A773BB193B6FD4AD7C33E.slk(注意,例子里是slk后辍,你自己在获取微信小程序录音重命名时如果是.silk,别疑惑了,linux环境文件后辍名是没有实际意义的,感兴趣自己网搜,to小白)转码成33921FF3774A773BB193B6FD4AD7C33E.mp3。

没有silk源文件?别急,我准备了个silk_v3录音文件,附带着转出来的mp3一起放在我服务器上了,需要的可以去下载(右击后另存即可,mp3可以在线播放,silk播放不了,直接单击会“403”):
微信小程序原始录音文件:sample.silk
converter.sh脚本转码后的文件:sample.mp3

附上我转码的操作过程:

root@alijod:/home/jod/wechat_app/download/silk-v3-decoder# ll
total 48
drwxr-xr-x 5 root root 4096 Sep 18 10:43 ./
drwxr-xr-x 3 root root 4096 Sep 18 10:11 ../
-rw-r--r-- 1 root root 4131 Sep 18 10:11 converter_beta.sh
-rw-r--r-- 1 root root 3639 Sep 18 10:11 converter.sh
drwxr-xr-x 8 root root 4096 Sep 18 10:11 .git/
-rw-r--r-- 1 root root 1076 Sep 18 10:11 LICENSE
-rw-r--r-- 1 root root 3582 Sep 18 10:11 README.md
-rw-r----- 1 root root 6188 Sep 18 10:43 sample.silk
drwxr-xr-x 5 root root 4096 Sep 18 10:26 silk/
drwxr-xr-x 3 root root 4096 Sep 18 10:11 windows/
root@alijod:/home/jod/wechat_app/download/silk-v3-decoder# 
root@alijod:/home/jod/wechat_app/download/silk-v3-decoder# 
root@alijod:/home/jod/wechat_app/download/silk-v3-decoder# sh converter.sh sample.silk mp3
-e [OK] Convert sample.silk To sample.mp3 Finish.
root@alijod:/home/jod/wechat_app/download/silk-v3-decoder# ll
total 68
drwxr-xr-x 5 root root  4096 Sep 18 10:43 ./
drwxr-xr-x 3 root root  4096 Sep 18 10:11 ../
-rw-r--r-- 1 root root  4131 Sep 18 10:11 converter_beta.sh
-rw-r--r-- 1 root root  3639 Sep 18 10:11 converter.sh
drwxr-xr-x 8 root root  4096 Sep 18 10:11 .git/
-rw-r--r-- 1 root root  1076 Sep 18 10:11 LICENSE
-rw-r--r-- 1 root root  3582 Sep 18 10:11 README.md
-rw-r--r-- 1 root root 17709 Sep 18 10:43 sample.mp3
-rw-r----- 1 root root  6188 Sep 18 10:43 sample.silk
drwxr-xr-x 5 root root  4096 Sep 18 10:26 silk/
drwxr-xr-x 3 root root  4096 Sep 18 10:11 windows/

关于converter.sh脚本

vim打开converter.sh脚本,显示一下行号(vim中输入”:set nu”后回车,我为小白操心不少),想要简单使用,其实只需要关注最后面这一段,如果想要深入研究,最好是把脚本完整过程搞懂。

 82 $cur_dir/silk/decoder "$1" "$1.pcm" > /dev/null 2>&1
 83 if [ ! -f "$1.pcm" ]; then
 84         ffmpeg -y -i "$1" "${1%.*}.$2" > /dev/null 2>&1 &
 85         ffmpeg_pid=$!
 86         while kill -0 "$ffmpeg_pid"; do sleep 1; done > /dev/null 2>&1
 87         [ -f "${1%.*}.$2" ]&&echo -e "${GREEN}[OK]${RESET} Convert $1 to ${1%.*}.$2 success, ${YELLOW}but not a silk v3 encoded file.${RESET}"&&exit
 88         echo -e "${YELLOW}[Warning]${RESET} Convert $1 false, maybe not a silk v3 encoded file."&&exit
 89 fi
 90 ##ffmpeg -y -f s16le -ar 24000 -ac 1 -i "$1.pcm" "${1%.*}.$2" > /dev/null 2>&1
 91 ffmpeg -y -f s16le -ar 12000 -ac 2 -i "$1.pcm" -f wav -ar 16000 -ac 1 "${1%.*}.$2" > /dev/null 2>&1
 92 ffmpeg_pid=$!
 93 while kill -0 "$ffmpeg_pid"; do sleep 1; done > /dev/null 2>&1
 94 rm "$1.pcm"
 95 [ ! -f "${1%.*}.$2" ]&&echo -e "${YELLOW}[Warning]${RESET} Convert $1 false, maybe ffmpeg no format handler for $2."&&exit
 96 echo -e "${GREEN}[OK]${RESET} Convert $1 To ${1%.*}.$2 Finish."
 97 exit

其实关键的两行也就是Line 82和Line 90。第82行就是调用我们上文编出来的decoder解码silk_v3文件,第90行是将silk_v3文件解码出来的raw data数据转成相应格式。

这里额外说明一下我跟这两行的几个插曲:

插曲一:speex压缩

我做这个SILK语音识别服务的起初目的是让我的“遥知之”支持语音输入功能,“遥知之”上用的OLAMI接口也有语音识别,而且研究了一下他们的JAVA SDK和在线文档,从在线文档(OLAMI 文档中心->语音识别接口文档->“支持的音频格式”)上看是支持wav格式,另外支持speex压缩。

wav格式文件是很占空间的(相当于PCM原始采样数据未经压缩的,加了一个文件头),如下图所示(可能实际speex压缩的效果会更好一点):
pcm, silk, speex格式文件占空间比较图

如果将数据通过speex压缩,就只需要脚本中的第62行,就不用依赖ffmpeg去转码也可以直接省流量上传到OLAMI语音识别服务器了。这里就是为什么我前面说到,ffmpeg并不是此服务搭建中必备之原因。

如果通过speex会大大降低传输效率,于是期间我有花蛮长时间在研究如何将pcm数据转成speex的,比如怎么调用c代码实现的speex的编码(java下通过JNI调用speex的encoder,研究未果,放弃了这个方案),后来又找了jspeex(java版的speex codec)等等,后面因有另一个省事方案,这里用jspeex的方案就中断未深入研究了,其实应该是行的通的。

在QQ群(群号:656580961)里提了一下,热心的群主“黄眉毛”说olami java sdk里默认是将wav或pcm通过speex压缩传输的,这样一来,我只需要将wav或pcm对接olami java sdk就可以实现“省流量”传输到olami语音识别服务器了。这就是我最终采用的省事方案。

插曲二:采样率不适配

发现通过微信小程序端录音出来的silk v3文件,经过kn007的converter.sh转出来的wav文件,再送到olami语音识别接口,发现识别效果很糟,把wav文件拿出来听听,似乎也正常。

这时候想起来脚本中PCM转wav是按24K转的,转出来的WAV应该是24K的,而olami语音识别端支持的是16K(讯飞还支持8K的),可能是这个采样率不一致导致的识别率差,网搜了一下,还真有前人碰到过相同问题,参见此文文中提到的“误打误撞”那一段:从微信中提取语音文件,并转换成文字的全自动化解决方案 ,他的误打误撞的原理应该是小程序录音就是双通道12K的,然后ffmpeg额外指定一下参数将双通道12K的数据流转成16K的wav。

好了,离不开ffmpeg了,需要它帮着转采样率呀,speex压缩又不负责解决采样率转换的问题。
重要的事说三遍:在原始脚本的基础上,修改一下第90行:
重要的事说三遍:在原始脚本的基础上,修改一下第90行:
重要的事说三遍:在原始脚本的基础上,修改一下第90行:

ffmpeg -y -f s16le -ar 12000 -ac 2 -i "$1.pcm" -f wav -ar 16000 -ac 1 "${1%.*}.$2" > /dev/null 2>&1

插曲三:假silk(或mp3)真webm/base64格式

在使用微信小程序开发工具模拟手机做调试时,录音文件不能被silk和ffmpeg转,vim打开一看,头部是“data:audio/webm;base64,”。

由此引伸出一个现象:微信小程序的录音不全是silk v3(或mp3)格式,其中还有刚刚提到的webm/base64的,好像还有AMR格式的,听kn007大神说还有混淆格式,也就是那种一个文件含多种格式混合的,也不知道为什么会有这种情况。

关于webm/base64格式,kn007的回复是,base64 decoder然后直接ffmpeg转,于是我分两步实现:
第一步:用java代码做base64 decoder,再将文件写到 xxx.webm文件中,这部分简单,可参考微信小程序 录音文件格式silk 坑那样做即可。
第二步:再调用ffmpeg命令直接转码成wav,主要是调用一下下述转码命令转成16K的WAV:

ffmpeg -i "$1" -f wav -ar 16000 -ac 1 "${1%.*}.$2" > /dev/null 2>&1

其中调用ffmpeg命令容易出现失败,原因之一可能会是文件读写权限不足,原因之二可能会是调用ffmpeg后,需要等ffmpeg进程消失,即转码任务完成,才退出。 觉得我个人碰到的问题应该是原因之二导致的,因为我确实是将/usr/bin/ffmpeg设置成了777权限,还是会转失败,将调ffmpeg命令的部分在脚本中实现,并且加上kn007大神converter.sh中那样的等待ffmpeg完成的部分,就搞定了。

为了让脚本更通用,我将上述解决采样率不匹配的问题,修改后的脚本基础上,又添加了对webm格式的单独ffmpeg转码支持(通过判断传入第1个参数的后辍是否是webm来判断是不是直接ffmpeg转码然后exit,简单粗暴并且高效!)大概在脚本的上方添加下面这一段:

SOURCE_FILE_SUFFIX=${1##*.}
echo -e "XXXX SOURCE_FILE_SUFFIX:${SOURCE_FILE_SUFFIX}"
if [[ "${SOURCE_FILE_SUFFIX}" == "webm" || "${SOURCE_FILE_SUFFIX}" == "mp3" ]]; then
        ## if webm, ffmpeg it directly. webm/base64 had been base64 decode on api.happycxz.com already.
        ## if mp3, ffmpeg it directly. mp3 do not need to decode, can be convert to wav directly.
        echo -e "begin to ffmpeg $2 from webm now..."
        ##ffmpeg -i "$1" -f wav -ar 16000 -ac 1 "${1%.*}.$2" > ffmpeg.cxz.log 2>&1
        ffmpeg -i "$1" -f wav -ar 16000 -ac 1 "${1%.*}.$2" > /dev/null 2>&1
        ##ffmpeg -i "$1" -f wav "${1%.*}.$2" > /dev/null 2>&1
        ffmpeg_pid=$!
        while kill -0 "$ffmpeg_pid"; do sleep 1; done > /dev/null 2>&1
        [ ! -f "${1%.*}.$2" ]&&echo -e "${YELLOW}[Warning]${RESET} Convert $1 false, maybe ffmpeg no format handler for $2."&&exit
        echo -e "${GREEN}[OK]${RESET} Convert $1 To ${1%.*}.$2 Finish."
        exit
else
        echo -e "begin to silk decoder flow..."
        ## if not webm, follows default silk decoder road.
fi

(注:以上脚本片断在2017.11.09更新了mp3部分支持,因为小程序录音新接口已经支持mp3格式录音,录音文件直接经ffmpeg转wav即可)

至此,converter_cxz.sh修改结束。

搭建web服务及主要代码说明

前面相当于评估可行性,基本验证了从小程序录音文件 xx.silk 到语音识别API能认的数据或文件格式,这条路走通了,接下来就是堆JAVA代码实现细节部分了。

创建sprinMVC工程

大概的工程目录结构如下:
这里写图片描述

com.happycxz.controller中有两个controller:
第1个,AdditionalController.java是用来查服务器状态和在线更新数据用的,可忽略。
第2个,OlamiController.java是对接微信小程序silk文件上传API接口的,代码如下:

package com.happycxz.controller;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.NoSuchAlgorithmException;
import java.util.Map;  

import javax.servlet.ServletException;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import javax.servlet.http.Part;

import org.springframework.stereotype.Controller;  
import org.springframework.util.StringUtils;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RequestParam;  
import org.springframework.web.bind.annotation.ResponseBody;

import com.happycxz.olami.AsrAdditionInfo;
import com.happycxz.olami.OlamiEntityFactory;
import com.happycxz.olami.SdkEntity;
import com.happycxz.utils.Configuration;
import com.happycxz.utils.Util;
import com.sun.org.apache.xml.internal.security.utils.Base64;  

/** 
 * olami与微信小程序 接口相关对接
 * @author Jod
 */
@Controller  
@RequestMapping("/olami")  
public class OlamiController {

    //保存linux shell命令字符串
    private static final String SHELL_CMD = Configuration.getInstance().getValue("local.shell.cmd", "sh /YOUR_PATH/silk-v3-decoder/converter_cxz.sh %s wav");

    //保存silk_v3, mp3和wav文件的目录,放在web目录、或一个指定的绝对目录下 
    private static final String localFilePath = Configuration.getInstance().getValue("local.file.path", "/YOUR/LOCAL/VOICE/PATH/");;  

    static {
        Util.p("OlamiController base SHELL_CMD:" + SHELL_CMD);
        Util.p("OlamiController base localFilePath:" + localFilePath);
    }

    @RequestMapping(value="/asr", produces="plain/text; charset=UTF-8")  
    public @ResponseBody String asrUploadFile(HttpServletRequest request, HttpServletResponse response, @RequestParam Map<String, Object> p)  
            throws ServletException, IOException {  
        return processBase(request, p, false);
    }  

    @RequestMapping(value="/mp3asr", produces="plain/text; charset=UTF-8")  
    public @ResponseBody String asrUploadFileMp3(HttpServletRequest request, HttpServletResponse response, @RequestParam Map<String, Object> p)  
            throws ServletException, IOException {
        return processBase(request, p, true);
    }


    public String processBase(HttpServletRequest request, @RequestParam Map<String, Object> p, boolean isMp3)  
            throws ServletException, IOException {  

        AsrAdditionInfo additionInfo = new AsrAdditionInfo(p);
        if (additionInfo.getErrCode() != 0) {
            //参数不合法,或者appKey没有在支持列表中备录
            return Util.JsonResult(String.valueOf(additionInfo.getErrCode()), additionInfo.getErrMsg());  
        }

        String localPathToday = localFilePath + getSrcFmt(isMp3) + File.separator + Util.getDateStr() + File.separator;
        // 如果文件存放路径不存在,则mkdir一个  
        File fileSaveDir = new File(localPathToday);  
        if (!fileSaveDir.exists()) {  
            fileSaveDir.mkdirs();  
        }

        int count = 1;
        String asrResult = "";
        for (Part part : request.getParts()) {  
            String fileName_origin = extractFileName(part);
            //这里必须要用原始文件名是否为空来判断,因为part列表是所有数据,前三个被formdata占了,对应文件名其实是空
            if(!StringUtils.isEmpty(fileName_origin)) {
                Util.p("originFileName[" + count + "]:" + fileName_origin);
                String fileName = additionInfo.getVoiceFileName(isMp3);
                //DEBUG on windows, add temp path preffix to local D: to preserve part.write exception.
                //String recFile = "D:" + localPathToday + fileName;
                String recFile = localPathToday + fileName;
                Util.p("recFileName[" + count + "]:" + recFile);

                part.write(recFile);

                if (webmBase64Decode2Wav(recFile)) {
                    //support webm/base64 in webmBase64Decode2Wav(), wxapp develop IDE record format. 
                    //even if the suffix is xx.silk(wx.startRecord generate) or xx.mp3(wx.getRecorderManager generate)
                    //if webm base64 format , and xxxx.webm file is temporary created, xxxx.wav was last be converted.
                } else {
                    // run script to convert silk_v3 or mp3 to wav
                    Util.RunShell2Wav(SHELL_CMD, recFile);
                }

                // get wave file path and name, prepare for olami asr
                String waveFile = DotMp3OrDotSilk2DotOther(recFile, "wav");
                Util.p("OlamiController.asrUploadFile() waveFile:" + waveFile);

                if (new File(waveFile).exists() == false) {
                    Util.w("OlamiController.asrUploadFile() wav file[" + waveFile + "] not exist!", null);
                    return Util.JsonResult("80", "convert " + getSrcFmt(isMp3) + " to wav failed, NOW NOT SUPPORT WXAPP DEVELOP RECORD because it is not " + getSrcFmt(isMp3) + " format. anyother reason please tell QQ:404499164."); 
                }

                try {
                    SdkEntity entity = OlamiEntityFactory.createEntity(additionInfo.getAppKey(), additionInfo.getAppSecret(), additionInfo.getUserId());
                    asrResult = entity.getSpeechResult(waveFile);
                    Util.p("OlamiController.asrUploadFile() asrResult:" + asrResult);
                } catch (NoSuchAlgorithmException | InterruptedException e) {
                    Util.w("OlamiController.asrUploadFile() asr NoSuchAlgorithmException or InterruptedException", e);
                } catch (FileNotFoundException e) {
                    Util.w("OlamiController.asrUploadFile() asr FileNotFoundException", e);
                    return Util.JsonResult("80", "convert " + getSrcFmt(isMp3) + " to wav failed, NOW NOT SUPPORT WXAPP DEVELOP RECORD because it is not " + getSrcFmt(isMp3) + " format. anyother reason please tell QQ:404499164."); 
                } catch (Exception e) {
                    Util.w("OlamiController.asrUploadFile() asr Exception", e);
                }
            }
            count++;
        }

        //防止数据传递乱码
        //response.setContentType("application/json;charset=UTF-8");

        return Util.JsonResult("0", "olami asr success!", asrResult);  
    }

    private static String getSrcFmt(boolean isMp3) {
        return (isMp3 ? "mp3":"silk_v3");
    }

    /**
     * 将  xxxxx.silk 文件名转 xxxx.wav
     * @param silkName
     * @param otherSubFix
     * @return
     */
    private static String DotMp3OrDotSilk2DotOther(String recName, String otherSubFix) {
        int removeByte = 4;
        if (recName.endsWith("silk")) {
            removeByte = 4;
        } else if (recName.endsWith("slk") || recName.endsWith("mp3")) {
            removeByte = 3;
        }
        return recName.substring(0, recName.length()-removeByte) + otherSubFix;
    }

    /** 
     * 从content-disposition头中获取源文件名 
     *  
     * content-disposition头的格式如下: 
     * form-data; name="dataFile"; filename="PHOTO.JPG" 
     *  
     * @param part 
     * @return 
     */
    private String extractFileName(Part part) {  
        String contentDisp = part.getHeader("content-disposition");  
        String[] items = contentDisp.split(";");  
        for (String s : items) {  
            if (s.trim().startsWith("filename")) {  
                return s.substring(s.indexOf("=") + 2, s.length()-1);  
            }  
        }  
        return "";  
    }


    /**
     * 通过filePath内容判断是否是webm/base64格式,如果是,先decode base64后,再直接ffmpeg转wav,
     * 如果不是,返回false丢给外层继续当作silk v3去解
     * @param filePath
     * @return
     */
    public static boolean webmBase64Decode2Wav(String filePath) {
        boolean isWebm = false;
        try {
            String encoding = "utf-8";
            File file = new File(filePath);
            // 判断文件是否存在
            if ((file.isFile() == false) || (file.exists() == false)) {
                Util.w("webmBase64Decode2Wav() no file[" + filePath + "] exist.", null);
            }

            StringBuilder lineTxt = new StringBuilder();
            String line = null;
            try (
            InputStreamReader read = new InputStreamReader(new FileInputStream(file), encoding);
            BufferedReader bufferedReader = new BufferedReader(read);) {
                while ((line = bufferedReader.readLine()) != null) {
                    lineTxt.append(line);
                }
                read.close();
            } catch (Exception e) {
                Util.w("webmBase64Decode2Wav() exception0:", e);
                return isWebm;
            }

            String oldData = lineTxt.toString();
            if (oldData.startsWith("data:audio/webm;base64,") == false) {
                Util.d("webmBase64Decode2Wav() file[" + filePath + "] is not webm, or already decoded." );
                return isWebm;
            }

            isWebm = true;
            oldData = oldData.replace("data:audio/webm;base64,", "");
            String webmFileName = DotMp3OrDotSilk2DotOther(filePath, "webm");
            try {

                File webmFile = new File(webmFileName);
                byte[] bt = Base64.decode(oldData);
                FileOutputStream in = new FileOutputStream(webmFile);
                try {
                    in.write(bt, 0, bt.length);
                    in.close();
                } catch (IOException e) {
                    Util.w("webmBase64Decode2Wav() exception1:", e);
                    return isWebm;
                }
            } catch (FileNotFoundException e) {
                Util.w("webmBase64Decode2Wav() exception2:", e);
                return isWebm;
            }

            // run cmd to convert webm to wav
            Util.RunShell2Wav(SHELL_CMD, webmFileName);
        } catch (Exception e) {
            Util.w("webmBase64Decode2Wav() exception3:", e);
            return isWebm;
        }

        return isWebm;
    }

    public static void main(String[] args) {
        webmBase64Decode2Wav("D:\\secureCRT_RZSZ\\1505716415538_f7d98081-4d21-3b40-a7df-e56c046a784d_b4118cd178064b45b7c8f1242bcde31f.silk");
    }
} 

利用springMVC的注解,很方便的实现API功能,主要看这个asrUploadFile方法,参数包括request和response之外,还有一个Map结构的p,这个p是用来接收formdata的,即上传录音文件时附带的信息。
我这里强制了必须上传appKey、appSecret以及userId,因为我是直接对接的olami开放平台的接口。

大概的流程是(懒的画流程图了,直接看上面代码,很容易看明白的):
1. 接收p中上传的appKey、appSecret以及userId这三个必选参数
2. 接收request中的Parts,获取原始silk格式文件及对应的上传文件名
这里面其实是包括file和formdata的,这里还掉进一个坑过,想着不需要调用“extractFileName”来拿原始文件名,直接收以请求,随机生成一个文件名保存了得了,事实是,通过“extractFileName”拿文件名,当文件名为”“或null时,这时候是formdata,不是文件,强制保存成文件肯定就出问题了(调试时发现有些录音文件里只有一个很短的数字字母组成的字符串,就是这个原因)。
3. 将文件另取个名字保存到服务器指定目录
为什么要另存文件名:微信小程序上传的录音文件统一是wx-file.silk,不像小程序开发工具上录音那样文件名随机生成。
4. 这里有个额外判断第3步中保存的xxx.silk是不是webm/base64格式的,如果是,就直接base64 decoder后保存文件 xxx.webm,然后调用converter_cxz.sh将webm格式的文件转码成xxx.wav的,走完流程或异常都跳过下一步,直接到第6步。如果不是webm/base64格式的,返回false,继续走下一步。
5. 调用silk_v3_decoder中的脚本(这里是上文提到的修改之后的脚本,我给重命名converter_cxz.sh了)转xxx.wav
6. 通过原来的silk文件全路径,计算出wav文件全路径
7. 通过上一步得到的wav文件全路径,以及appKey、appSecret以及userId这三个参数,生成一个SdkEntity实体,调用getSpeechResult接口获取语音识别和语义处理的结果
8. 组织输出结果返回。

com.happycxz.olami中有四个文件:
第1个,AsrAdditionInfo.java是用来检查https请求中formdata必选的三个参数是否都上传了,是否合法。
这里我额外做了个限制,除了在olami平台上申请的appKey和appSecret之外,appKey还要额外告知我,我在支持列表中加上才可以用,避免被攻击了大家都没法用,没办法,小窝带宽有限。

第2个,OlamiEntityFactory.java是做一个SdkEntity的缓存,如果formdata中上传的userId不一样,这个缓存就没用了:(

第3个,OlamiKeyManager.java是配合第一个文件做appKey限制管理的。

第4个,SdkEntity.java是对接olami接口的部分,主要是从olami java sdk sample代码中拷出来改改的。代码如下:

package com.happycxz.olami;


import java.io.IOException;
import java.security.NoSuchAlgorithmException;

import com.google.gson.Gson;
import com.happycxz.utils.Util;

import ai.olami.cloudService.APIConfiguration;
import ai.olami.cloudService.APIResponse;
import ai.olami.cloudService.CookieSet;
import ai.olami.cloudService.SpeechRecognizer;
import ai.olami.cloudService.SpeechResult;
import ai.olami.nli.NLIResult;
import ai.olami.util.GsonFactory;

public class SdkEntity {

    //indicate simplified input
    private static int localizeOption = APIConfiguration.LOCALIZE_OPTION_SIMPLIFIED_CHINESE;
    // * Replace the audio type you want to analyze with this variable.

    private static int audioType = SpeechRecognizer.AUDIO_TYPE_PCM_WAVE;
    //private static int audioType = SpeechRecognizer.AUDIO_TYPE_PCM_RAW;

    // * Replace FALSE with this variable if your test file is not final audio. 
    private static boolean isTheLastAudio = true;

    private APIConfiguration config = null;

    //configure text recognizer
    SpeechRecognizer recoginzer = null; 
    // * Prepare to send audio by a new task identifier.
    //CookieSet cookie = new CookieSet();

    // json string for print pretty
    private static Gson jsonDump = GsonFactory.getDebugGson(false);
    // normal json string
    private static Gson mGson = GsonFactory.getNormalGson();

    public SdkEntity(String appKey, String appSecret, String userId) {
        Util.d("new SdkEntity() start.  appKey:" + appKey + ", appSecret: " + appSecret + ", userId: " + userId);
        try {
            config = new APIConfiguration(appKey, appSecret, localizeOption);
            recoginzer = new SpeechRecognizer(config);
            recoginzer.setEndUserIdentifier(userId);
            recoginzer.setTimeout(10000);
            recoginzer.setAudioType(audioType);
        } catch (Exception e) {
            Util.w("new SdkEntity() exception", e);
        }
        Util.d("new SdkEntity() done");
    }

    public String getSpeechResult(String inputFilePath) throws NoSuchAlgorithmException, IOException, InterruptedException {
        String lastResult = "";

        Util.d("SdkEntity.getSpeechResult() inputFilePath:" + inputFilePath);

        CookieSet cookie = new CookieSet();

        // * Start sending audio.
        APIResponse response = recoginzer.uploadAudio(cookie, inputFilePath, audioType, isTheLastAudio);
        //
        // You can also send audio data from a buffer (in bytes).
        //
        // For Example :
        // ===================================================================
        // byte[] audioBuffer = Files.readAllBytes(Paths.get(inputFilePath));
        // APIResponse response = recoginzer.uploadAudio(cookie, audioBuffer, audioType, isTheLastAudio);
        // ===================================================================
        //
        Util.d("\nOriginal Response : " + response.toString());
        Util.d("\n---------- dump ----------\n");
        Util.d(jsonDump.toJson(response));
        Util.d("\n--------------------------\n");

        //四种结果,full最完整,seg, nli, asr只包括那一部分
        String full = "", seg = "", nli = "", asr = "";
        // Check request status.
        if (response.ok()) {
            // Now we can try to get recognition result.
            Util.d("\n[Get Speech Result] =====================");
            while (true) {
                Thread.sleep(500);
                // * Get result by the task identifier you used for audio upload.
                Util.d("\nRequest CookieSet[" + cookie.getUniqueID() + "] speech result...");
                response = recoginzer.requestRecognitionWithAll(cookie);
                Util.d("\nOriginal Response : " + response.toString());
                Util.d("\n---------- dump ----------\n");
                Util.d(jsonDump.toJson(response));
                Util.d("\n--------------------------\n");
                // Check request status.
                if (response.ok() && response.hasData()) {
                    full = mGson.toJson(response.getData());
                    // * Check to see if the recognition has been completed.
                    SpeechResult sttResult = response.getData().getSpeechResult();
                    if (sttResult.complete()) {
                        // * Get speech-to-text result
                        Util.p("* STT Result : " + sttResult.getResult());
                        asr = mGson.toJson(sttResult);
                        // * Check to see if the recognition has be
                        // Because we used requestRecognitionWithAll()
                        // So we should be able to get more results.
                        // --- Like the Word Segmentation.
                        if (response.getData().hasWordSegmentation()) {
                            String[] ws = response.getData().getWordSegmentation();
                            for (int i = 0; i < ws.length; i++) {
                                Util.d("* Word[" + i + "] " + ws[i]);
                            }
                            seg = response.getData().getWordSegmentationSingleString();
                        }
                        // --- Or the NLI results.
                        if (response.getData().hasNLIResults()) {
                            NLIResult[] nliResults = response.getData().getNLIResults();
                            nli = mGson.toJson(nliResults);
                        }
                        // * Done.
                        break;
                    } else {
                        // The recognition is still in progress.
                        // But we can still get immediate recognition results.
                        Util.d("* STT Result [Not yet completed] ");
                        Util.d(" --> " + sttResult.getResult());
                    }
                }
            }
        } else {
            // Error
            Util.w("* Error! Code : " + response.getErrorCode(), null);
            Util.w(response.getErrorMessage(), null);
        }

        lastResult = full;

        Util.d("\n===========================================\n");
        return lastResult;
    }

    public static void main(String[] args) throws NoSuchAlgorithmException, IOException, InterruptedException {
        Util.p("SdkEntity.main() start...");
        int argLen = args.length;

        Util.d("SdkEntity.main() args.length[" + argLen + "]:");
        for (String arg : args) {
            Util.d("SpeexPcm.main() arg[" + arg + "]");
        }

        new SdkEntity("b4118cd178064b45b7c8f1242bcde31f", "7908028332a64e47b8336d71ad3ce9ab", "abdd").getSpeechResult(args[0]);
        Util.p("SdkEntity.main() end...");
    }
}

com.happycxz.olami中有两个文件,是使用到的一些util、读配置文件、系统日志等部分。

另外WEB-INFO/lib中加载olami的java sdk,如图:
这里写图片描述

另外,额外附上一张olami-java-client-1.0.1-source.jar中关于默认采用speex压缩的源码部分:
这里写图片描述

怎么用

老接口(录音为silk格式的,通过wx.startRecord录音的)调用:https://api.happycxz.com/wxapp/silk2asr

新接口(录音为mp3格式的,通过wx.getRecorderManager录音的)调用:https://api.happycxz.com/wxapp/mp32asr
(注:如果调用新接口,要求小程序端录音配置成:sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000, format: ‘mp3’)

formdata必选参数(以上两个接口均适用):

参数 是否必选 说明
appKey 从olami.cn上申请的key
appSecret 从olami.cn上申请的secret
userId 用户的唯一标识,比如手机号,或唯一性的ID,或IMEI号之类的

返回数据res.data就是olami开放平台返回结果完全一致,未经修改,具体参考他们在线文档:
olami开放平台的API接口返回数据格式

大概的是 seg字段是语音识别分段结果,asr是语音识别结果,nli是语义或语义处理的结果。小程序的开发工具上没法DEBUG,就没办法截一段例子说明了。

调用案例:“遥知之”智能小秘

小程序码小
欢迎扫码试用。这一版支持语音识别,博客还没来得及更新,稍后我会把相关代码在这个文章“我的微信小程序支持语音识别啦!“遥知之”不再装聋”中分享出来,主要是分享一下微信小程序里如何上传SILK录音部分以及如何解析olami返回的语音识别和语义处理结果的代码。

最后闲话

本文欢迎转载,原文链接:http://www.happycxz.com/m/?p=32

服务端工程的代码分享:
本文所有源码对应码云链接:https://gitee.com/happycxz/silk2asr
本文所有源码对应github链接:https://github.com/happycxz/silk2asr

如果有不明白的都可以在本博客文章后面留言,也欢迎大家指正文中的理解或文字描述错误或不清楚的部分,我将及时更正,避免带人跳坑。

需要用这个接口的,appKey可以在我的个人博客留言或私信告诉我,我加进我的白名单你才可以用。

如果觉得有用,欢迎赞助我

支付宝收款码 微信赞赏码

微信小程序 语音识别开发

博文 来自: ckzhouzhe
没有更多推荐了,返回首页