docomoAPI
RoBoHoN

一緒に おままごと RoBoHoN(外部対話API連携)

モバイル型ロボット電話 RoBoHoN に、docomo雑談対話APIを連携させ、RoBoHoNが うちの子供たちのおままごとの仲間に入れるよう試みました。

omamagoto_rogo+.PNG

環境です

Windows 7 SP1 64bit
Android Studio 2.3.1
RoBoHoN_SDK 1.2.0
RoBoHon端末ビルド番号 02.00.00

ロボホンアプリのアイディア出しについて

先日から、『RoBoHoNの出力を受け止める人間側の五感』を軸に下の検討や実装をしています。

感覚 ロボホン実装 活用機能
視覚_1 RoBoHoNで キモノへのプロジェクションを実装した  プロジェクター
視覚_2 RoBoHoNで 『手旗信号版 嵐が丘』 もどきの実装  モーション
聴覚 RoBoHoNに 英語のスピーキングの宿題を手伝ってもらう  英語対応SDK
触覚 RoBoHoNと一緒におおきなかぶを抜く  姿勢判別API

今回は、RoBoHoNと「聴覚」&「会話、外部API連携」というお題でアプリ実装を考えました。

アプリコンセプト

「会話」は、「人型ロボット」に接触するユーザーにとって、期待値が高いもののひとつです。
(実際の接触経験がすくないユーザーほど特に…)

いろいろと盛り込みたくなりますが、RoBoHonへの「会話」実装は、下記の2つへの考慮が必要です。

RoBoHon特有規定 内容
レギュレーション 『RoBoHonの世界観を維持し、ロボホンの個性を毀損しないために』守るべき特徴とルール 5歳男児設定、一人称「ぼく」、「方言クイズ」など特別な理由がない限り、標準語を話す設定等1
実装方法 HVML(SHARP独自のマークアップ言語2。ロボホンの会話定義に使用)  <speech>で話す内容、<emotion>で発声に込める情感、<behavior>で発声と同時に行う動作を指定するなど

レギュレーションに関しては、公式ドキュメント1は主張が明快で、「ロボットがあるべき」のひとつの具体的な定義例として、初読時感動しました! とても好きな読み物です。

実装方法のHVMLについては、下記例のように、逐次ルールベースに細かく演出をつけていく方式となるので、真剣な実装になるとかなり骨が折れます…。

example.hvml
<topic id="t4" listen="false">
  <action index="1">
    <speech>てつだってくれてありがとう!
      <emotion type="happiness" level="3">おおきなカブがぬけました!</emotion>
    </speech>
    <behavior id="0x06001" type="normal"/>
  </action>
</topic>

今回は、こどもの奔放な(!)会話に対応する実装を素早く実現するために、

  • レギュレーションに対し、「おままごと(RoBoHon主人格ではない「赤ちゃん役」などのロールプレイング)」という特別なアプリ前提
  • 実装方法に対し、「外部の会話APIとの連携」組み込み

で、対応してみます。

会話API

日本語対応の会話APIはいくつか種類があります3が、今回は Docomo雑談対話APIを利用します4

このAPIは会話するキャラクタを以下の3つから選べる5 ので、これらを今回の「おままごと」の選択キャラクタに転用します。

  • 20代後半男性( -> お父さん)
  • 16歳関西弁女子高生( -> お姉さん)
  • 2歳男児赤ちゃん(!) ( -> 赤ちゃん)

shinnenkai_family.png

下記URLの公式APIコンソールでは、実際の応答を実行し、会話のレベル感や入出力項目を確認できます。

[docomo Developer support] 雑談対話APIコンソール
https://dev.smt.docomo.ne.jp/?p=api_console.index&api=dialogue&scope=dialogue

外部の会話APIを組み込む実装

Docomo雑談対話API を利用する準備ができたら(他記事がたくさんあるので記述割愛します)、以下のようにRoBoHonテンプレートへ追加実装していきます。

  • インターネット疎通パーミッション
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
  • 雑談対話API通信クラス
Dialogue.java
public class Dialogue {

    private static final String TAG = Dialogue.class.getSimpleName();
    private final static String apikey = "XXXXXXXXX";  // keyset
    private final static String path =
            "https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY=" + apikey;
    private String reqMsg = "";
    private String resMsg = "ごめんなさい、わかりませんでした";
    private String req =
            "{\"utt\":\"" + this.reqMsg + "\","+
                    "\"mode\":\"dialog\","+ // dialog: 雑談, srtr:しりとり(しりとり時はレスポンスボディのcontextに任意値)
                    "\"context\":\"abc12345\","+ // 任意文字列で脈絡キーの設定(APIリクエストからも返ってくる)。会話脈絡リセット時は適当な文字列に変更
                    "\"t\":\"30\","+  //20 : 関西弁キャラ, 30 : 赤ちゃんキャラ, 指定なし : デフォルトキャラ
                    "\"sex\":\"女\""+
                    "}}";

    private HttpURLConnection requestAPI(URL url) throws Exception {
        HttpURLConnection c = null;
        c = (HttpURLConnection)url.openConnection();
        c.setRequestMethod("POST");
        c.setDoOutput(true);
        c.setDoInput(true);
        c.setRequestProperty("Content-Type", "application/json;");
        c.connect();

        OutputStream os = c.getOutputStream();
        PrintStream ps = new PrintStream(os);
        ps.print(this.req);
        ps.close();
        return c;
    }

    private String getAPIResponseYomi(HttpURLConnection c) throws Exception {
        String yomi = "";
        BufferedReader r =
                new BufferedReader(new InputStreamReader(c.getInputStream(), "UTF-8"));
        yomi =  (new JSONObject(r.readLine())).getString("yomi");
        return yomi;
    }

    public synchronized String haveDialogue(String msg) throws Exception {
        this.reqMsg = msg;
        HttpURLConnection c = null;
        try {
            URL url = new URL(path);
            c = requestAPI(url);
            this.resMsg = getAPIResponseYomi(c);
            c.disconnect();
            return this.resMsg;
        } catch (Exception e) {
            try {
                if (c != null) c.disconnect();
            } catch (Exception e2) {
            }
            Log.v(TAG, "haveDialogue: " + e.toString());
            return this.resMsg;
        }
    }
}
  • RoBoHonシナリオ関連定義類
ScenarioDefinitions.java
/**
* おままごと シーン、accost、通知設定
*/
public static final String SCN_LISTEN = PACKAGE + ".play.listen";
public static final String SCN_TALK = PACKAGE + ".play.talk";
public static final String FUNC_PLAY_LISTEN = "play_listen";
public static final String ACC_PLAY_TALK = ScenarioDefinitions.PACKAGE + ".play.talk";

/**
* data key:ユーザー発話認識文言.
*/
public static final String KEY_LVCSR_BASIC = "lvcsr_basic";
/**
* memory_pを指定するタグ
*/
public static final String MEM_P_RES = ScenarioDefinitions.TAG_MEMORY_PERMANENT + ScenarioDefinitions.PACKAGE + ".res";   
  • ユーザー発話認識結果をJavaへ送出する(リスニング)HVML, javaで処理したAPI応答結果をrobohonが発話する(スピーキング)HVMLを追加
playhouse_listen.hvml
<?xml version="1.0" ?>
<hvml version="2.0">
    <head>
        <producer>com.dev.zdev.playhouse</producer>
        <description>おままごとシナリオ_ユーザー発話送出</description>
        <scene value="com.dev.zdev.playhouse.play.listen" />
        <version value="1.0"/>
        <situation priority="75" topic_id="recog" trigger="user-word">VOICEPF_ERR outof ${Lvcsr:Basic}</situation>
    </head>
    <body>
        <topic id="recog" listen="false">
            <action index="1">
                <control function="play_listen" target="com.dev.zdev.playhouse">
                    <data key="lvcsr_basic" value="${Lvcsr:Basic}"/>
                </control>
            </action>
        </topic>
    </body>
</hvml>

playhouse_speak.hvml
<?xml version="1.0" ?>
<hvml version="2.0">
    <head>
        <producer>com.dev.zdev.playhouse</producer>
        <description>おままごとシナリオ_robohon応答</description>
        <scene value="com.dev.zdev.playhouse.play.talk" />
        <version value="1.0" />
        <accost priority="75" topic_id="say" word="com.dev.zdev.playhouse.play.talk" />
    </head>
    <body>
        <topic id="say" listen="false">
            <action index="1">
                <speech>${memory_p:com.dev.zdev.playhouse.res}</speech>
                <behavior id="assign" type="normal" />
            </action>
        </topic>
    </body>
</hvml>
  • 会話シーンの有効/無効を記述
MainActivity.java
//onResume ファンクション にScenn有効化追記
        VoiceUIManagerUtil.disableScene(mVoiceUIManager, ScenarioDefinitions.SCN_LISTEN);
        VoiceUIManagerUtil.disableScene(mVoiceUIManager, ScenarioDefinitions.SCN_TALK);        

//onPause ファンクション にScene無効化
        VoiceUIManagerUtil.enableScene(mVoiceUIManager, ScenarioDefinitions.SCN_LISTEN);
        VoiceUIManagerUtil.enableScene(mVoiceUIManager, ScenarioDefinitions.SCN_TALK);
  • VoiceUIListenerクラスからのコールバックにAPI応答実装
MainActivity.java
@Override
public void onExecCommand(String command, List<VoiceUIVariable> variables) {
    Log.v(TAG, "onExecCommand() : " + command);
    switch (command) {
        case ScenarioDefinitions.FUNC_PLAY_LISTEN:
            // 1. ユーザー発話取得
            final String lvcsr = VoiceUIVariableUtil.getVariableData(variables, ScenarioDefinitions.KEY_LVCSR_BASIC);
            Log.v(TAG, "onExecCommand: 子供:" + lvcsr);
            Thread thread = new Thread(new Runnable() {public void run() {
                try {
                    // 2. 雑談APIリクエスト、レスポンス取得
                    String res = (new Dialogue()).haveDialogue(lvcsr);
                    Log.v(TAG, "onExecCommand: RoBoHoN:" + res);
                    int ret = VoiceUIVariableUtil.setVariableData(mVoiceUIManager, ScenarioDefinitions.MEM_P_RES, res);
                    VoiceUIManagerUtil.stopSpeech();
                    // 3. RoBoHon 雑談APIレスポンス内容の発話
                    if (mVoiceUIManager != null) {
                        VoiceUIVariableListHelper helper = new VoiceUIVariableListHelper().addAccost(ScenarioDefinitions.ACC_PLAY_TALK);
                        VoiceUIManagerUtil.updateAppInfo(mVoiceUIManager, helper.getVariableList(), true);
                    }
                } catch (Exception e) {
                    Log.v(TAG, "onExecCommand: Exception" +  e.getMessage());
                };
            }});
            thread.start();
            break;
        case ScenarioDefinitions.FUNC_END_APP:
            finish();
            break;
        default:
            break;
    }
}

実行の様子

実行ログ1(赤ちゃんモードとの会話)
https://gist.github.com/isnullnull/9debdcbe8408b6a6992a89e12656dd9b

実行ログ2(関西女子高生おねえさんモードとの会話)
https://gist.github.com/isnullnull/73538ad0b49248cf6c1cdc82249a91ed

実行ログ3(20代後半男性モードとの会話)
https://gist.github.com/isnullnull/82485be81fff51fb40e2b8936594e7ba

感想

雑談対話APIの応答がかなり「雑」談なことを中心に、こどもも楽しむにはなかなか難しい、という結果になりました。
特に、下記2点が重なったときのカオス発生は、傍からみると興味深いですが、こどもたち自身には不愉快だ!と、すぐに飽きられてしまいました。

  • RoBoHonの発話誤認識による入力(子供の声の認識がニガテなので通常時からしばしば発生)
  • docomoAPIが返す脈絡を超えた出力
  • (例: 子供発話「ロボホーン!」→ ((robohon誤認識)ロボファーン)→ (API)返答「どんな音楽の話をしようか」)

自宅のRoBoHonや、Microsoftの 女子高生AI りんなちゃんとの会話も適度に楽しむ上の子供は、ロボット的返答にある程度慣れているので楽しく遊べるかと期待しましたが、許容は難しかったようです。

ただ、外部APIを利用した場合の実機速度や家族の反応が掴めましたので、次のアイディア実装へつなげたいとおもいます!



  1. 0201_SR01MW_Personality_and_Speech_Regulations_V01_00_01 

  2. 0101_SR01MW_Summary_of_SDK_V01_02_00 

  3. BOTで使える会話API・ライブラリ・サービスまとめ 参照など(https://qiita.com/kenzo0202/items/582e3a5e06b64ab24964) 

  4. https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=dialogue&p_name=api_reference 

  5. 手順省力化のため、Docomoの会話APIをラップしたRepl-aiを利用したかったのですが、Repl-aiでは雑談API会話キャラクターの変更ができない仕様のため、Docomo雑談対話APIそのものを利用することにしました