利用OLAMI在unity游戲中加入中文語音控制(一

(歡迎轉(zhuǎn)載症汹。本文源地址:http://blog.csdn.net/speeds3/article/details/76209152)


最近打算嘗試一下OLAMI在游戲中應用的可能性,這里做一下記錄耳奕。

unity官方教程中的幾個項目很精簡岔乔,但看起來很不錯瞻佛,里面有全套的資源梅忌。最后我選擇了tanks-tutorial來做這個實驗。

下載和修改項目

首先按照教程下好項目僧鲁,把坦克移動和射擊的代碼加上虐呻。這時就已經(jīng)可以稱的上是一個“游戲”了,可以控制坦克在地圖上環(huán)游寞秃,也可以開炮斟叼。雖然缺少了挨揍的敵人,但是對設想的用語音控制坦克移動和射擊已經(jīng)足夠了春寿。這里我把地圖擴大了一些朗涩,把坦克的速度降了一些,這樣不至于幾下就開到了地圖的邊緣绑改。

修改速度

準備語義理解服務

接下來就可以開始加入語音功能了谢床。OLAMI官網(wǎng)有c#的示例,示例中分別有cloud-speech-recognition和natural-language-understanding兩個部分厘线,前者字面意思似乎是語音識別识腿,后者看起來是自然語義理解,里面又分為speech-input和text-input兩部分造壮,只是speech-input是空的渡讼。看看readme费薄,原來已經(jīng)包含在cloud-speech-recognition了硝全。由于在這里不關心語音識別栖雾,所以就把他倆當作一樣使用了楞抡,一個對應語音理解,是我們需要的部分析藕,一個對應文字理解召廷,可以用來測試,正好账胧。

把SpeechApiSample.cs和NluApiSample.cs拖入unity里竞慢,稍作修改就可以直接使用。

在移動和射擊腳本中添加語音控制接口

因為打算實現(xiàn)的方案是語音和鍵盤混合輸入治泥,鍵盤輸入能打斷語音控制的輸入筹煮,所以這里要保存一些狀態(tài),記錄是否是通過語音在控制行動或轉(zhuǎn)向居夹,以及語音轉(zhuǎn)向的角度和當前已經(jīng)轉(zhuǎn)過的角度败潦。代碼如下:

TankMovement.cs
  // 語音控制中已經(jīng)轉(zhuǎn)過的角度
  private float turnAmount = 0f;
  // 語音控制中希望轉(zhuǎn)到的角度
  private float turnTarget = 0f;
  // 記錄是否是語音控制移動的狀態(tài)
  private bool voiceMove;
  // 記錄是否是語音轉(zhuǎn)向的狀態(tài)
  private bool voiceTurn;

  private void Update () {
        // Store the value of both input axes.
        float movement = Input.GetAxis (m_MovementAxisName);
        if (movement != 0) {
            voiceMove = false;
            m_MovementInputValue = movement;
        } else if (!voiceMove) {
            m_MovementInputValue = 0f;
        }

        float turn = Input.GetAxis (m_TurnAxisName);
        if (turn != 0) {
            voiceTurn = false;
            m_TurnInputValue = turn;
        } else if (!voiceTurn) {
            m_TurnInputValue = 0f;
        }
        EngineAudio ();
    }

  private void Turn () {
        // Determine the number of degrees to be turned based on the input, speed and time between frames.
        float turn = m_TurnInputValue * m_TurnSpeed * Time.deltaTime;

        if (turnTarget != 0) {
            turnAmount += turn;
            if (turnTarget > 0) {
                if (turnAmount > turnTarget) {
                    m_TurnInputValue = 0f;
                    turnTarget = 0f;
                    turnAmount = 0f;
                    voiceTurn = false;
                }
            } else {
                if (turnAmount < turnTarget) {
                    m_TurnInputValue = 0f;
                    turnTarget = 0f;
                    turnAmount = 0f;
                    voiceTurn = false;
                }
            }
        }

        // Make this into a rotation in the y axis.
        Quaternion turnRotation = Quaternion.Euler (0f, turn, 0f);

        // Apply this rotation to the rigidbody's rotation.
        m_Rigidbody.MoveRotation (m_Rigidbody.rotation * turnRotation);
    }

    public void VoiceMove(float movement) {
        if (movement != 0) {
            voiceMove = true;
            m_MovementInputValue = movement;
        } else {
            voiceMove = false;
            m_MovementInputValue = 0f;
        }
    }

    public void VoiceTurn(float turn) {
        if (turn == 0) {
            voiceTurn = false;
            return;
        }
        turnTarget = turn;
        voiceTurn = true;
        if (turn > 0) {
            m_TurnInputValue = 1.0f;
        } else {
            m_TurnInputValue = -1.0f;
        }

    }

轉(zhuǎn)向和移動稍有些不同本冲,移動時只要模擬按鍵值一直是1就可以,轉(zhuǎn)向就有一個轉(zhuǎn)到多少度的問題劫扒。所以Turn的代碼里加了一些處理檬洞。

TankShootin中就比較簡單,直接添加方法:

public void VoiceFire() {
    m_CurrentLaunchForce = m_MaxLaunchForce / 2;
    Fire ();
}

考慮到語音輸入本身需要時間沟饥,這里沒有加入冷卻的代碼添怔,而且蓄力直接定為滿格的1/2。

為了方便之后在錄音和輸入文本后使用贤旷,將語音控制包裝到TankVoiceControl中广料,并將腳本附加到tank上。

TankVoiceControl.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TankVoiceControl : MonoBehaviour {

    TankMovement move;

    TankShooting shooting;

    // Use this for initialization
    void Start () {
        move = GetComponent<TankMovement> ();
        shooting = GetComponent<TankShooting> ();
    }

    // Update is called once per frame
    void Update () {

    }

    public void VoiceMove(float movement) {
        move.VoiceMove (movement);
    }

    public void VoiceTurn(float turn) {
        move.VoiceTurn (turn);
    }

    public void VoiceFire() {
        shooting.VoiceFire ();
    }

  // 處理OLAMI解析出來的語義
    public void ProcessSemantic(Semantic sem) {
        if (sem.app == "game") {
            string modifier = sem.modifier [0];
            Slot[] slots = sem.slots;
            switch (modifier) {
            case "move":
                {
                    string move = "0f";
                    foreach (Slot slot in slots) {
                        if (slot.name == "movement") {
                            move = slot.value;
                        }
                    }
                    VoiceMove (float.Parse (move));
                }
                break;
            case "stop":
                {
                    VoiceMove (0f);
                }
                break;
            case "leftturn":
                {
                    string turn = "0f";
                    foreach (Slot slot in slots) {
                        if (slot.name == "turn") {
                            turn = slot.value;
                        }
                    }
                    VoiceTurn (0 - float.Parse (turn));
                }
                break;
            case "rightturn":
                {
                    string turn = "0f";
                    foreach (Slot slot in slots) {
                        if (slot.name == "turn") {
                            turn = slot.value;
                        }
                    }
                    VoiceTurn (float.Parse (turn));
                }
                break;
            case "fire":
                {
                    VoiceFire ();
                }
                break;
            }
            return;
        }
    }
}

ProcessSemantic方法用來處理OLAMI接口返回的語義幼驶。

在OLAMI平臺添加語義

其實我的語義是在ProcessSemantic之前就寫好了的性昭,不過先規(guī)劃好語義再去OLAMI添加也沒什么問題。

添加語義

加完之后別忘了發(fā)布县遣,再在應用管理頁面配置上剛加的NLI模塊糜颠。

用文本來測試語義解析

現(xiàn)在可以來測試一下語義能不能起作用了。這里是場景增加一個InputField萧求,on end edit的回調(diào)函數(shù)中調(diào)用NluApiSample的GetRecognitionResult方法的其兴。當然這其中少不了一些封裝。

on end edit的回調(diào)函數(shù)
public void OnSubmitText(string text) {
        string result = VoiceService.GetInstance().sendText (text);
        VoiceResult voiceResult = JsonUtility.FromJson<VoiceResult> (result);
        if (voiceResult.status.Equals ("ok")) {
            Nli[] nlis = voiceResult.data.nli;
            if (nlis.Length != 0) {
                foreach (Nli nli in nlis) {
                    if (nli.type == "game") {
                        foreach (Semantic sem in nli.semantic) {
                            voiceControl.ProcessSemantic (sem);
                            return;
                        }
                    }
                }
            }
        }
    }
VoiceService的sendText方法
public string sendText(string text) {
        return nluApi.GetRecognitionResult ("nli", text);
    }

保存腳本夸政,測試元旬。文本的語義理解速度非常快守问,雖然是通過http請求的方式拿結(jié)果匀归,但在我的機器上測試時感覺不到延時,坦克的轉(zhuǎn)向耗帕、移動都很順暢穆端。

增加錄音功能

unity中提供了一個Microphone類來實現(xiàn)麥克風的功能,可以直接得到AudioClip對象仿便。這里采用按下F1開始錄音体啰,松開結(jié)束錄音的方式。錄音長度暫定為5秒嗽仪。由于olami接口支持的是wav格式的PCM錄音荒勇,所以在github上找到一個WavUtility來做轉(zhuǎn)換。

VoiceController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
using System;
using System.Threading;

public class VoiceController : MonoBehaviour {
    AudioClip audioclip;

    bool recording;

    [SerializeField]
    TankVoiceControl voiceControl;

    // Use this for initialization
    void Start () {
    }

    // Update is called once per frame
    void Update () {
        if (Input.GetKeyDown (KeyCode.F1)) {
            recording = true;
        } else if (Input.GetKeyUp(KeyCode.F1)) {
            recording = false;
        }
    }

    void LateUpdate() {
        if (recording) {
            if (!Microphone.IsRecording (null)) {
        // 開始錄音
                audioclip = Microphone.Start (null, false, 5, 16000);
            }
        } else {
            if (Microphone.IsRecording(null)) {
                Microphone.End (null);
                if (audioclip != null) {
          // WavUtility中有方法必須在主線程中執(zhí)行闻坚,所以只能放在這里轉(zhuǎn)換
                    byte[] audiodata = WavUtility.FromAudioClip (audioclip);
          // 將發(fā)送錄音的過程放到新線程里沽翔,減少主線程卡頓
                    Thread thread = new Thread (new ParameterizedThreadStart(process));
                    thread.Start ((object) audiodata);
                }
            }

        }
    }

    void process(object obj) {
        byte[] audiodata = (byte[]) obj;
        string result = VoiceService.GetInstance ().sendSpeech (audiodata);
        audioclip = null;
        Debug.Log (result);
        VoiceResult voiceResult = JsonUtility.FromJson<VoiceResult> (result);
        if (voiceResult.status.Equals ("ok")) {
            Nli[] nlis = voiceResult.data.nli;
            if (nlis != null && nlis.Length != 0) {
                foreach (Nli nli in nlis) {
                    if (nli.type == "game") {
                        foreach (Semantic sem in nli.semantic) {
                            voiceControl.ProcessSemantic (sem);
                        }
                    }
                }
            }
        }
    }
}

// 下面的幾個class用于解析json數(shù)據(jù)。
[Serializable]
public class VoiceResult {
    public VoiceData data;
    public string status;
}

[Serializable]
public class VoiceData {
    public Nli[] nli;
}

[Serializable]
public class Nli {
    public DescObj desc;
    public Semantic[] semantic;
    public string type;
}

[Serializable]
public class DescObj {
    public string result;
    public int status;
}

[Serializable]
public class Semantic {
    public string app;
    public string input;
    public Slot[] slots;
    public string[] modifier;
    public string customer;
}

[Serializable]
public class Slot {
    public string name;
    public string value;
    public string[] modifier;
}

測試

現(xiàn)在可以啟動游戲窿凤,試試語音的控制了仅偎。在我的機器上西潘,從錄音結(jié)束到坦克開始行動大概要一兩秒的時間。不過說前進哨颂,后退之后不用一直按著按鍵喷市,感覺還是不錯的。還可以說“左轉(zhuǎn)1800度”來看坦克傻傻的轉(zhuǎn)圈威恼。

總結(jié)

總的來說品姓,雖然是在線語義理解,但OLAMI還是可以用在游戲中實時性要求不是特別高的場景箫措,比如自動向前跑動腹备。OLAMI在文本語義理解上的速度表現(xiàn)更是出乎意料的好。如果能提高語音識別的速度斤蔓,例如提供離線包植酥,相信語音控制應用的范圍會更大一些。這個游戲后續(xù)我還會繼續(xù)完善弦牡,敬請期待友驮。

附錄

游戲試玩下載連接:

鏈接:

http://pan.baidu.com/s/1pLDgq9t

密碼: dmxx

源碼下載:

鏈接:

http://pan.baidu.com/s/1qYWcuYC

密碼: gh3n


推薦一些其他的關于OLAMI使用的文章:

根據(jù)OLAMI平臺開發(fā)的日歷Demo

用olami開放語義平臺做匯率換算應用

自然語言處理-實際開發(fā):用語義開放平臺olami寫一個翻譯的應用

自定義java.awt.Canvas—趣味聊天

微信小程序+OLAMI自然語言API接口制作智能查詢工具–快遞、聊天驾锰、日歷等

熱門自然語言理解和語音API開發(fā)平臺對比

使用OLAMI SDK和訊飛語音合成制作一個語音回復的短信小助手

告訴你如何使用OLAMI自然語言理解開放平臺API制作自己的智能對話助手

微信小程序——智能小秘“遙知之”源碼分享(語義理解基于olami)

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末卸留,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子椭豫,更是在濱河造成了極大的恐慌耻瑟,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赏酥,死亡現(xiàn)場離奇詭異喳整,居然都是意外死亡,警方通過查閱死者的電腦和手機裸扶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門框都,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人姓言,你說我怎么就攤上這事瞬项。” “怎么了何荚?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長猪杭。 經(jīng)常有香客問我餐塘,道長,這世上最難降的妖魔是什么皂吮? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任戒傻,我火速辦了婚禮税手,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘需纳。我一直安慰自己芦倒,他們只是感情好,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布不翩。 她就那樣靜靜地躺著兵扬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪口蝠。 梳的紋絲不亂的頭發(fā)上器钟,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機與錄音妙蔗,去河邊找鬼傲霸。 笑死,一個胖子當著我的面吹牛眉反,可吹牛的內(nèi)容都是我干的昙啄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼寸五,長吁一口氣:“原來是場噩夢啊……” “哼跟衅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起播歼,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤伶跷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后秘狞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叭莫,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年烁试,在試婚紗的時候發(fā)現(xiàn)自己被綠了雇初。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡减响,死狀恐怖靖诗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情支示,我是刑警寧澤刊橘,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站颂鸿,受9級特大地震影響促绵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一败晴、第九天 我趴在偏房一處隱蔽的房頂上張望浓冒。 院中可真熱鬧,春花似錦尖坤、人聲如沸稳懒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽场梆。三九已至,卻和暖如春贮缕,著一層夾襖步出監(jiān)牢的瞬間辙谜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工感昼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留装哆,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓定嗓,卻偏偏與公主長得像蜕琴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子宵溅,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容

  • (歡迎轉(zhuǎn)載凌简。本文源地址:http://blog.csdn.net/speeds3/article/details/...
    stdioh閱讀 1,883評論 0 2
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,164評論 25 707
  • 承接前面的《淺談機器學習基礎》、《淺談深度學習基礎》和《淺談自然語言處理基礎》恃逻,主要參考了《解析深度學習:語音識別...
    我偏笑_NSNirvana閱讀 23,519評論 6 67
  • 愛溝通的課程中提到:如果父母跟孩子講雏搂,孩子都不聽,那父母就需要回過頭來檢查一下自己舉止寇损,檢查舉止凸郑,就是去檢查能量,...
    翌的晴空閱讀 84評論 0 1
  • 做企業(yè)的人一開始都雄心勃勃矛市,慢慢的越來越?jīng)]狀態(tài)芙沥,因為,經(jīng)營不是想象的那么容易浊吏! 有夢想的人一開始都激情滿滿而昨,慢慢的...
    慧杰經(jīng)營智慧閱讀 222評論 0 0