Pepper SDK入門(23) QiChatbotで会話に弾性をつける

#🇶iChatbot？
QiChatbotはTopicを使ったChatbotです。
前回QiChatを利用したい場合に使うクラスとして名前だけ紹介しましたが、今回はその内容について勉強します。
QiChatbotには主に、次のような特徴があります。

* ローカルでの音声認識とクラウドでの音声認識が存在する
* インターネットに接続していない場合でも、ローカルでの音声認識が可能。[^1]
* インターネットに接続している場合、Chatアクションはローカルとクラウドの両方で音声認識を行い、クラウドの音声認識が使えるときの方が認識精度が高い。

[^1]: セキュリティの都合で、インターネットに接続できないようなケースが考えられます。

以下のような場合は、ローカルの音声認識を利用した方が認識精度が上がったり、効率が良かったりする場合があります。

* 聞き取りが100語よりも少ない
* 商品名などの固有名詞が使われている
* ユーザの発話内容が予想できる（例えば「フラクタルねこ」と画面表示されている）
* QiChatの機能(変数やリスナー)で、会話とPepperを連動させる
* 多言語の音声認識に対応させる
* 音声認識以外のユーザ操作で会話を進める

これらを踏まえて、用法~~用量~~を見ていきましょう。

#QiChatbotの使いかた
TopicとQiChatbotをビルドしましょう。

```java
//Topicを作成
Topic topic = TopicBuilder.with(qiContext)
                          .withResource(R.raw.shop)
                          .build();

//QiChatbotを作成
QiChatbot qichatbot = QiChatbotBuilder.with(qiContext)
                                      .withTopic(topic)
                                      .build();
```

Topicの中身はQiChat syntaxを利用してトピックファイルで作成します。統語論は使いません。

```
#ファイル名
topic: main()

concept:(yes) [いいね いいよ うん]
concept:(no) [やだ 却下 いや 反対 むり]
concept:(other) [ランゲルハンス島 ヤコブレフ回路]

#u:はユーザ入力。()内のユーザ入力に対して()外の返答を行う。以下の場合、ユーザが「ひまだね」と言ったら、Pepperは「宇宙っぽい井戸端会議をしましょう」と発話する。
u:(ひまだね) 宇宙っぽい井戸端会議をしましょう
#u1は直前のuの発話後にのみ有効。会話開始直後にユーザが「なにそれ」と言ってもPepperは何も反応しないが、Pepperが「宇宙っぽい井戸端会議をしましょう」言った直後に「なにそれ」と言うと、Pepperは「コンタクティーの職業病です」と返す。
    u1:(~yes) 火星の月はどっちが好きですか？
        u2:(フォボスかな) 公転周期が火星の自転周期より短いとことかかっこいいですよね
        u2:(ダイモスかな) フォボスより外側の軌道にいるとことかエモいですよね
    u1:(なにそれ) コンタクティーの職業病です
        u2:(*) 分かりました

u:([今から出かけようかな どこか行ってみようかな]) ハンギング・ロックはどうですか？
    u1:(~yes) オーバールックホテルを予約しますね^endDiscuss(picnic_1)
    u1:(~no) それでは一緒にカウチポテトしましょう^endDiscuss(picnic_2)
    u1:(_*) $1 ってどっちですか？ ^stayInScope
    u1:("_~other に _[行こう 行きたい]") 交通手段を調べてみます^endDiscuss(picnic_3)
```
ほかにもいろいろ便利機能がありますが、qiChatの文法については別途説明します！

以下の画面は前回と同じく、<ins>[これ](https://github.com/aldebaran/qisdk-tutorials/tree/master/app/src/main/java/com/softbankrobotics/qisdktutorials/ui/tutorials)</ins>の"Mastering Chat locale"で台詞を変えてみたものです。
![スクリーンショット 2021-01-14 13.23.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/da8f9178-e6cd-7cb5-e68a-7ed417d5dce2.png)

#Chat🔚
QiChatbotをキャンセルしてもChatはキャンセルされないため、複数のChatbotを使用している場合、他のChatbotはユーザの発話に返答できます。
会話を終了するときは、Chatを終了させるのを忘れないようにしましょう。

```java
//QiChatbotを作成
final QiChatbot qichatbot = QiChatbotBuilder.with(qiContext)
                                            .withTopic(topic)
                                            .build();

//Chatを作成
final Chat chat = ChatBuilder.with(qiContext)
                             .withChatbot(qichatbot)
                             .build();

//Chatを非同期に実行
final Future<Void> fchat = chat.async().run();

//Qichatbotの終了時にChatを停止
qichatbot.addOnEndedListener(endReason -> {
    Log.i(TAG, "qichatbot end reason = " + endReason);
    fchat.requestCancellation();
});
```

#❎音声認識できなかった場合❌
会話を作成するときには以下のことを考慮してくださいね。

🕳クラウドの音声認識が利用できず、音声認識の精度が悪い場合がある
🕳🕳声が小さい、あるいはノイズや騒音があり、音声認識の精度が悪い場合がある
🕳🕳🕳ユーザによる想定外の発話（唐突な召喚魔術の呪文詠唱など）がなされる場合がある
🕳🕳🕳🕳qiChatの変数を使うときは、変数が初期化されていない可能性がある

こういった落とし穴は、事前に埋め立てておきましょう🥄👷‍♂️

#Pepper—halt!⏸
今までの記事でも何回か触れてきましたが、Pepperは発話中にその内容に応じたジェスチャーを行っています。基本的に微動だにしているのです。
それをそのままにするのも止めるのも、setSpeakingBodyLanguageで変えられます。
後者を選択する場合は、以下のようにBodyLanguageOption.DISABLEDを使ってください。

```java
// QiChatbotによる発話中のボディーランゲージを止めたいとき
qichatbot.setSpeakingBodyLanguage(BodyLanguageOption.DISABLED);
// 聞き取り中のボディーランゲージを止めたいとき
chat.setListeningBodyLanguage(BodyLanguageOption.DISABLED);
```

#Recommendationsの取得
QiChatbot APIを使用すると、Topicファイルから音声入力の候補を取得できます。
QiChatbotを使用するとき、Pepperが応答できる内容をディスプレイに表示するなどして、ユーザを誘導することができます。いざ対面しても何と話しかければいいのか分からない、Pepperガチ恋勢にも優しい仕様になっています。👼💘
maxRecommendationsパラメーターは、取得してくる候補の最大量です。そこに設定した数だけ、ランダムに候補を選出してきてくれます。[^2]
[^2]: scopeRecommendationsの末尾で述べるとおり、Topicファイル内の[]や{}で指定された選択肢の中からは、どのRecommendationsでもそれぞれ一つずつが候補として取得されます。そのため、maxRecommendationsパラメーターを5にしてもRecommendationsの種類やTopicファイルの内容によって、それ以下の数の候補しか表示されない場合があります。
QiChatbotには、

* globalRecommendations
* focusedTopicRecommendations
* scopeRecommendations

の3つの異なるRecommendationsがありますので、以下で詳しく見ていきましょう。

なお、ここでは発話の候補を分かりやすく画面に表示するため、Gitのサンプルアプリではなく新たに作成した簡易アプリを使用しています。コードはブログの末尾にサンプルとして載せておきます。

##globalRecommendations
現在Focusが当たっていないTopicファイルからも候補を取得するものです。
先出のTopicファイル、main以外にglobalという名前のファイルがあり、以下のように書かれていたとします。

```
u:(シーラカンスもちもち) オーナメントにしよう
u:(イルカおいしい) 流線型が奇麗だよね
u:(クジラしゅわしゅわ) さっきメロンソーダの中にいたよ
```
すると「ひまだね」と発話した後、「今から出かけようかな」だけでなく、「シーラカンスもちもち」「イルカおいしい」「クジラしゅわしゅわ」も候補として取得してきます。

簡易アプリで何かを発話する前に貝殻のボタンを押下したところ、以下のように候補を表示してくれました。
![スクリーンショット 2021-01-25 12.06.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/0d9df3f2-5bb3-79a8-4e7a-f4e8e27ae330.png)

##focusedTopicRecommendations
現在のTopicファイルの中から候補を取得します。
先出の例で言えば、「ひまだね」と発話した後なら「今から出かけようかな」「どこか行ってみようかな」などを候補として取得してきます。

簡易アプリで「ひまだね」と発話した後にボタンを押下したところ、以下のように候補を表示してくれました。
![スクリーンショット 2021-01-25 13.19.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/eecd8b53-d6f0-ba00-53f2-69fc48657788.png)

##scopeRecommendations
現在のスコープ内の会話から候補を取得します。
先出の例で言えば、「ひまだね」と発話した後なら「いいね」「いいよ」「うん」「なにそれ」などを候補として取得してきます。確実に現在の会話の流れから候補を取得したい場合は、これを使用してください。

簡易アプリで「ひまだね」と発話した後にボタンを押下したところ、以下のように候補を表示してくれました。
![スクリーンショット 2021-01-25 13.14.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/0141413c-9fa8-11e1-2b49-7e7fb1a9e733.png)

前述のようにfocusedTopicRecommendationsは現在のTopicファイルから、scopeRecommendationsはスコープ内の会話から候補を取得します。そのため、発話前にボタンを押下しても何も表示されません。

また、Topicファイル内のワイルドカード "*"、つまり何を入力してもいいと指定したところについてはどのRecommendationsでも候補はありません。
ただしTopicファイル内の[]や{}で指定された選択肢の中からは、どのRecommendationsでもそれぞれ一つずつが候補として取得されます。(以下の画像はscopeRecommendationsで「どこか行ってみようかな」と発話してからボタンを押下した結果)
![スクリーンショット 2021-01-26 9.52.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/1ee233bc-8993-0d3c-e4c1-fbdff3ad95d2.png)

#あとがき
今回もPepperSDKforAndroidを参考に書かせていただきました。
さらに詳しい情報は[QiChatbotのAPIリファレンス](https://android.aldebaran.com/sdk/doc/javadoc/qisdk/com.aldebaran.qi.sdk.object.conversation/-qi-chatbot/index.html)を参照してみてください。

次回はTopicとTopicStatusのお話です。TOPIX(東証株価指数)の登場はありません。
<img width="40" alt="nyoronyoro-gif.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/de4daefb-61f1-b47b-d4d1-e6116ce8cdc2.gif">以下にサンプルのコードを載せておきます、それでは:dizzy:

```java
public class MainActivity extends AppCompatActivity implements RobotLifecycleCallbacks {

    private QiContext qiContext;
    private Future<Void> future;
    private QiChatbot qiChatbot;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //ボタンを設定
        setContentView(R.layout.activity_main);

        QiSDK.register(this, this);

        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        //発話の候補を取得
                        final String test = qiChatbot.globalRecommendations(5).toString();
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                //TextViewに取得した発話の候補を表示
                                ((TextView)findViewById(R.id.text)).setText(test);
                            }
                        });
                    }
                }).start();
            }
        });
    }

//（中略）
    @Override
    public void onRobotFocusGained(QiContext qiContext) {
        this.qiContext = qiContext;

        //Topicを作成
        Topic mainTopic = TopicBuilder.with(qiContext)// QiContextを利用してビルダーを作成
                                      .withResource(R.raw.main)//Topicファイルを設定
                                      .build();//Topicをビルド
        Topic globalTopic = TopicBuilder.with(qiContext).withResource(R.raw.global).build();
        //Futureを作成
        this.future = QiChatbotBuilder.with(qiContext)// QiContextを利用してビルダーを作成
                                      .withTopic(mainTopic, globalTopic)//Topicファイルを設定
                                      .buildAsync()//Futureをビルド
                //andThenComposeで複数の処理を非同期に連続実行
                .andThenCompose(new Function<QiChatbot, Future<Chat>>() {
                    @Override
                    //作成されたQiChatbotを呼び出し
                    public Future<Chat> execute(QiChatbot qiChatbot) throws Throwable {
                        MainActivity.this.qiChatbot = qiChatbot;
                        return ChatBuilder.with(qiContext).withChatbot(qiChatbot).buildAsync();
                    }
                })
                .andThenCompose(new Function<Chat, Future<Void>>() {
                    @Override
                    //Chatを非同期に実行
                    public Future<Void> execute(Chat chat) throws Throwable {
                        return chat.async().run();
                    }
                });
    }
```