Unityの音声認識を使いやすくしてみた

より低遅延かつ柔軟な音声認識システムを開発しましたので、ぜひお使いください。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">音声認識エンジンJuliusをUnityで使用するためのライブラリUniJuliusを開発しました！<br>標準の音声認識に加え、魔法詠唱のような長文の発話認識結果を逐次的に取得することができます！<br>↓ダウンロードはこちらから↓<a href="https://t.co/h6guiUvNIG">https://t.co/h6guiUvNIG</a><a href="https://twitter.com/hashtag/Unity?src=hash&amp;ref_src=twsrc%5Etfw">#Unity</a> <a href="https://twitter.com/hashtag/Julius?src=hash&amp;ref_src=twsrc%5Etfw">#Julius</a> <a href="https://t.co/EyFbMIcuWP">pic.twitter.com/EyFbMIcuWP</a></p>&mdash; ZeniZeni (@ZeniYuki0922) <a href="https://twitter.com/ZeniYuki0922/status/1415683121253535750?ref_src=twsrc%5Etfw">July 15, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


こんにちは、自称永遠の中二病のZeniZeniです。
普段から心の中でかっこいい詠唱とかして一人興奮してるわけですが、半年くらい前から詠唱して魔法を使うようなVRコンテンツを作ってます。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">今開発中の音声認識型の魔法戦闘VRゲームです。<br>（オーバーロードの魔法システムかっこいいよね）<br>VRM対応にして自キャラで戦えるようにしたら、どこかで世に出したい。<a href="https://twitter.com/hashtag/VRM?src=hash&amp;ref_src=twsrc%5Etfw">#VRM</a> <a href="https://t.co/jgqmkCBeuw">pic.twitter.com/jgqmkCBeuw</a></p>&mdash; ZeniZeni (@ZeniYuki0922) <a href="https://twitter.com/ZeniYuki0922/status/1046306500816199681?ref_src=twsrc%5Etfw">September 30, 2018</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


これはUnityEngine.Windows.Speechの[PhraseRecognizer](https://docs.unity3d.com/ja/2017.4/ScriptReference/Windows.Speech.PhraseRecognizer.html)を使って音声認識を行っています。(他の音声認識の手段もいろいろと試してみたんですが、レスポンスが遅かったり、そもそも設定がたいへんだったりして自分は断念しました。)
PhraseRecognizerにもKeywordRecognizer(特定の単語のみ)と GrammarRecognizer（発声された単語すべて）の二種類があります。
コンテンツ内で魔法の詠唱に用いるような、速いレスポンスが求められるような場面ではKeyeordRecognizerを用いるのがいいと思います。

この[KeywordRecognizer](https://docs.unity3d.com/ja/2017.4/ScriptReference/Windows.Speech.KeywordRecognizer.html)の使い方はunityのドキュメントを見ていただければいいんですが、非常に簡単です。
下のコードはドキュメントに書いてあるコードを少し加筆して、設定した単語が認識されたかどうかを、別のメソッド等で使用するために、bool値を用いて切り替えています。

#コードの例
```C#:KeywordScript
using System;
using System.Text;
using UnityEngine;
using UnityEngine.Windows.Speech;

public class KeywordScript : MonoBehaviour
{
    [SerializeField]
    private string[] m_Keywords;
    [SerializeField]
    private bool hasRecognized;
    private KeywordRecognizer m_Recognizer;

    void Start()
    {
        m_Recognizer = new KeywordRecognizer(m_Keywords);
        m_Recognizer.OnPhraseRecognized += OnPhraseRecognized;//認識された文字列を渡したいメソッドを追加する。
        m_Recognizer.OnPhraseRecognized += Test;
        m_Recognizer.Start();
    }

    void Update () 
    {
	     if (hasRecognized)
	     {
		    Debug.Log("m_keyword[0] was recognized");
		    hasRecognized = false;
	     }
	}

//認識された単語をコンソールに表示するメソッド
    private void OnPhraseRecognized(PhraseRecognizedEventArgs args)
    {
        StringBuilder builder = new StringBuilder();
        builder.AppendFormat("{0} ({1}){2}", args.text, args.confidence, Environment.NewLine);
        builder.AppendFormat("\tTimestamp: {0}{1}", args.phraseStartTime, Environment.NewLine);
        builder.AppendFormat("\tDuration: {0} seconds{1}", args.phraseDuration.TotalSeconds, Environment.NewLine);
        Debug.Log(builder.ToString());
    }

//認識された単語がm_Keywords[0]だったらhasRecognizedをtrueにする
    private void Test(PhraseRecognizedEventArgs args)
    {
        if(args.text == m_Keywords[0])
        {
           hasRecognized = true;
        }
    }
}
```
簡単ですね！
しかし冷静になってみてから思ったんですがこれかなり面倒くさい。
音声認識を使うときは毎回これを記述する必要が出てきますし、設定する単語数が増えたり、音声認識を使うメソッドが増えたりするともっと長くなります。
僕なんかは、音声認識の精度を疑似的に向上させるために、一つの単語とそれに近い発音の単語もまとめて設定したりしたので（"マジック"と"まじく"みたいな）必要なコードはけっこう長くなりました。

#改良しよう
というわけでこの音声認識をもっと簡潔に実装するために、長くなりそうな部分を一つのクラスとしてまとめてあげて、音声認識をする際には、認識したい文字列を記述して渡すだけになるようにしました。
まとめたクラスが下記のコードです。

```C#:KeywordController
using System;
using System.Text;
using UnityEngine;
using UnityEngine.Windows.Speech;


public class KeywordController : MonoBehaviour{

	public string[][] keywords;//認識したい単語を二次元配列で記録
	public bool[] hasRecognized;
	public KeywordRecognizer[] m_Recognizer;
	public bool ConsoleKeyword;//認識された単語をコンソールで表示するかしないか

	public KeywordController(string[][] keywords,bool ConsoleKeyword)
	{
		this.ConsoleKeyword = ConsoleKeyword;
		this.keywords = keywords;
	}

	public void SetKeywords()
	{
		hasRecognized = new bool[keywords.Length];
		m_Recognizer = new KeywordRecognizer[keywords.Length];
		for (int i = 0; i < keywords.Length; i++)
		{
			SetRecognizer(i);
		}
	}

	private void SetRecognizer(int i)
	{
		m_Recognizer[i] = new KeywordRecognizer(keywords[i]);
		m_Recognizer[i].OnPhraseRecognized += SetKeywordsBool;
		if (ConsoleKeyword)
		{
			m_Recognizer[i].OnPhraseRecognized += OnPhraseRecognized;
		}
	}

	private void SetKeywordsBool(PhraseRecognizedEventArgs args)
	{
		for (int i = 0; i < keywords.Length; i++)
		{
			for (int j = 0; j < keywords[i].Length; j++)
			{
				if (args.text == keywords[i][j])
				{
					hasRecognized[i] = true;
				}
			}
		}
	}

	public void StartRecognizing(int i)
	{
		if (!m_Recognizer[i].IsRunning)
		{
			m_Recognizer[i].Start();
		}
		else
		{
			Debug.Log("KeywordRecognizer" + "[" + i + "]" + " is already started.");
		}

	}

	public void StopRecognizing(int i)
	{
		if (m_Recognizer[i].IsRunning)
		{
			m_Recognizer[i].Stop();
		}
		else
		{
			Debug.Log("KeywordRecognizer" + "[" + i + "]" + " is already stopped.");
		}
	}

	private void OnPhraseRecognized(PhraseRecognizedEventArgs args)
	{
		StringBuilder builder = new StringBuilder();
		builder.AppendFormat("{0} ({1}){2}", args.text, args.confidence, Environment.NewLine);
		builder.AppendFormat("\tTimestamp: {0}{1}", args.phraseStartTime, Environment.NewLine);
		builder.AppendFormat("\tDuration: {0} seconds{1}", args.phraseDuration.TotalSeconds, Environment.NewLine);
		Debug.Log(builder.ToString());
	}
}
```
これをUnityのプロジェクト内に作成してください。
実際に音声認識をする際には下記のようにします。

```C#:KeywordTest
using System;
using System.Text;
using UnityEngine;
using UnityEngine.Windows.Speech;

public class KeywordTest : MonoBehaviour {

	private KeywordController keyCon;
	private string[][] keywords;

	// Use this for initialization
	void Start () {
		keywords = new string[2][];
		keywords[0] = new string[] {"りんご","アップル"};//ひらがなでもカタカナでもいい
		keywords[1] = new string[] {"みかん","オレンジ"};

		keyCon = new KeywordController(keywords,true);//keywordControllerのインスタンスを作成
		keyCon.SetKeywords();//KeywordRecognizerにkeywordsを設定する
		keyCon.StartRecognizing(0);//シーン中で音声認識を始めたいときに呼び出す
		keyCon.StartRecognizing(1);
	}
	
	// Update is called once per frame
	void Update () {
		if (keyCon.hasRecognized[0])//設定したKeywords[0]の単語らが認識されたらtrueになる
		{
			Debug.Log("keyword[0] was recognized");
			keyCon.hasRecognized[0] = false;
		}
		if (keyCon.hasRecognized[1])
		{
			Debug.Log("keyword[1] was recognized");
			keyCon.hasRecognized[1] = false;
		}
	}
}

```
けっこうすっきりしましたね。
シーン内で音声認識を止めたいと思ったらStopRecognizingを呼んでください。まだ検証してないんですが、KeywordRecognizerを多用して起動しっぱなしにすると動作が重くなりそうな気がするので、使わない場面ではこまめにStopした方がいいと思います。

#注意事項
KeywordRecognizer は現在 Windows 10 でのみしか機能しないようです。
また、ネットワーク接続がないと使えないようです。


#開発、執筆にあたり、下記のサイト様を参考、引用させていただきました。
[Unity スクリプトリファレンス](https://docs.unity3d.com/ja/2017.4/ScriptReference/Windows.Speech.KeywordRecognizer.html)