让你的Hybrid App听懂你的话(Android篇)

3,900 阅读5分钟

前言

最近需要对接语音识别业务,毕竟现在是AI时代,一个产品如果能通过AI能力给用户带来全新的体验,也是很值得尝试的。技术对接上选择的是科大讯飞开放平台,也算是国内最早一批做语音识别的企业了,文档方面都比较全面,对接起来也很方便。

技术基础

开发技术栈为Cordova+Angular+Ionic,这篇分享会介绍如何从头开始创建Cordova插件,并实现科大讯飞Android sdk与App端的数据交互。

Apache Cordova是一个开源的移动开发框架。允许你用标准的web技术——HTML5,CSS3和JavaScript做跨平台开发。 应用在每个平台的具体执行被封装了起来,并依靠符合标准的API绑定去访问每个设备的功能,比如说:传感器、数据、网络状态等。

在继续阅读之前,应该确保你有通过Cordova创建并打包一个简单Hybrid App的经验,感兴趣的童鞋可以到Ionic官网Cordova官网学习下。

创建Cordova插件

全局安装plugman

plugman用于创建Cordova插件,在项目目录下执行cnpm i -g plugman

创建插件

创建一个插件并添加android平台,并生成package.json,插件名xFeiVoice,插件idcom.qinsilk.xFeiVoice,版本号为0.01

plugman create --name xFeiVoice --plugin_id com.qinsilk.xFeiVoice --plugin_version 0.0.1
cd xFeiVoice
plugman createpackagejson ./
plugman platform add --platform_name android

创建成功可以看到对应目录如下

  • src目录存放原生代码,此处为java文件
  • www目录存放js暴露给设备的接口,如下
var exec = require('cordova/exec');

exports.coolMethod = function (arg0, success, error) {
    exec(success, error, 'xFeiVoice', 'coolMethod', [arg0]);
};

此时我们做下修改,让参数名跟业务命名更加相关。

var exec = require('cordova/exec');

exports.record = function (arg0, success, error) {
    exec(success, error, 'xFeiVoice', 'record', [arg0]);
};

安装插件

执行cordova plugin add xFeiVoice,再执行cordova plugin ls可以查看当前App安装的插件。

此时用Android Studio打开项目,可以看到这个插件已经添加成功。
到这里我们的准备工作就完成了。

对接语音识别

导入sdk

Android sdk可以去科大讯飞开放平台下载。将在官网下载的Android SDK 压缩包中libs目录下所有子文件拷贝至Android工程的libs目录下。如下图所示:

添加权限

在工程 AndroidManifest.xml 文件中添加如下权限:

<!--连接网络权限,用于执行云端语音能力 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!--获取手机录音机使用权限,听写、识别、语义理解需要用到此权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!--读取网络信息状态 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--获取当前wifi状态 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--允许程序改变网络连接状态 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<!--读取手机信息权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<!--读取联系人权限,上传联系人需要用到此权限 -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<!--外存储写权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--外存储读权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!--配置权限,用来记录应用配置信息 -->
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<!--手机定位信息,用来为语义等功能提供定位,提供更精准的服务-->
<!--定位信息是敏感信息,可通过Setting.setLocationEnable(false)关闭定位请求 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!--如需使用人脸识别,还要添加:摄相头权限,拍照需要用到 -->
<uses-permission android:name="android.permission.CAMERA" />

初始化语音识别对象

此处调用的是60秒语音听写功能。excute是在开发插件时,用户的自定义方法,当页面调用插件时系统首先将会运行此方法。

@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
    this.callbackContext = callbackContext;
    if (action.equals("record")) {
        // 初始化语音识别对象
        SpeechUtility.createUtility(cordova.getActivity(), "appid=yourAppid,force_login=true");
        // 使用SpeechRecognizer对象,可根据回调消息自定义界面;
        mIat = SpeechRecognizer.createRecognizer(cordova.getActivity(), mInitListener);
        // 设置参数
        setParam();
        // 监听事件
        mIat.startListening(mRecognizerListener);
        return true;
    }
    return false;
}

初始化监听器

private InitListener mInitListener = new InitListener() {
    @Override
    public void onInit(int code) {
        Log.d(LOG_TAG, "SpeechRecognizer init() code = " + code);
        if (code != ErrorCode.SUCCESS) {
            Log.d(LOG_TAG, "初始化失败,错误码:" + code);
        }
    }
};

设置参数

public void setParam() {
    // 清空参数
    mIat.setParameter(SpeechConstant.PARAMS, null);

    // 设置听写引擎
    mIat.setParameter(SpeechConstant.ENGINE_TYPE, mEngineType);
    // 设置返回结果格式
    mIat.setParameter(SpeechConstant.RESULT_TYPE, "json");

    // 设置语音前端点:静音超时时间,即用户多长时间不说话则当做超时处理
    mIat.setParameter(SpeechConstant.VAD_BOS, "4000");

    // 设置语音后端点:后端点静音检测时间,即用户停止说话多长时间内即认为不再输入, 自动停止录音
    mIat.setParameter(SpeechConstant.VAD_EOS, "1000");

    // 设置标点符号,设置为"0"返回结果无标点,设置为"1"返回结果有标点
    mIat.setParameter(SpeechConstant.ASR_PTT, "0");

    // 设置音频保存路径,保存音频格式支持pcm、wav,设置路径为sd卡请注意WRITE_EXTERNAL_STORAGE权限
    // 注:AUDIO_FORMAT参数语记需要更新版本才能生效
    mIat.setParameter(SpeechConstant.AUDIO_FORMAT,"wav");
    mIat.setParameter(SpeechConstant.ASR_AUDIO_PATH, Environment.getExternalStorageDirectory()+"/msc/iat.wav");
}

识别监听器

private RecognizerListener mRecognizerListener = new RecognizerListener() {
    @Override
    public void onVolumeChanged(int volume, byte[] data) {
        // showTip("当前正在说话,音量大小:" + volume);
        Log.d(LOG_TAG, "返回音频数据:"+data.length);
    }

    @Override
    public void onResult(final RecognizerResult result, boolean isLast) {
        //此处有坑,isLast为true时会返回标点符号
        if (null != result && !isLast) {
            String text = parseIatResult(result.getResultString());
            JSONObject obj = new JSONObject();
            try {
                obj.put("searchText", text);
            } catch (JSONException e) {
                Log.d(LOG_TAG, "This should never happen");
            }
            if( null != mIat ){
                // 退出时释放连接
                mIat.cancel();
                mIat.destroy();
            }
            getSearchText(obj);
        } else {
            Log.d(LOG_TAG, "recognizer result : null");
        }
    }

    @Override
    public void onEndOfSpeech() {
        // 此回调表示:检测到了语音的尾端点,已经进入识别过程,不再接受语音输入
        Log.d(LOG_TAG, "结束说话");
    }

    @Override
    public void onBeginOfSpeech() {
        // 此回调表示:sdk内部录音机已经准备好了,用户可以开始语音输入
        Log.d(LOG_TAG, "开始说话");
    }

    @Override
    public void onError(SpeechError error) {
        Log.d(LOG_TAG, "onError Code:"	+ error.getErrorCode());
    }

    @Override
    public void onEvent(int eventType, int arg1, int arg2, Bundle obj) {
        // 以下代码用于获取与云端的会话id,当业务出错时将会话id提供给技术支持人员,可用于查询会话日志,定位出错原因
        // 若使用本地能力,会话id为null
    }

};

处理结果并返回App端

// 处理结果
public static String parseIatResult(String json) {
    StringBuffer ret = new StringBuffer();
    try {
        JSONTokener tokener = new JSONTokener(json);
        JSONObject joResult = new JSONObject(tokener);

        JSONArray words = joResult.getJSONArray("ws");
        for (int i = 0; i < words.length(); i++) {
            // 转写结果词,默认使用第一个结果
            JSONArray items = words.getJSONObject(i).getJSONArray("cw");
            JSONObject obj = items.getJSONObject(0);
            ret.append(obj.getString("w"));
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return ret.toString();
}
//将结果通过callbackContext返回App端
public void getSearchText(JSONObject obj) {
    this.callbackContext.success(obj);
}

App端调用sdk并监听数据返回

//语音识别
$scope.record = function () {
    if (window.cordova && window.cordova.plugins) {
        if(!$scope.recording){
            //调用sdk
            cordova.plugins.xFeiVoice.record({}, function (result) {
                if(result){
                    //返回识别结果
                    $scope.search.goodKey = result.searchText;
                    $scope.openModal();
                    $scope.recording = false;
                }
                console.log('success');
            }, function (result) {
                console.log('fail');
            });
        }else{
            //$scope.recordMedia.stopRecord();
        }
        $scope.recording = !$scope.recording;
    }
};

结语

插件代码我已经上传到github了,有需要的可以clone。走过路过给个star吧~