kun432's blog

Alexaなどスマートスピーカーの話題中心に、Voiceflowの日本語情報を発信してます。たまにAWSやkubernetesなど。

〜スマートスピーカーやVoiceflowの記事は右メニューのカテゴリからどうぞ。〜

apla-responderモジュールを使って、APLドキュメントを書かずにコードでAPL for Audioを実装する

APL for Audio向けにAdam Rannさん(@adamderann)がこんなnpmパッケージを公開されてました。早い!

APLAドキュメントのディレクティブを使わずに、responseBuilderをラップして直接ハンドラーの中に書き込むような形で使えるようです。まだまだ開発中ということで、ドキュメントに記載されているようなメソッドがなかったりもするのですが、試してみたいと思います。

ドキュメントはこちらです。

www.notion.so

例のごとく、Alexa-hostedで「ハローワールド」のテンプレートをベースにまずはやってみます。

インストール

Alexa-hosetedの場合は、コードエディタを開いて、package.jsonのdependenciesに追加します。ただ、こちらで試したところ、どうもv0.1.0のパッケージが壊れている(TypeScriptからJSにトランスパイルされたファイルが含まれていない)ようなので、手元でビルドしたものを読み込むようにしてみました。多分すぐに治ったものが出てくると思いますが、とりあえず今回はこれで。

あとは、ask-sdk-core/ask-sdk-modelも最新バージョンにしておいたほうがよいでしょう。カンマに注意してください。

...snip...
  "dependencies": {
    "ask-sdk-core": "^2.9.0",
    "ask-sdk-model": "^1.29.0",
    "aws-sdk": "^2.326.0",
    "apla-responder": "https://www.dropbox.com/s/xxxxxxxxxxxx/apla-responder-0.1.0.tgz?dl=1"
  }
...snip...

「保存」「デプロイ」をお忘れなく。

コード

apla-responder'をインポートします。

const Alexa = require('ask-sdk-core');
const { AudioResponse, Components: Apla } = require('apla-responder');

...snip...

LaunchRequestHandlerのところで試してみましょう。handleのところを以下のように変更してみてください。

    handle(handlerInput) {
        const speakOutput = 'ハローワールドにようこそ。こんにちは、というか、ヘルプ、と言ってみてください。';
        const reprompt = ' どちらにしますか?';

        const res = new AudioResponse(handlerInput);
        res.speak(speakOutput + reprompt)
        //res.repromptWith(reprompt)
        return res.getResponse();
    }

別にAPLAって感じには見えないのですが、一番シンプルな使い方の例です。handlerInputを含んだAudioResponseオブジェクトが作成されて、speakメソッドとかrepromptWithメソッドでレスポンスを組み立ててgetResponseで返す感じです。repromptWithはまだ実装されていないようなのでコメントアウトしてます。

こんな感じで出力が返ってきます。

{
    "body": {
        "version": "1.0",
        "response": {
            "directives": [
                {
                    "type": "Alexa.Presentation.APLA.RenderDocument",
                    "token": "amzn1.echo-api.request.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
                    "datasources": {},
                    "document": {
                        "type": "APLA",
                        "version": "0.8",
                        "mainTemplate": {
                            "items": [
                                {
                                    "type": "Sequencer",
                                    "items": [
                                        {
                                            "type": "Speech",
                                            "content": "ハローワールドにようこそ。こんにちは、というか、ヘルプ、と言ってみてください。 どちらにしますか?",
                                            "contentType": "PlainText"
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                }
            ]
        },
        "userAgent": "ask-node/2.9.0 Node/v10.21.0",
        "sessionAttributes": {}
    }
}

はい、レスポンスはきちんとAPLAで返ってきていますね。デフォルトだと"Sequencer"になるので、シーケンシャルに再生されるようです。

もう一つの書き方を試してみましょう。getResponseBuilder()を使うと、responseBuilderと同じようにディレクティブやメソッドを追加したりことができます。以下の例ではrepromptを追加しました。

    handle(handlerInput) {
        const speakOutput = 'ハローワールドにようこそ。こんにちは、というか、ヘルプ、と言ってみてください。';
        const reprompt = ' どちらにしますか?';

        const res = new AudioResponse(handlerInput);
        res.speak(speakOutput + reprompt)
        return res.getResponseBuilder()
                .reprompt(reprompt)
                .getResponse();
    }

レスポンスはこんな感じです。

{
    "body": {
        "version": "1.0",
        "response": {
            "directives": [
                {
                    "type": "Alexa.Presentation.APLA.RenderDocument",
                    "token": "amzn1.echo-api.request.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
                    "datasources": {},
                    "document": {
                        "type": "APLA",
                        "version": "0.8",
                        "mainTemplate": {
                            "items": [
                                {
                                    "type": "Sequencer",
                                    "items": [
                                        {
                                            "type": "Speech",
                                            "content": "ハローワールドにようこそ。こんにちは、というか、ヘルプ、と言ってみてください。 どちらにしますか?",
                                            "contentType": "PlainText"
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                }
            ],
            "reprompt": {
                "outputSpeech": {
                    "type": "SSML",
                    "ssml": "<speak>どちらにしますか?</speak>"
                }
            },
            "shouldEndSession": false,
            "type": "_DEFAULT_RESPONSE"
        },
        "sessionAttributes": {},
        "userAgent": "ask-node/2.9.0 Node/v10.21.0"
    }
}

repromptが返ってきているのがわかりますね。repromptWithメソッドがまだ実装されていないので現時点ではこちらのほうが良さそうです。

で、ここまでの例では、レスポンスはAPLAドキュメントで返ってきてますけど、別に普通ですね。APL for Audioのメリットである、"Mixer"を使った並列再生を試してみましょう。

    handle(handlerInput) {
        const voice_momotaro = 'こんにちは、ぼくの名前は桃太郎。お供と一緒に鬼ヶ島に鬼を退治に行く途中なんだ。応援してね。';
        const voice_dog      = 'soundbank://soundlibrary/animals/amzn_sfx_dog_med_bark_1x_01';
        const voice_monkey   = 'soundbank://soundlibrary/animals/amzn_sfx_monkey_chimp_01';
        const voice_bird     = 'soundbank://soundlibrary/animals/amzn_sfx_bird_chickadee_chirps_01';
        const main_bgm_url   = 'https://www.dropbox.com/s/xxxxxxxxxxxx/momotaro_bgm.mp3?dl=1';

        const ending_sfx_url = 'https://www.dropbox.com/s/xxxxxxxxxxxx/syamisen.mp3?dl=1';
        const ending_sfx     = new Apla.Audio(ending_sfx_url);

        const momotaroMixer   = new Apla.Mixer(
                new Apla.Audio(main_bgm_url),
                new Apla.Speech("<speak><voice name=\"Takumi\">" + voice_momotaro + "</voice></speak>", "SSML"),
                new Apla.Audio(voice_dog),
                new Apla.Audio(voice_monkey)
                new Apla.Audio(voice_bird),
        );

    const res = new AudioResponse(handlerInput);
        
    res.speak("今日の昔話は桃太郎です。");
    res.useMixer(momotaroMixer);
    res.silence(1500);
    res.speak("おしまい。");
    res.playAudio(ending_sfx);
        
    return res.getResponse();
    }

"Mixer"の場合は、並列で発話させるオブジェクトを配列で渡してMixerオブジェクトを作成します。これで配列に入ったオブジェクトが全て並列で発話されます。

        const momotaroMixer   = new Apla.Mixer(
                new Apla.Audio(main_bgm_url),
                new Apla.Speech("<speak><voice name=\"Takumi\">" + voice_momotaro + "</voice></speak>", "SSML"),
                new Apla.Audio(voice_dog),
                new Apla.Audio(voice_monkey)
                new Apla.Audio(voice_bird),
        );

各オブジェクトは以下のように指定します。

Alexaの発話はSpeechオブジェクトにします。第2引数に"SSML"を指定することでSSMLで書けます。speakタグで囲むのを忘れないようにしてください(APLAでも同じです)

const voice_momotaro = 'こんにちは、ぼくの名前は桃太郎。お供と一緒に鬼ヶ島に鬼を退治に行く途中なんだ。応援してね。';
....snip....
new Apla.Speech("<speak><voice name=\"Takumi\">" + voice_momotaro + "</voice></speak>", "SSML"),

オーディオの場合はAudioオブジェクトにして、URLを渡します。

const main_bgm_url   = 'https://www.dropbox.com/s/xxxxxxxxxxxx/momotaro_bgm.mp3?dl=1';
....snip....
new Apla.Speech("<speak><voice name=\"Takumi\">" + voice_momotaro + "</voice></speak>", "SSML"),

でちょっとハマりがちなのが、サウンドライブラリ。あれSSMLで書けるのでSpeechオブジェクトと思いきや、Audioなんですね・・・soundbank://...で始まるURIを記載すればよいです。

const voice_dog      = 'soundbank://soundlibrary/animals/amzn_sfx_dog_med_bark_1x_01';
...snip...
new Apla.Audio(voice_dog),

そして、AudioResponseオブジェクトでレスポンスに組み込みます。

 const res = new AudioResponse(handlerInput);
        
    res.speak("今日の昔話は桃太郎です。");
    res.useMixer(momotaroMixer);
    res.silence(1500);
    res.speak("おしまい。");
    res.playAudio(ending_sfx);
        
    return res.getResponse();

Mixerオブジェクトをレスポンスに組み込む場合はuseMixer()を使います。レスポンスに組み込んだオブジェクトは"Sequencer"で再生されますので、

  • 「今日の昔話は桃太郎です。」と発話される
  • Mixerオブジェクトに含まれている各オブジェクトが「並列」で再生される。
  • silenceメソッドで、1.5秒空白を入れる。
  • 「今日の昔話は桃太郎です。」と発話される
  • playAudioメソッドでAudioオブジェクトが再生される。

というような感じになります。

で、実際に試してみると。。。。

{
    "body": {
        "version": "1.0",
        "response": {
            "directives": [
                {
                    "type": "Alexa.Presentation.APLA.RenderDocument",
                    "token": "amzn1.echo-api.request.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
                    "datasources": {},
                    "document": {
                        "type": "APLA",
                        "version": "0.8",
                        "mainTemplate": {
                            "items": [
                                {
                                    "type": "Sequencer",
                                    "items": [
                                        {
                                            "type": "Speech",
                                            "content": "今日の昔話は桃太郎です。",
                                            "contentType": "PlainText"
                                        },
                                        {
                                            "type": "Mixer",
                                            "items": []             // からっぽ!
                                        },
                                        {
                                            "type": "Silence",
                                            "duration": 1500
                                        },
                                        {
                                            "type": "Speech",
                                            "content": "おしまい。",
                                            "contentType": "PlainText"
                                        },
                                        {
                                            "type": "Audio",
                                            "source": "https://www.dropbox.com/s/XXXXXXXXXXXX/syamisen.mp3?dl=1"
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                }
            ],
            "type": "_DEFAULT_RESPONSE"
        },
        "sessionAttributes": {},
        "userAgent": "ask-node/2.9.0 Node/v10.21.0"
    }
}

"Mixer"がからっぽですね。。。ソースを追っかけるとどうやらこの部分の実装はまだ行われていないようです。ただし、それとは別にSequencerやMixerのitemsとして個別にオブジェクトの追加を行うaddItemというメソッドがありこちらはテストもされているようです。それを使えばいけそうですね。ちょっと冗長ですが書き換えるとこんな感じになります。

    handle(handlerInput) {
        const voice_momotaro = 'こんにちは、ぼくの名前は桃太郎。お供と一緒に鬼ヶ島に鬼を退治に行く途中なんだ。応援してね。';
        const voice_dog      = 'soundbank://soundlibrary/animals/amzn_sfx_dog_med_bark_1x_01';
        const voice_monkey   = 'soundbank://soundlibrary/animals/amzn_sfx_monkey_chimp_01';
        const voice_bird     = 'soundbank://soundlibrary/animals/amzn_sfx_bird_chickadee_chirps_01';
        const main_bgm_url   = 'https://www.dropbox.com/s/XXXXXXXXXXXX/momotaro_bgm.mp3?dl=1';

        const ending_sfx_url = 'https://www.dropbox.com/s/XXXXXXXXXXXX/syamisen.mp3?dl=1';
        const ending_sfx     = new Apla.Audio(ending_sfx_url);

        const animalSequencer = new Apla.Sequencer();
        animalSequencer.addItem(new Apla.Audio(voice_dog));
        animalSequencer.addItem(new Apla.Silence(2000));
        animalSequencer.addItem(new Apla.Audio(voice_monkey));
        animalSequencer.addItem(new Apla.Silence(2000));
        animalSequencer.addItem(new Apla.Audio(voice_bird));

        const momotaroMixer   = new Apla.Mixer();
        
        momotaroMixer.addItem(new Apla.Audio(main_bgm_url));
        momotaroMixer.addItem(new Apla.Speech("<speak><voice name=\"Takumi\">" + voice_momotaro + "</voice></speak>", "SSML"));
        momotaroMixer.addItem(animalSequencer);

        const res = new AudioResponse(handlerInput);
        
    res.speak("今日の昔話は桃太郎です。");
    res.useMixer(momotaroMixer);
    res.silence(1500);
    res.speak("おしまい。");
    res.playAudio(ending_sfx);
        
    return res.getResponse();
    }

さきほどの例では、Mixerオブジェクト作成時に並列で再生させたいオブジェクトを配列に入れていたわけですが、上記の場合は最初にMixerオブジェクトを空で作成して、addItemde 個々に追加する感じです。

        const momotaroMixer   = new Apla.Mixer();
        
        momotaroMixer.addItem(new Apla.Audio(main_bgm_url));
        momotaroMixer.addItem(new Apla.Speech("<speak><voice name=\"Takumi\">" + voice_momotaro + "</voice></speak>", "SSML"));

で、addItemメソッドはSequencerにも変えるので少しひねってみました。

Mixerオブジェクトのitemsに各オブジェクトを追加すると全てが並列で再生されます。ということは再生時間が短いものは最初の方で全部終わっちゃうわけですね。ここだとサウンドライブラリを使っている動物の鳴き声とかがそれになります。再生イメージはこんな感じ。

Alexaの発話  ->->->->->->
BGM         ->->->->->->
犬の声       ->
猿の声       ->
鳥の声       ->

できれば動物の声は少しタイミングをずらして順に再生されたりすると良いですね。そのために、Sequencerオブジェクトを作って、順に入れて、Sequencerオブジェクトで複数を順に再生すればいいわけです。

Alexaの発話  ->->->->->->
BGM         ->->->->->->
犬の声       ->
空白           ->
猿の声           ->
空白               ->
鳥の声               ->

なので、Mixerオブジェクトへの追加と同じように、Sequencerオブジェクトへの追加もaddItemで行えます。

        const animalSequencer = new Apla.Sequencer();

        animalSequencer.addItem(new Apla.Audio(voice_dog));
        animalSequencer.addItem(new Apla.Silence(2000));
        animalSequencer.addItem(new Apla.Audio(voice_monkey));
        animalSequencer.addItem(new Apla.Silence(2000));
        animalSequencer.addItem(new Apla.Audio(voice_bird));

あとはレスポンスを返すだけです。実際に返ってきたレスポンスを見てみましょう。

{
    "body": {
        "version": "1.0",
        "response": {
            "directives": [
                {
                    "type": "Alexa.Presentation.APLA.RenderDocument",
                    "token": "amzn1.echo-api.request.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
                    "datasources": {},
                    "document": {
                        "type": "APLA",
                        "version": "0.8",
                        "mainTemplate": {
                            "items": [
                                {
                                    "type": "Sequencer",
                                    "items": [
                                        {
                                            "type": "Speech",
                                            "content": "今日の昔話は桃太郎です。",
                                            "contentType": "PlainText"
                                        },
                                        {
                                            "type": "Mixer",
                                            "items": [
                                                {
                                                    "type": "Audio",
                                                    "source": "https://www.dropbox.com/s/XXXXXXXXXXXX/momotaro_bgm.mp3?dl=1"
                                                },
                                                {
                                                    "type": "Speech",
                                                    "content": "<speak><voice name=\"Takumi\">こんにちは、ぼくの名前は桃太郎。お供と一緒に鬼ヶ島に鬼を退治に行く途中なんだ。応援してね。</voice></speak>",
                                                    "contentType": "SSML"
                                                },
                                                {
                                                    "type": "Sequencer",
                                                    "items": [
                                                        {
                                                            "type": "Audio",
                                                            "source": "soundbank://soundlibrary/animals/amzn_sfx_dog_med_bark_1x_01"
                                                        },
                                                        {
                                                            "type": "Silence",
                                                            "duration": 2000
                                                        },
                                                        {
                                                            "type": "Audio",
                                                            "source": "soundbank://soundlibrary/animals/amzn_sfx_monkey_chimp_01"
                                                        },
                                                        {
                                                            "type": "Silence",
                                                            "duration": 2000
                                                        },
                                                        {
                                                            "type": "Audio",
                                                            "source": "soundbank://soundlibrary/animals/amzn_sfx_bird_chickadee_chirps_01"
                                                        }
                                                    ]
                                                }
                                            ]
                                        },
                                        {
                                            "type": "Silence",
                                            "duration": 1500
                                        },
                                        {
                                            "type": "Speech",
                                            "content": "おしまい。",
                                            "contentType": "PlainText"
                                        },
                                        {
                                            "type": "Audio",
                                            "source": "https://www.dropbox.com/s/XXXXXXXXXXXX/syamisen.mp3?dl=1"
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                }
            ],
            "type": "_DEFAULT_RESPONSE"
        },
        "sessionAttributes": {},
        "userAgent": "ask-node/2.9.0 Node/v10.21.0"
    }
}

はい、ちゃんと出力できているようですね。実際に再生させてみるとこういう感じです。

まとめ

メソッドチェーンで書けるところとか、なんとなくssml-builder を思い出してしまいましたねー。

動的に出力内容を変えたい場合などはdatasourceでやるよりも直接的に書ける気がするし、APLドキュメントとコードの両方を見ることなくコードだけで完結するというのは良いです。反面、オーサリングツールで試したりもできないし、まだ実装されていない機能(例えばフィルタとか)もあったりするので、実際に使うのはもう少しこなれてからかなと思ってます。あと、あまり詳しくないですが、APLとAPLAを連携させる場合なんかは逆にAPL/APLAドキュメントを書いたほうがいいんじゃないかなと思います、しらんけど。

でも個人的にはとても期待してます。今後が楽しみ。