Pepper SDK入門(28) BaseChatbotで何でも言うことを聞いてくれるPepper

#はじめに
BaseChatbotとは、Chatbotの実装において必ず継承される抽象クラスです。
[Chatbotの回](https://qiita.com/kskrin/items/3f02aa8f55e3e52aac84#qichatbotbasechatbot)でも一瞬だけ触れましたね！
Chatbotでは会話に終始していましたが、Chatbotの親クラスであるBaseChatbotを利用することでPepperの全身を生かしたリアクションも可能になります👏

以下の動画に登場するPepperは、今回オリジナルで作成するサンプルアプリを使用しています🌗🐰
<iframe width="560" height="315" src="https://www.youtube.com/embed/BNAs8y1lEsM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

言葉とモーションでPepperに世界観を与えられるということですね！※映像はイメージです
早速、Pepperをカスタムしていきます💄🍳

#BaseChatbotの手練れになろう🍬
BaseChatbotを継承するクラスを作成し、replyToなど必要なメソッドを実装しましょう。

:loudspeaker:$\style{background-color:orange;}
{注意事項}$:warning:

クラウドの音声認識を利用できるようにしておかなければならないため、インターネットにつなぐ必要があります。

* BaseChatbotで独自のChatbotを使う場合でも、聞き取りできなかった場合(Fallbackなど)に備えて、QiChatbotも同時に設定することをお勧めします。インターネットが繋がっていない場合や、ユーザーが想定外の台詞を言った場合の応答を可能にしておくと良いでしょう🔮


QiChatbotにおける落とし穴については[過去記事](https://qiita.com/kskrin/items/ef9290e0393ebb00316b#%E9%9F%B3%E5%A3%B0%E8%AA%8D%E8%AD%98%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E5%A0%B4%E5%90%88)も参考にして下さい🕳🐇


```java
 Chatbot oddchat = new OddChat(qiContext);
            Future<Void> future = ChatBuilder.with(qiContext)
                    .withChatbot(oddchat)
                    .buildAsync().andThenCompose(new Function<Chat, Future<Void>>() {
                        @Override
                        public Future<Void> execute(Chat value) throws Throwable {
                            return value.async().run();
                        }
                    });
```
OddChatの中身については、記事の後半にコードを載せています。

* ReplyReactionとReplyPriority.FALLBACKで、簡単なフォールバックを実装することが可能です。これについては本記事にも登場しますが次回詳述する予定のため、ここでは馬耳が東風をスルーするように聞き流しておいて大丈夫です！

今回の記事は前回よりも長丁場ですが、蓮實重彦[^1]の一文よりは体感として短いですので、脳味噌をブドウ糖やビタミンB群にディップするなどして読破してみてください🧠
[^1]: 映画評論家。句点がなかなかつかない特徴的な文体は、飴細工の加工過程を見ている感覚になります

#Chatbotインタフェースのこと
Chatbotを独自に開発するには、会話の入力に基づいて[ReplyReaction](https://android.aldebaran.com/sdk/doc/javadoc/qisdk/com.aldebaran.qi.sdk.object.conversation/-reply-reaction/index.html)を作成する$\style{background-color:pink;}{\textrm{replyTo(phrase, locale)}}$が実装されている必要があります。

ChatbotにPepperが聞き取った台詞を通知して返答をコールする$\style{background-color:pink;}{\textrm{acknowledgeHeard(phrase, locale)}}$、ChatbotにPepperが発話した台詞を通知して[ChatbotReaction](https://android.aldebaran.com/sdk/doc/javadoc/qisdk/com.aldebaran.qi.sdk.object.conversation/-chatbot-reaction/index.html)をコールする$\style{background-color:pink;}{\textrm{acknowledgeSaid(phrase, locale)}}$については後ほど紹介しますので、メソッドの存在だけ~~重箱の隅じゃなくて~~頭の片隅に入れておいてください🤙

オリジナルのChatbotを実装するために、BaseChatbotを拡張していきましょう！🛠

#replyToを実装することでPepperの実相(ありのままの姿)に近づく気がする章
Pepperの聞き取った音声が認識された時、Chatから呼ばれるメソッドです。外部のChatbotＡＰＩと連携して応答を作成したりします。
replyToの実装は必ず、StandardReplyReactionを返さなくてはいけません。

ReplyReactionは言葉としての返答も可能ですが、アニメーションのようなアクションも返すことができます🩰

ゼウスのように黄金の雨になったり白鳥になったりすることはできませんが、StandardReplyReactionを受け取るBaseChatbotReactionを何種類か実装することで、入力された言葉によって返答を変えることは可能です🌂🦢

```java

if (heard.equals("今何時")) {
// 「今何時」と言われたら「月の海へ帰る時間です」と返す
    return StandardReplyReaction(
                new MyChatbotReaction(getQiContext(), "月の海へ帰る時間です"),
                ReplyPriority.NORMAL);
} else if (heard.contains("またね")) {
//「またね」と言われたらアニメーションを返す
    return new StandardReplyReaction(
                new MyAnimatedChatbotReaction(getQiContext()),
                ReplyPriority.NORMAL);
} else {
//指定していない言葉については鸚鵡返しにする🦜
    return new StandardReplyReaction(
                new MyDefaultChatbotReaction(getQiContext(), heard),
                ReplyPriority.FALLBACK);
}
```
MyChatbotReaction、MyAnimatedChatbotReaction、MyDefaultChatbotReactionそれぞれの中身についても、記事の後半に載せてありますので参考にしてください👀👀👀👀
ちなみにユーザー側の発話に対して返答しない場合には、replyToの戻り値としてnullを返す必要があります。

##Q.ReplyReactionって何❓
A.返答時、Chatbotが作成します。
これには以下のものが含まれています。

|$\style{background-color:GreenYellow;}{\textrm{ReplyPriority}}$ |$\style{background-color:GreenYellow;}{\textrm{ChatbotReaction}}$  |
|---|---|
|ReplyReactionの優先順位を設定でき、どのReplyReactionを使うかを決定する時に使用されます。御託でも五択でもなくNORMALかFALLBACKの二択です✌️<br><br>複数のChatbotが登録されていて応答が複数得られる場合、Normalの回答が優先されます。 |Chatが返答する時に実行されます。|

###Q.ChatbotReactionって何❓
A.これには以下のインタフェースが含まれています。

|$\style{background-color:GreenYellow;}{\textrm{ runWith(SpeechEngine)}}$ |$\style{background-color:GreenYellow;}{\textrm{stop()}}$  |
|---|---|
|何かしらのリアクションを実行します。リアクションは発話だけでなくPepperの動き、タブレット表示なども作成可能です。<br><br>つまり機械語を喋らせておいてタブレットに人語を表示しておく、バウリンガルならぬペパリンガルごっこもできます🤖🌿|ChatbotReactionを停止させます。Chatのアクションが停止した時、自動的に呼ばれるメソッドです。|

#isAvailableToReplyのことも知るだけ知っておいてあげよう
デフォルトでisAvailableToReplyはtrueになっており、trueに設定しておけば音声認識するたびにreplyToが呼ばれます。isAvailableToReplyを使って、一時的に応答しないようにすることも可能です。
ネットワーク切断や容量不足などでChatbotが使えないような場合には、falseに設定してください。ChatbotのreplyToが呼ばれなくなります。
**ちなみにAPI Level6向けのため、現在(API Level4)のPepperでは使うことができません👼**

#伝書鳩的なメソッド🐦
必要に応じてacknowledgeHeardメソッドとacknowledgeSaidメソッドを実装しましょう。先ほど登場したメソッドですね！
いくつかChatbotが登録されていて、別のChatbotの会話の内容を取得したいときに使用するメソッドで、返答が選択されたことをChatbotに通知します🕊
自身のリアクションが選択されなかった場合でも、Chatbotの会話の状態を更新することができます。
<img width="50px" alt="bird_pouter_hato.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/58f467b4-fb82-b771-29ee-7a6f430c2446.png"><img width="50px" alt="bird_pouter_hato.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/58f467b4-fb82-b771-29ee-7a6f430c2446.png"><img width="50px" alt="bird_pouter_hato.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/58f467b4-fb82-b771-29ee-7a6f430c2446.png"><img width="50px" alt="bird_pouter_hato.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/58f467b4-fb82-b771-29ee-7a6f430c2446.png"><img width="50px" alt="bird_pouter_hato.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/58f467b4-fb82-b771-29ee-7a6f430c2446.png"><img width="50px" alt="bird_pouter_hato.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/58f467b4-fb82-b771-29ee-7a6f430c2446.png"><img width="50px" alt="bird_pouter_hato.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/58f467b4-fb82-b771-29ee-7a6f430c2446.png"><img width="50px" alt="bird_pouter_hato.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574697/58f467b4-fb82-b771-29ee-7a6f430c2446.png">
$\tiny{これは行進するポーター}$

Chatが一つのChatbotしか持たない場合は、これらのメソッドは呼ばれません🔇

```java
@Override
public void acknowledgeHeard(Phrase phrase, Locale locale) {
    Log.i(TAG, "返事はしないけど、一番最近聞いたこと" + phrase.getText());
}

@Override
public void acknowledgeSaid(Phrase phrase, Locale locale) {
    Log.i(TAG, "ほかのChatbotがお返事したよ" + phrase.getText());
}
```

#タイトルを回収する
いざ、Chatbotを作成してみましょう❗️

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

    private QiContext qiContext = null;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        QiSDK.register(this, this);

        findViewById(R.id.btn_start).setOnClickListener(view -> {
           
            Chatbot oddchat = new OddChat(qiContext);
            Future<Void> future = ChatBuilder.with(qiContext)
                    .withChatbot(oddchat)
                    .buildAsync().andThenCompose(new Function<Chat, Future<Void>>() {
                        @Override
                        public Future<Void> execute(Chat value) throws Throwable {
                            return value.async().run();
                        }
                    });

        });

    }
    @Override
    protected void onDestroy() {
        QiSDK.unregister(this,this);
        super.onDestroy();
    }


    @Override
    public void onRobotFocusGained(QiContext qiContext) {
        this.qiContext = qiContext;
    }


    @Override
    public void onRobotFocusLost() {

    }

    @Override
    public void onRobotFocusRefused(String reason) {

    }
    public class OddChat extends BaseChatbot {

        private static final String TAG = "Oddchat";

        public OddChat(QiContext context) {
            super(context);
        }

        public StandardReplyReaction replyTo(@NonNull Phrase phrase, Locale locale) {
            String heard = phrase.getText();

            if (heard.contains("今何時")) {
                return new StandardReplyReaction(
                        new MyChatbotReaction(getQiContext(), "月の海へ帰る時間です"),
                        ReplyPriority.NORMAL);
            } else if (heard.contains("またね")){
                return new StandardReplyReaction(
                        new MyAnimatedChatbotReaction(getQiContext()),
                        ReplyPriority.NORMAL);
            }else {
                return new StandardReplyReaction(
                        new MyDefaultChatbotReaction(getQiContext(), heard),
                        ReplyPriority.FALLBACK);
            }

        }


        class MyChatbotReaction extends BaseChatbotReaction {

            private String answer;
            private Future<Void> fSay;

            MyChatbotReaction(final QiContext context, String answer) {
                super(context);
                this.answer = answer;
            }

            @Override
            public void runWith(SpeechEngine speechEngine) {

                Say say = SayBuilder.with(speechEngine)
                        .withText(answer)
                        .build();
                fSay = say.async().run();

                try {
                    fSay.get();
                } catch (ExecutionException e) {
                    Log.e(TAG, "Error during Say", e);
                } catch (CancellationException e) {
                    Log.i(TAG, "Interruption during Say");
                }
            }

            @Override
            public void stop() {
                if (fSay != null && !fSay.isDone()) {
                    fSay.requestCancellation();
                }
            }
        }

        class MyAnimatedChatbotReaction extends BaseChatbotReaction {

            private QiContext qiContext = null;
            private Future<Void> fAnima;

            public MyAnimatedChatbotReaction(QiContext context) {
                super(context);
                qiContext = context;
            }

            public void runWith(SpeechEngine speechEngine) {

                Animation animation = AnimationBuilder.with(this.qiContext)
                        .withResources(R.raw.bow_a001)
                        .build();

                Animate animate = AnimateBuilder.with(this.qiContext)
                        .withAnimation(animation)
                        .build();

                fAnima = animate.async().run();

                try {
                    fAnima.get();
                } catch (ExecutionException e) {
                    Log.e(TAG, "Error", e);
                } catch (CancellationException e) {
                    Log.i(TAG, "Interruption");
                }
            }

            @Override
            public void stop() {
                if (fAnima != null && !fAnima.isDone()) {
                    fAnima.requestCancellation();
                }
            }
        }

        class MyDefaultChatbotReaction extends BaseChatbotReaction {

            private String answer;
            private Future<Void> fSay;

            MyDefaultChatbotReaction(final QiContext context, String answer) {
                super(context);
                this.answer = answer;
            }

            @Override
            public void runWith(SpeechEngine speechEngine) {

                Say say = SayBuilder.with(speechEngine)
                        .withText(answer)
                        .build();
                fSay = say.async().run();

                try {
                    fSay.get();
                } catch (ExecutionException e) {
                    Log.e(TAG, "Error", e);
                } catch (CancellationException e) {
                    Log.i(TAG, "Interruption");
                }
            }

            @Override
            public void stop() {
                if (fSay != null) {
                    fSay.cancel(true);
                }
            }
        }
    }

}

```

上記のコードにおける

```java
fSay.get();
```
及び

```java
fAnima.get();
```
の部分については次回、BaseChatbotReactionの記事の中でその必要性を説明する予定です！

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

次回はBaseChatbotReactionです。reaction(反作用)の出番、及びニュートンさんのカメオ出演はありません🍎
それではまたお会いしましょう˚✧₊⁎❝᷀ົཽ≀ˍ̮ ❝᷀ົཽ⁎⁺˳✧༚