C#
Unity
SDK
Watson

WatsonのUnity-SDKから消えたConfiguration Editorを調査し、今後の方針を考えてみる(2/2)

前回 はConfiguration Editorが何をしていたか、どうして消えたか、などを探ってまとめてみました。

今回は以下を中心に考えていきたいとおもいます。

  • Configuration Editorなしでの資格情報を取り扱いはどう実装したら効果的か
  • SDKの更新で動かなくなったプログラムにはどう対応したら良いか

資格情報の扱いについて

試してみる続・試してみる の投稿では、過去のSDKに依存したサンプルプログラムを書き直して公開しました。

その方法は「資格情報をサンプルプログラムにハードコードする」で、最もお手軽ではあるものの、あまり上品な方法ではありませんでした。ちゃんとしたプロジェクトでやると怒られる手法ですw

もう少し真っ当な方法を考えてみましょう。

インスペクターで値を管理する

Unityという開発ツールにおいて、設定といえばまずインスペクターなんですよね。そこで最初に考えるのは、インスペクターに資格情報を設定するフィールドを表示してあげれば良いのでは?となります。

そこで調べ始めたのですが… 意外なところにこの答えはありました。灯台元暗し、Unity-SDK の Examples にあるサンプル実装 ServiceExamples です。

image.png

さっそくこのシーンファイルをダブルクリックして開いてみますと、いろんなゲームオブジェクトが登録されます。その中には ExampleSpeechToText などもありますね…

image.png

そしてこれを選択してインスペクターを見てみますと!

image.png

ほら見事に資格情報の設定欄が… コード読んだり、ググったり資料探す前に、用意されたExampleコード読めよ!と。ですよね、開発者はそうあるべきでした。未熟すぎ、甘えてましたね自分。

さて、実際のコードの最初のほうを見てみましょう。

ExampleSpeechToText.cs
public class ExampleSpeechToText : MonoBehaviour
{
    #region PLEASE SET THESE VARIABLES IN THE INSPECTOR
    [SerializeField]
    private string _username;
    [SerializeField]
    private string _password;
    [SerializeField]
    private string _url;
    #endregion

    private SpeechToText _speechToText;

これがインスペクターに表示されている部分ですね。private宣言ですが[SerializeField]があるので入力項目として表示されています。

そして _speechToText ですが Start() のなかでこの資格情報を用いて初期化されています。

ExampleSpeechToText.cs
    void Start()
    {
        LogSystem.InstallDefaultReactors();

        //  Create credential and instantiate service
        Credentials credentials = new Credentials(_username, _password, _url);

        _speechToText = new SpeechToText(credentials);
        _customCorpusFilePath = Application.dataPath + "/Watson/Examples/ServiceExamples/TestData/theJabberwocky-utf8.txt";
        _customWordsFilePath = Application.dataPath + "/Watson/Examples/ServiceExamples/TestData/test-stt-words.json";
        _acousticResourceMimeType = Utility.GetMimeType(Path.GetExtension(_acousticResourceUrl));
        _oggResourceMimeType = Utility.GetMimeType(Path.GetExtension(_oggResourceUrl));

        _speechToText.StreamMultipart = true;

        Runnable.Run(Examples());
    }

うん、シンプルで、いかにもUnityっぽいサンプルで良いですね。SDKのExampleなのですから、開発側の推奨する使い方はコレなんだろうという安心感もあります。

インスペクターで実装してみよう

このExampleから、必要最低限の機能を抜き出してクラスを作成してみましょう。名前はベタですが SpeechToTextCredential としました。

SpeechToTextCredential.cs
using IBM.Watson.DeveloperCloud.Utilities;
using System;
using UnityEngine;

public class SpeechToTextCredential : MonoBehaviour {
    #region PLEASE SET THESE VARIABLES IN THE INSPECTOR
    [SerializeField]
    private Credential _credential;
    [Serializable]
    public class Credential
    {
        public string _username;
        public string _password;
        public string _url;
    }
    #endregion

    public Credentials getCredentials()
    {
        return new Credentials(_credential._username, _credential._password, _credential._url);
    }
}

違いですが、やはりパスワードとか丸見えなのはアレなので、クラスにまとめてみました。こうするとインスペクター上ではまとまったデータとして表示されるので分かりやすいですし、入力後は閉じて隠すこともできます。

unity-chan に追加した後はこんな感じ。

image.png

過去のコードとの互換性を考慮してみる

SpeechToText では過去のコードの修正点は以下のふたつでした。

  • 資格情報の簡易ツールが無いのでコードで設定してあげないと駄目
  • 処理関数の順番が変わっており、エラー時に呼ばれる OnFail 関数を追加してあげないと駄目

古い関数の復活

この二つ目の問題を先に解決してしまいましょう。C# にはこういった場合に力強い味方が居ます、それが 拡張メソッド で、既存のクラスを後から拡張できるという、裏技のような記述方法です。

既存の SpeechToText クラスを拡張するので、SpeechToTextExtender というクラスを新規作成します。コードは以下のようになります。

SpeechToTextExtender.cs
using IBM.Watson.DeveloperCloud.Services.SpeechToText.v1;
using IBM.Watson.DeveloperCloud.Services.Assistant.v1;
using IBM.Watson.DeveloperCloud.Connection;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public static class SpeechToTextExtender {

    public static bool Recognize(this SpeechToText stt, AudioClip clip, Action<SpeechRecognitionEvent> successCallback)
    {
        stt.Keywords = stt.Keywords == null ? new string[] { "ibm" } : stt.Keywords;
        stt.KeywordsThreshold = stt.KeywordsThreshold == null ? 0.1f : stt.KeywordsThreshold;

        Debug.LogWarning("Deprecated function: SpeechToText.Recognize(successCallback, clip)");

        return stt.Recognize((r, c) => {
            successCallback(r);
        }, (e, c) => {
            Debug.Log("SpeechToText.OnFail() Error received: " + e.ToString());
        }, clip);
    }
}

C#の拡張メソッドを初めて見た方は、なんじゃこりゃ?って感じでしょう。私もそうです。

このコードでやっていることは、古いサンプルで使われているRecognize関数(引数が少ないバージョン)が削除されてしまったので、あらためて追加してあげる、です。いわゆるラッパー?実際にはエンハンサー。

このコードを作成してプロジェクトに含めることで、元となる SpeechToText クラスに古いRecognize関数が復活します。つまり古いサンプルプログラムを書き直す必要がなくなるのです。

ただしあくまで古い関数ですので、実行時にはログに警告文を表示させています。今後はなるべく使わないでね、と。Javaとかでよくある警告文ですね。

image.png

なお先頭の2行で設定している Keywords と KeywordsThreshold は、何故か設定しないとエラーになるから、です。詳しくは 以前の投稿 をみてください。

資格情報の読み込み

さて残りの資格情報の問題も拡張メソッドで対応しましょう!と考えていましたが、無理でした。拡張メソッドではコンストラクタは追加できません。

かといってSDKのコードを修正しちゃう度胸はないので… 結果として、古いサンプルにある以下の行は、やはり書き換えないとどうしようもありません。

SampleSpeechToText.cs
private SpeechToText m_SpeechToText = new SpeechToText();

ただ変更は最小限としたいため、上記のコードはやはり以下のようにnewを外してやり…

SampleSpeechToText.cs
private SpeechToText m_SpeechToText;

そして Start() 関数の最初に以下の2行を追加することとします。

SampleSpeechToText.cs
IEnumerator Start()
{
    var sstc = (SpeechToTextCredential)FindObjectOfType(typeof(SpeechToTextCredential));
    m_SpeechToText = new SpeechToText(sstc.getCredentials());

古いサンプルコードをそのまま使用できないのは残念ですが、初期化はサービスごとに最初の一回だけですし、場所もだいたいわかっています。この対応で逃げたいとおもいますので、ご了承ください。

実際に古いサンプルコードを利用してみる

さてここで 元のdW記事 にある最初のサンプルコードをもう一度コピペして試してみましょう。

予想通り最初の new のところでエラーになるので上記の資格情報の修正(ここのnewを消しStart関数の中に2行追加)を実施します。

image.png

以下の赤枠の部分が変更箇所です。

image.png

これだけの修正でちゃんと動作しました!

他の2つのサービスへの対応

SpeechToText でうまくいったので、残った2つのAPIへの対応も実施します。

TextToSpeech への対応

TextToSpeech用の資格情報の管理クラスを追加しましょう。SpeechToTextCredential.cs ファイルをコピーしてクラス名を変更しただけなのですが、一応コードを載せておきます。

TextToSpeechCredential.cs
using IBM.Watson.DeveloperCloud.Utilities;
using System;
using UnityEngine;

public class TextToSpeechCredential : MonoBehaviour {
    #region PLEASE SET THESE VARIABLES IN THE INSPECTOR
    [SerializeField]
    private Credential _credential;
    [Serializable]
    public class Credential
    {
        public string _username;
        public string _password;
        public string _url;
    }
    #endregion

    public Credentials getCredentials()
    {
        return new Credentials(_credential._username, _credential._password, _credential._url);
    }
}

こちらも unity-chan に追加して、資格情報を入力しておきましょう。

image.png

次は古い関数を復活させる TextToSpeechExtender.cs ですね。以下のようなコードになります。

TextToSpeechExtender.cs
using IBM.Watson.DeveloperCloud.Services.TextToSpeech.v1;
using System;
using UnityEngine;

public static class TextToSpeechExtender {

    public static bool ToSpeech(this TextToSpeech tts, string text, Action<AudioClip> successCallback)
    {
        Debug.LogWarning("Deprecated function: TextToSpeech.ToSpeech(text, successCallback)");

        return tts.ToSpeech((r, c) => {
            successCallback(r);
        }, (e, c) => {
            Debug.Log("SpeechToText.OnFail() Error received: " + e.ToString());
        }, text);
    }

}

さて、同様に 元のdW記事 にある2番目のサンプルコードをもう一度コピペして試してみましょう。

今回も最初の new のところでエラーになるので同様の修正(ここのnewを消しStart関数の中に2行追加)を実施します。追加する2行は以下になります。

SampleTextToSpeech.cs
    var ttsc = (TextToSpeechCredential)FindObjectOfType(typeof(TextToSpeechCredential));
    m_TextToSpeech = new TextToSpeech(ttsc.getCredentials());

さて実行です。古い関数の警告を出しつつ、古いサンプルプログラムが問題なく動作しました。

image.png

Conversation への対応

Conversation用の資格情報の管理クラスを追加しましょう。これも SpeechToTextCredential.cs ファイルをコピーしてクラス名を変更し、更にワークスペースID用のコードを追加しました。以下にコードを載せておきます。

ConversationCredential.cs
using IBM.Watson.DeveloperCloud.Utilities;
using System;
using UnityEngine;

public class ConversationCredential : MonoBehaviour {
    #region PLEASE SET THESE VARIABLES IN THE INSPECTOR
    [SerializeField]
    private Credential _credential;
    [Serializable]
    public class Credential
    {
        public string _username;
        public string _password;
        public string _url;
        public string _workspace;
    }
    #endregion

    public Credentials getCredentials()
    {
        return new Credentials(_credential._username, _credential._password, _credential._url);
    }
    public string getWorkspaceID()
    {
        return _credential._workspace;
    }
}

unity-chanに追加して必要な情報を入力しておきます。

image.png

次は古い関数を復活させる ConversationExtender.cs ですね。さて、ここが問題です。なんか渡されるデータの形式がまったく異なっていた気がするんですよね…

ちょっと迷ったのですが、以前の関数が返していた MessageResponse 型への変換ロジックも実装してみました。ちょっと面倒でしたが、これなら元原稿の古いサンプルプログラムでもうまく動作します。ただし、ちょっと手抜きをしていまして、今回のお題で使用した属性(intents, intents)だけの対応になっています。

今回の投稿で一番長いコードですね、コレ。

ConversationExtender.cs
using IBM.Watson.DeveloperCloud.Services.Conversation.v1;
using System;
using System.Collections.Generic;
using UnityEngine;

public static class ConversationExtender {

    public static bool Message(this Conversation conv, Action<MessageResponse,string> successCallback, string m_WorkspaceID, string m_Input)
    {
        conv.VersionDate = "2017-05-26";

        Debug.LogWarning("Deprecated function: Conversation.Message(successCallback, m_WorkspaceID, m_Input)");

        return conv.Message((r, c) => {
            if (r is Dictionary<string, object>)
            {
                Dictionary<string, object> dic = (Dictionary<string, object>)r;
                List<RuntimeIntent> ints = new List<RuntimeIntent>();
                foreach (object obj_int in (List<object>)dic["intents"])
                {
                    Dictionary<string, object> dic_int = (Dictionary<string, object>)obj_int;
                    ints.Add(new RuntimeIntent { intent = (string)dic_int["intent"], confidence = (float)(double)dic_int["confidence"] });
                }
                Dictionary<string, object> dic_out = (Dictionary<string, object>)dic["output"];
                string t = "";
                foreach (object obj_out in (List<object>)dic_out["text"])
                {
                    t += obj_out.ToString();
                }
                MessageResponse mr = new MessageResponse
                {
                    intents = ints.ToArray(),
                    output = new OutputData { text = new string[] { t } }
                };
                successCallback(mr, "");
            }
        }, (e, c) => {
            Debug.Log("Conversation.OnFail() Error received: " + e.ToString());
        }, m_WorkspaceID, m_Input);
    }
}

あとエラー対策で VersionDate もここでセットしちゃってます。手抜きなので違う値を使用している方は気を付けてください。(インスペクター化しとくべきだったかな?)

さて、今回も 元のdW記事 にある3番目のサンプルコードをもう一度コピペして試してみましょう。

今回も最初の new のところでエラーになるので同様の修正(ここのnewを消しStart関数の中に2行追加)を実施します。またワークスペースIDもcredentialを参照するよう修正し、結果として追加するのは以下の3行になります。

SampleConversation.cs
    ConversationCredential cc = (ConversationCredential)FindObjectOfType(typeof(ConversationCredential));
    m_WorkspaceID = cc.getWorkspaceID();
    m_Conversation = new Conversation(cc.getCredentials());

そして以下の using もお忘れなく。

SampleConversation.cs
using IBM.Watson.DeveloperCloud.Utilities;

また Intent というクラスは RuntimeIntent になったようなので、お勧めに従ってクラス名を変更してください。

image.png

そして実行すると… やった、動作しました!

image.png

Conversation はラッパー部分のデータ型の変換で苦労しましたが、結果として他の2つのAPIと同様に初期化部分だけ、最少限の修正だけで対応できるようになり、良かったとおもいます。

というわけで

前編 もあわせるとちょっと長くなりましたが、如何だったでしょうか。

基本的にはインスペクターを使った *Credential クラスを使用してもらえば、Watson API を Unity で問題なく使用いただけるとおもいます。

IBM の developerWorks サイトの記事 Watson×Unity!初心者でもできる、VR 空間で Unity ちゃんとおしゃべりアプリ! にあるような古いサンプルコードを最少限の修正で動かしたい場合には、ご紹介した *Extender のラッパークラス(実際には拡張メソッド用クラス)を使ってみてください。

これでこんかいの話題はひとだんらくしたかなぁ、と思います。

まあ、もう少し時間があれば確認しておきたいこととしては…

  • 元記事で4番目以降のサンプルも今回の修正で問題なく利用できるか
  • SDK のお勧めとはいえインスペクターにパスワードを平文で残すのはアレなので暗号化も検討

ぐらいですかね。まあ更には、

  • 使い方は見えたので、そろそろWatsonを利用した自分のコードも書きたい

ともおもっています。

何か面白いものができたら、もしくはネタがたまってきたら、また投稿しますね!ではでは。

2018年5月12日追記 (Unity 2018.1対応)

SampleSpeechToText.cs は Unity 2018.1.0f2 環境で実行したところエラーが発生しましたが、特に使用している様子のない12行目をコメントアウトしたら動作しました。

image.png

SampleTextToSpeech.cs と SampleConversation.cs はこの記事の修正のままで、問題なく動作しました。