1. 程式人生 > >用Kotlin開發android平臺語音識別,語義理解應用(olamisdk)

用Kotlin開發android平臺語音識別,語義理解應用(olamisdk)

本文使用Kotlin開發android平臺的一個語音識別方面的應用,用的是尤拉密開放平臺olamisdk。

1.Kotlin簡介

Kotlin是由JetBrains建立的基於JVM的程式語言,IntelliJ正是JetBrains的傑作,而android Studio是
基於IntelliJ修改而來的。Kotlin是一門包含很多函數語言程式設計思想的面向物件程式語言。

  後來瞭解到Kotlin原來是以一個島的名字命名的(Котлин),它是一門靜態型別程式語言,支援JVM平臺,Android平臺,瀏覽器JS執行環境,本地機器碼等。支援與Java,Android 100% 完全互操作。Kotlin生來就是為了彌補Java缺失的現代語言的特性,並極大的簡化了程式碼,使得開發者可以編寫儘量少的樣板程式碼。

2.Kotlin,java,Swift簡單比較

  • 1.輸出Hello,World!
        JAVA:  System.out.println("Hello,World!"); 
        Kotlin: println("Hello,World!")
        Swift:  print("Hello,World!")
  • 2.變數和常量
        Java:  int  mVariable =10;
                mVariable =20;
                static final int mConstant = 10
; Kotlin:var mVariable = 10 mVariable = 20 val mConstant = 10 Swift:var mVariable = 10 mVariable = 20 let mConstant = 10 感覺Swift和Kotlin比Java簡潔,Kotlin和swift很像。
  • 3.強制型別轉換
      Swift : 
               let
label = "Hello world " let width = 80 let widthLabel = label + String(width) Kotlin : val label = "Hello world " val width = 80 val widthLabel = label + width
  • 4陣列
     Swift :
                var tempList = ["one", "two","three"]
                tempList[1] = "zero"
     Kotlin :
               val tempList = arrayOf("one", "two","three")
               tempList[1] = "zero"
  • 5.函式
  Swift : func greet(_ name: String,_ day: String) -> String { 
                                     return "Hello \(name),today is \(day)." } 
                    greet("Bob", "Tuesday")

            Kotlin :    
                   fun greet(name: String, day: String): String { 
                                      return "Hello $name, today is $day."}
                   greet("Bob", "Tuesday")            
  • 6.類宣告及用法
 Swift : 

       宣告:class Shape {
                    var numberOfSides = 0
                    func simpleDescription() -> String {
                         return "A shape with \(numberOfSides) sides."
                    }
              }
       用法:var shape = Shape()
              shape.numberOfSides = 7
              var shapeDescription = shape.simpleDescription()
    Kotlin : 

        宣告:class Shape {
                    var numberOfSides = 0
                    fun simpleDescription() = "A shape with $numberOfSides sides."
              }
        用法: var shape = Shape()
               shape.numberOfSides = 7
               var shapeDescription = shape.simpleDescription()

可見,Kotlin和Swift好像,現代語言的特徵,比java這樣的高階語言更加簡化,更貼近自然語言。

3.開發環境

本文使用的是android studio2.0版本,啟動androd studio。
如下圖在configure下拉選單中選擇plugins,在搜尋框中搜索Kotlin,找到結果列表中的”Kotlin”外掛。

這裡寫圖片描述

如下圖,找了一張還沒有安裝kotlin外掛的圖

這裡寫圖片描述

點選右側intall,安裝後重啟studio.

4.新建android專案

你可以像以前使用android stuio一樣新建一個andoid專案,建立一個activity。本文用已經完成的一個demo來做示範。

如下圖是一個stuio的demo工程
這裡寫圖片描述

選擇MainActivity和MessageConst兩個java檔案,然後選擇導航欄上的code,在下拉選單中選擇convert java file to kotlin file
這裡寫圖片描述

系統會自動進行轉化,轉化完後會生成對應的MainActivity.kt MessageConst.kt檔案,開啟MainActivity.kt,編譯器上方會提示”Kotlin not configured”,點選一下Configure按鈕,IDE就會自動幫我們配置好了!

將兩個kt檔案複製到src/kotlin目錄下,如下圖

這裡寫圖片描述

轉化後的檔案,也許有些語法錯誤,需要按照kotlin的語法修改。

環境配置好後,來看下gradle更新有哪些區別

project的gradle程式碼如下:

buildscript {
    ext.kotlin_version = '1.1.3-2'
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.0.0'
        //此處多了kotlin外掛依賴
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

再來看看某個module的gradle程式碼:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'//此處多了這條外掛宣告

android {
    compileSdkVersion 14
    buildToolsVersion "24.0.0"

    defaultConfig {
        applicationId "com.olami"
        minSdkVersion 8
        targetSdkVersion 14
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }
    sourceSets {
        main.java.srcDirs += 'src/main/kotlin' //生成的***.kt檔案需要copy到對應的目錄
    }
}

dependencies {
    compile 'com.android.support:support-v4:18.0.0'
    compile files('libs/voicesdk_android.jar')
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"//此處多了kotlin包的依賴
}
repositories {
    mavenCentral()
}

如上所示,如果不是通過轉化的方式新建kotlin工程,則需要自己按照上面的gradle中增加的部分配置好。

5.olami語音識別應用

先貼一張識別後的效果圖:
這裡寫圖片描述

在MainActivity.kt中

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initHandler()//初始化handler用於處理訊息

        initView()//初始化view控制元件,比如點選開始錄音的button

        initViaVoiceRecognizerListener()//初始化語音識別回撥,用於返回錄音狀態和識別結果

        init()//初始化語音識別物件
    }
fun init() 
{
        initHandler()
        //定義olami語音識別物件
        mOlamiVoiceRecognizer = OlamiVoiceRecognizer(this@MainActivity)
        val telephonyManager = this.getSystemService(
                                    Context.TELEPHONY_SERVICE) as TelephonyManager
        val imei = telephonyManager.deviceId

        mOlamiVoiceRecognizer!!.init(imei)
        //set null if you do not want to notify olami server.

        //設定回撥,用於更新錄音狀態和資料等的介面
        mOlamiVoiceRecognizer!!.setListener(mOlamiVoiceRecognizerListener)

        //設定支援的語言型別,預設請設定簡體中文
        mOlamiVoiceRecognizer!!.setLocalization(
                                 OlamiVoiceRecognizer.LANGUAGE_SIMPLIFIED_CHINESE)
        mOlamiVoiceRecognizer!!.setAuthorization("51a4bb56ba954655a4fc834bfdc46af1",   
                                   "asr", "68bff251789b426896e70e888f919a6d", "nli")

        //註冊Appkey,在olami官網註冊應用後生成的appkey
        //註冊api,請直接填寫“asr”,標識語音識別型別
        //註冊secret,在olami官網註冊應用後生成的secret

        mOlamiVoiceRecognizer!!.setVADTailTimeout(2000)
        //錄音時尾音結束時間,建議填//2000ms

        mOlamiVoiceRecognizer!!.setLatitudeAndLongitude(
                                             31.155364678184498, 121.34882432933009)
        //設定經緯度資訊,不願上傳位置資訊,可以填0 
    }

程式碼比較簡單,點選開始錄音button後,啟動錄音,在OlamiVoiceRecognizerListener中回撥處理,然後通過handler傳送訊息用於更新介面。

來看一下初始化view的程式碼,看看跟java方式書寫有哪些不同

private fun initView() 
{
        mBtnStart = findViewById(R.id.btn_start) as Button
        mBtnStop = findViewById(R.id.btn_stop) as Button
        mBtnCancel = findViewById(R.id.btn_cancel) as Button
        mBtnSend = findViewById(R.id.btn_send) as Button
        mInputTextView = findViewById(R.id.tv_inputText) as TextView
        mEditText = findViewById(R.id.et_content) as EditText
        mTextView = findViewById(R.id.tv_result) as TextView
        mTextViewVolume = findViewById(R.id.tv_volume) as TextView

        mBtnStart!!.setOnClickListener {
            if (mOlamiVoiceRecognizer != null)
                mOlamiVoiceRecognizer!!.start()
        }

        mBtnStop!!.setOnClickListener {
            if (mOlamiVoiceRecognizer != null)
                mOlamiVoiceRecognizer!!.stop()
            mBtnStart!!.text = "開始"
            Log.i("led", "MusicActivity mBtnStop onclick 開始")
        }

        mBtnCancel!!.setOnClickListener {
            if (mOlamiVoiceRecognizer != null)
                mOlamiVoiceRecognizer!!.cancel()
        }

        mBtnSend!!.setOnClickListener {
            if (mOlamiVoiceRecognizer != null)
                mOlamiVoiceRecognizer!!.sendText(mEditText!!.text.toString())
            mInputTextView!!.text = "輸入: " + mEditText!!.text
        }


    }

是不是感覺程式碼更簡練了?
下面兩句賦值,效果相同,第二句可以用id之間進行文字賦值,比以前簡練好多。

 mInputTextView!!.text = "輸入: " + mEditText!!.text
 tv_inputText.text = "輸入: " + et_content.text

再來看看handler:

private fun initHandler() {
        mHandler = object : Handler() {
            override fun handleMessage(msg: Message) {
                when (msg.what) {
                    MessageConst.CLIENT_ACTION_START_RECORED -> mBtnStart!!.text 
                                                                = "錄音中"
                    MessageConst.CLIENT_ACTION_STOP_RECORED -> mBtnStart!!.text 
                                                                = "識別中"
                    MessageConst.CLIENT_ACTION_CANCEL_RECORED -> {
                        mBtnStart!!.text = "開始"
                        mTextView!!.text = "已取消"
                    }
                    MessageConst.CLIENT_ACTION_ON_ERROR -> {
                        mTextView!!.text = "錯誤程式碼:" + msg.arg1
                        mBtnStart!!.text = "開始"
                    }
                    MessageConst.CLIENT_ACTION_UPDATA_VOLUME -> mTextViewVolume!!.text
                                                                = "音量: " + msg.arg1
                    MessageConst.SERVER_ACTION_RETURN_RESULT -> {
                        if (msg.obj != null)
                            mTextView!!.text = "伺服器返回: " + msg.obj.toString()
                        mBtnStart!!.text = "開始"
                        try {
                            val message = msg.obj as String
                            var input: String? = null
                            val jsonObject = JSONObject(message)
                            val jArrayNli = 
                                  jsonObject.optJSONObject("data").optJSONArray("nli")
                            val jObj = jArrayNli.optJSONObject(0)
                            var jArraySemantic: JSONArray? = null
                            if (message.contains("semantic")) {
                                jArraySemantic = jObj.getJSONArray("semantic")
                                input = 
                                   jArraySemantic!!.optJSONObject(0).optString("input")
                            } else {
                                input =   jsonObject.optJSONObject("data")
                                              .optJSONObject("asr").optString("result")
                            }
                            if (input != null)
                                mInputTextView!!.text = "輸入: " + input
                        } catch (e: Exception) {
                            e.printStackTrace()
                        }

                    }
                }
            }
        }
    }

原來的switch case的方式,變成了when***,程式碼不僅簡練,更貼近現代語言,更容易理解。

上面的MessageConst.SERVER_ACTION_RETURN_RESULT時,獲取了伺服器返回的結果,緊接著對這段語義進行了簡單的解析

{
    "data": {
        "asr": {
            "result": "我要聽三國演義",
            "speech_status": 0,
            "final": true,
            "status": 0
        },
        "nli": [
            {
                "desc_obj": {
                    "result": "正在努力搜尋中,請稍等",
                    "status": 0
                },
                "semantic": [
                    {
                        "app": "musiccontrol",
                        "input": "我要聽三國演義",
                        "slots": [
                            {
                                "name": "songname",
                                "value": "三國演義"
                            }
                        ],
                        "modifier": [
                            "play"
                        ],
                        "customer": "58df512384ae11f0bb7b487e"
                    }
                ],
                "type": "musiccontrol"
            }
        ]
    },
    "status": "ok"
}

1)解析出nli中type型別是musiccontrol,這是語法返回app的型別,而這個線上聽書的demo只關心musiccontrol這 個app型別,其他的忽略。

2)使用者說的話轉成文字是在asr中的result中獲取
3)在nli中的semantic中,input值是使用者說的話,同asr中的result。
modifier代表返回的行為動作,此處可以看到是play就是要求播放,slots中的資料表示歌曲名稱是三國演義。
那麼動作是play,內容是歌曲名稱是三國演義,在這個demo中呼叫
mBookUtil.searchBookAndPlay(songName,0,0);會先查詢,查詢到結果會再發播放訊息要求播放,我要聽三國演義這個流程就走完了。

6.程式碼下載

7.相關部落格

基於javascript用olamisdk實現web端語音識別語義理解(speex壓縮)