Node.js
AdventCalendar
lambda
AmazonEcho
Alexa

【保存版】開発部長が102時間かけてまとめたAlexaの落とし穴と習得方法

はじめに

@aiji42 さん22日目ありがとうございました!
エイチームライフスタイルアドベントカレンダー2017、23日目は株式会社エイチームライフスタイルの 技術開発部 部長 @tsutorm が担当します。

Alexa に色々な事を喋らせたい!!!

Amazon_Echo_Hero_300_310.jpg

皆さん Amazon Echo ゲットできていますか?

私の周辺では早々に招待メールが来て大喜びな人、中々来なくて落ち込む人と様々ですが、おしなべて スマートスピーカー という新しい技術に盛り上がっている印象です。

ユーザーとしての体験も大変魅力的です。が、それよりも 自分の好きなことを喋らせられる(カスタムスキル)機能を開発できる魅力! 堪りませんね!!

先行するアメリカでは既に 25,000 以上 のスキルが公開されており、ピザの注文からアドベンチャーゲーム、筋トレサポートまで、様々なスキルが展開されています。

対して 日本の公開スキル数は 300 程度。逆に言うと、独創的なスキルを世に放つ余地がまだまだある!ということです。ワクワクしてきたでしょうか!

このワクワクを早く具現化していきたい!

  • けど、何から手をつけたらいいかわからん。
  • 調べてみたけど、公式ドキュメント重すぎ・・・。

という人向けに、簡単にまとめてみました!
年末の楽しい時間のお供にどうぞ!

最初に知りたい

Q. お金ってかかるの?

A. 普通に開発する分には気にしなくてOK

費用発生ポイントの AWS Lambda ですが 1 か月に 1,000,000 件の無料リクエストおよび 400,000 GB-秒のコンピューティング時間 無料利用枠 があります。
例えば、コードサイズが200MB、平均処理時間が60秒のリクエストであれば、1日あたり約1,000リクエスト送ったとしても無料です。
個人開発や小規模な開発では実質無料と考えて良いでしょう。
気軽に試しましょう。

Q. Echo 持ってないけどスキルって作れる?

A. はい。シミュレーター関係がめちゃくちゃ充実してます。

まずは Alexa スキルテストツール-echosim.io で、好きなだけEcho感を体感することをオススメします。ここで公式ドキュメントにもある デザインプロセス
-音声体験を設計する際の検討プロセス
について読むことで、余分な開発を抑制して、より良いユーザー体験を提供する事につながると思います。

Alexa と AWS Lambda 間の開発と検証については、Developerコンソール内に ボイスシミュレーター / サービスシミュレーター / テストシミュレーター(beta) と かなり手厚い環境が用意されています。

私はサービスシミュレーターで開発することが多いですが、こちらは日本語で文字入力すると AWS Lambdaへの送受 json が見えるので非常に使い勝手が良いです。

Q. でも・・・自分にできるかな?

A. 大丈夫、きっと、みんな今はじめたばかりです

みんなから一歩抜け出るチャンスですね!!

ということで、どんどん進めていきましょう。

Echo / Alexa Skills Kit / Custom Skill の 全体像を掴む

Qiitaの過去エントリや、公式ドキュメントをしっかり読んでいけば良いっちゃ良いんだけど、もう少し手っ取り早く全体像がわかるルートが欲しいなぁ。と思う人向けです。

全体像をざっくりいうと、ユーザーの発話を Alexa が ある特定の 実行可能な関数に落とし込み、その実行結果を合成音声でユーザーに伝えるフレームワーク なんですが、
私が一番わかりやすかったのは

Alexaスキル開発トレーニングシリーズ 第2回 対話モデルとAlexa SDK を見る。

これでした。クラスメソッド様。流石でございます。

"トレーニングシリーズ"とあるように、最初から最後まで進めることで、ひと通り動くスキルが作れるようにもなります。

開発者コンソールやスキルビルダはとても分かり易くできていますが、発話(utterance), インテント(intent), スロット(slot) が何かわかってないと、結局ここに戻る必要があります。

また、先行して色々な方がエントリを書いていらっしゃいます。

こちらも見ておくと良いと思います。

実際に Custom Skill を作って動かす

Q. サクッとサンプルを動かしたい! 何がおすすめ?

A. AlexaのHelloWorldに当たる Alexa 豆知識スキルがおすすめです

https://github.com/alexa/skill-sample-nodejs-fact/tree/ja-JP を開いて README.md に書いてある 6つのステップ を進めていくだけで、ランダムに豆知識を答える Alexa スキルが出来上がります。

私はこれを読まずに、なんとなくググりながらスキルビルダーを弄ったり、AWS Lambda上で関数を何度か作り直したりして進めましたが、最短で動くものを体験できるのではないかなーと思います。

キモとなるコード部分は、正味 90行にも満たない簡単なものです。また ja-JP ブランチは公式かつ日本語のサンプルということで、最初に読むコードとしても入りやすい印象です。

lambda/custom/index.js
    'GetNewFactIntent': function () {
        var factArr = data;
        var factIndex = Math.floor(Math.random() * factArr.length);
        var randomFact = factArr[factIndex];
        var speechOutput = GET_FACT_MESSAGE + randomFact;
        this.emit(':tellWithCard', speechOutput, SKILL_NAME, randomFact)
    },

GetNewFactIntent が起動されたら ランダムで豆知識メッセージを振り出し(randomFact)て挨拶(GET_FACT_MESSAGE)をくっつけてるだけです。簡単でしょ。

最後の:tellWithCard ですが、音声応答を speechOutput に渡された文字で行うのと同時に、ユーザーがスマホにインストールした Alexa アプリ上に SKILL_NAME, randomFact をカードとして文字で配信する。と言うものです。

たったこれだけの処理で Alexa がいい感じに合成音声で喋ってくれるわけです。

すばらしいですね!!!

Q. :ask:tell の使い分けって何?

A. Askは応答して会話継続、Tellは応答して会話終了

前述の fact 内でも :ask:tell の2種類が登場しますが、これらはユーザーと対話を続けるかどうかで、使い方が変わります。

ユーザーセッションの扱いについては今回は割愛しますが、会話のテンポを作る上で大事な要素なので覚えておきましょう。

大体つかめてきたら

factスキルで大凡の動きもつかめたら、もうちょっと踏み込んで開発ができる環境を用意して行きましょう。

nodejsの開発環境を整備する

AWS Lambda コードは管理コンソール上で変更もテストもできて便利ですが、
npmyarn と言ったパッケージ管理ツールが使えないため、
アレやコレやの便利な機能を追加しづらいです。

nodejsの開発環境を揃えて zipパッケージでアップロードできる体制を作るのがおすすめです。

最低限以下+手に馴染むエディタを用意しましょう

  • バージョンマネージャー: nvm / nodenv / N
  • パッケージマネージャー: npm / yarn

ゼロから始めるJavaScript生活 / 1 - Node、NPM、Yarn、そして package.json で詳しいです。
私もパッケージマネージャーは yarn をおすすめします。未体験の方は是非この機会に試してみて下さい。

プログラム構造

AWS Lambda や nodejs の話に近くなるのですが、今後プログラムに手を入れていくに当たって、ある程度見通しの良さをどう確保するかと言うのは考えておきたい所です。

よくある話 + 好みの問題もあるので、自分のスタイルがあればそれに合わせて行けば良いと思います。

特に無い場合は、以下は注意を払うようにすると良いでしょう。

  • index.js は薄くする
  • helper/intent は外出してrequireする
  • AWS Lambdaへ upload するための zip化 はめんどくさいので node-lambda でタスク化する

ざっとやり終わるとこんな感じです。

package.json
{
  "name": "skill-sample-nodejs-fact-i18n",
  "version": "1.0.0",
  "description": "I18n version of fact skill.",
  "main": "index.js",
  "scripts": {
    "zip": "node-lambda zip -A ./build",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "alexa",
    "skill",
    "fact"
  ],
  "author": "Amazon.com",
  "license": "Apache-2.0",
  "dependencies": {
    "alexa-sdk": "^1.0.12"
  },
  "devDependencies": {
    "node-lambda": "^0.11.5"
  }
}

$ yarn zipbuildフォルダにいい感じにzipができるはずです

多言語対応

github 上の fact ではなく、 AWS Lambda のテンプレートとなる fact を見ると、 this.t(HOGEHOGE) という表記がいっぱいあると思います。これは、 alexa.resources に リソースデータを定義するだけで this.t('KEY')KEY: で定義されたメッセージを 適切なロケールから判断して取得してくれる仕組みで、国際化/多言語化の機構が備わっているんです。

しかしながら index.js に各国のメッセージリソースがベタで書いてあると、コード全体の見通しが悪くなるのと、あまりきれいな感じがしません。

ローカライゼーション用フォルダ+ローダーを作ってスッキリさせてしまうことをおすすめします。

index.js
//=========================================================================================================================================
//この行から下のコードに変更を加えると、スキルが動作しなくなるかもしれません。わかる人のみ変更を加えてください。
//=========================================================================================================================================
exports.handler = function(event, context, callback) {
    var alexa = Alexa.handler(event, context);
    alexa.APP_ID = APP_ID;
    alexa.registerHandlers(handlers);
    alexa.resources = require('./resources.js');
    alexa.execute();
};
resources.js
'use strict';

const fs = require('fs');
const path = require('path');

const defaultLang = 'ja-JP';
const translations = {};

fs.readdirSync('resources').forEach((file) => {
  const lang = file.replace('.js', '');
  translations[lang] = require(`./${path.join('resources', file)}`);
});

module.exports = translations;
resources/ja-JP.js
module.exports = {
  translation: {
    FACTS: [
      "水星の一年はたった88日です。",
      "金星は水星と比べて太陽より遠くにありますが、気温は水星よりも高いです。",
      "金星は反時計回りに自転しています。過去に起こった隕石の衝突が原因と言われています。",
      "火星上から見ると、太陽の大きさは地球から見た場合の約半分に見えます。",
      "木星の<sub alias='いちにち'>1日</sub>は全惑星の中で一番短いです。",
      "天の川銀河は約50億年後にアンドロメダ星雲と衝突します。",
      "太陽の質量は全太陽系の質量の99.86%を占めます。",
      "太陽はほぼ完璧な円形です。",
      "皆既日食は一年から二年に一度しか発生しない珍しい出来事です。",
      "土星は自身が太陽から受けるエネルギーの2.5倍のエネルギーを宇宙に放出しています。",
      "太陽の内部温度は摂氏1500万度にも達します。",
      "月は毎年3.8cm地球から離れていっています。"
    ],
    SKILL_NAME: "豆知識",
    GET_FACT_MESSAGE: "知ってましたか?",
    HELP_MESSAGE: "豆知識を聞きたい時は「豆知識」と、終わりたい時は「おしまい」と言ってください。どうしますか?",
    HELP_REPROMPT: "どうしますか?",
    STOP_MESSAGE: "さようなら",
  },
};

これで、AWS Lambdaの処理全体像もスッキリ見通しがよくなりますし、
多言語化を進めたい場合は、resources/en-US.js のように、各ロケールコード毎の言語ファイルを追加するだけでOKです。

テストフレームワーク

開発がノッて来くると、問題になるのがテストです。

前述のAWS Lambda コンソール + サービスシミュレーターに頼ったテストを続けようとすると

  1. コードを書く
  2. コードを zip 化して AWS Lambda に upload する
  3. テストイベントで検証する (駄目なら 1へ
  4. サービスシミュレーターでテストする (駄目なら 1へ

というサイクルになるのですが、いかんせん動作確認迄に時間が掛かり過ぎてパフォーマンスが出ません。

公式ではないのですが、Alexa Skill Test Framework という mocha ベースでのテストを Alexa 向けに 書けるものがあるので、これでテストコードを書いてしまいましょう。

  • 実はここで前述のメッセージリソース外出しが効いてきます。よかったですね!

npm add alexa-skill-test-frameworkyarn add alexa-skill-test-framework で一通り入りますが、実行には mocha が必要なので入れてしまいます。

package.json
{
  "name": "skill-sample-nodejs-fact-i18n",
  "version": "1.0.0",
  "description": "I18n version of fact skill.",
  "main": "index.js",
  "scripts": {
    "zip": "node-lambda zip -A ./build",
    "test": "mocha -R spec "
  },
  "keywords": [
    "alexa",
    "skill",
    "fact"
  ],
  "author": "Amazon.com",
  "license": "Apache-2.0",
  "dependencies": {
    "alexa-sdk": "^1.0.12"
  },
  "devDependencies": {
    "alexa-skill-test-framework": "^1.1.3",
    "mocha": "^4.0.1",
    "node-lambda": "^0.11.5"
  }
}

サンプルコードは node_modules/alexa-skill-test-framework/examples/ に色々入っていて、 skill-sample-nodejs-fact には fact 用のサンプルも用意されています。

ざっとコードを抜粋すると

spacefact-tests.js
/*                                                                                                                                                                          [77/1871]
Mocha tests for the Alexa skill "Space Facts" example (https://github.com/alexa/skill-sample-nodejs-fact).
Using the Alexa Skill Test Framework (https://github.com/BrianMacIntosh/alexa-skill-test-framework).

Run with 'mocha examples/skill-sample-nodejs-fact/spacefact-tests.js'.
*/

// include the testing framework
//const alexaTest = require('alexa-skill-test-framework');
const alexaTest = require('../../index');

// initialize the testing framework
alexaTest.initialize(
        require('./spacefact.js'),
        "amzn1.ask.skill.00000000-0000-0000-0000-000000000000",
        "amzn1.ask.account.VOID");

// initialize i18n
var textResources = require("./spacefact-language");
alexaTest.initializeI18N(textResources);

var supportedLocales = ["en-US", "en-GB", "de-DE"];

// perform each test in each supported language
for (var i = 0; i < supportedLocales.length; i++) {
        var locale = supportedLocales[i];

        // set the language
        alexaTest.setLocale(locale);

        // callback function that asserts if the provided string is not a fact from the list
        var assertIfNotFact = function (context, suspectedFact) {
                var facts = context.t("FACTS");
                for (var i = 0; i < facts.length; i++) {
                        if (suspectedFact === "<speak> " + context.t("GET_FACT_MESSAGE") + facts[i] + " </speak>") return;
                }
                context.assert({ message: "'" + suspectedFact + "' is not a space fact." });
        }

        describe("Space Fact Skill (" + locale + ")", function () {
                // tests the behavior of the skill's LaunchRequest
                describe("LaunchRequest", function () {
                        alexaTest.test([
                                {
                                        request: alexaTest.getLaunchRequest(), shouldEndSession: true, repromptsNothing: true,
                                        saysCallback: assertIfNotFact
                                }
                        ]);
                });
...

RSpec を読み慣れた人はなんとなくわかると思いますが、 describe 以降が検証コードになります。 この場合、 "LaunchRequest" の検証を行っています。 セッションが終了しているか、リプロンプト(一定時間待機で再度音声読み上げをするか)をしないか、音声発生の結果を assertIfNotFact にコールバックして、正しいかどうかを検証していますね。

こんな感じで、 結果となるSSMLの文字列との比較と言う形で、検証が可能です。
ですので

  1. テストコード書く
  2. コード書く
  3. yarn test spec/***.js でテスト流す (駄目なら 1へ
  4. zip化して AWS Lambdaへup
  5. サービスシミュレーターでテスト (駄目なら 1へ

1~3部分が随分と高速化するはずです。

会話の質向上 / SSML(音声合成マークアップ言語) の理解

文字列を返すだけで、いい感じに話をしてくれる Alexa はすごいですが、場合によって、ちょっと強調して話をしたり、ゆっくり話をしたりしたくなるかもしれません。

Alexaの発声音声は SSML(音声合成マークアップ言語) と呼ばれる標準規格に基づいています。

factの例で行くと、各インテントで最後に返却する文字列の前後に <speak> タグをつけて

<speak>これをゆっくりよんでね</speak>

と定義した文字列に対して、HTMLのタグのように prosody 要素を追加することで

<speak>これを<prosody rate="90%">ゆっくり</prosody>よんでね</speak>

ゆっくり の部分がゆっくり読まれるようになります。

正直ここは SSMLの組み立てが超便利なssml-builderについて でほとんど解説済みなのでそちらを読んで頂いた方が良いかと思います。

ハマり系

Slotの前後に空白入れないと認識しない問題

何を言っているのかわからないかもしれませんが、最初はマジでハマりました。


image.png


image.png

おわかり頂けただろうか。
実はこれ、slotの前後に空白が無いと、Utterancesには追加できるのに Build Model で壮大にエラーになるんです...

image.png

英語圏の場合、単語区切りに空白を入れるのは普通なので問題にはなりづらいのでしょう。
でも最初は何のことかさっぱりわからずに、slotの命名を変えたり語彙から記号を外したりして随分時間を食いました。

皆さんはこれをみて華麗に回避してください。

タイムゾーンがわからない問題

Alexaから受け取れるリクエストパラメータには timestamp が文字としてくっついてきます。

{
  "session": {
   // 省略
  },
  "request": {
    "type": "IntentRequest",
    "requestId": "EdwRequestId.b692a908-bd5a-4893-bc8c-25eefd59c0e1",
    "intent": {
      "name": "TestIntent",
      "slots": {
        // 
      }
    },
    "locale": "ja-JP",
    "timestamp": "2017-12-10T13:24:51Z"
  },

timestamp を元に処理をさせたい場合、通常であれば new Date(timestamp) のような処理をする場合が多いのですが、よく見ると、タイムゾーン(+09:00とかAsia/Tokyoとか)の指定はありません。。

new Dateでの日付パースの場合、エンドポイントとして設定した AWS Lambda が動作するリージョンのタイムゾーンが暗黙的に利用されます。そのため、 "とりあえずバージニア北部でいいか〜!" ってしておくと、JSTのつもりが時刻が合わない。ということになります。

一応、設定画面でリージョン選んでARN設定できるっちゃできるのですが、

image.png

そもそも Alexa本体にタイムゾーンの設定がある んですよね・・・

実機で検証出来ていないのですが、本体にタイムゾーン設定があるなら、リクエストに含められるはずで・・・どっかのパラメータで取れる可能性があるわけで・・・
それ前提で、柔軟にタイムゾーンを扱っておきたくなってしまいます。

ということで、moment-timezone を入れておきましょう

例えば timestamp に応じて挨拶を変える。とかだと

const moment = require("moment-timezone");

const helpers = {
  greeting: function(timestamp) {
    let ts = moment.tz(timestamp, 'Asia/Tokyo'); //FIXME: TZをAlexaから貰いたい
    let hour = ts.hour();
    if (5 <= hour && hour < 12) { //夏時間考えない
      return this.t('MORNING');
    } else if (12 <= hour && hour < 17) {
      return this.t('AFTERNOON');
    } else {
      return this.t('NIGHT');
    }
  },

これで明示的に JST での日付処理になります。

まとめ

いかがだったでしょうか。ここまで来ると悩みどころが Skill Kit の使い方から 会話デザインそのものへ移って行ったり、 DynamoDB や 外部リソースとの連携を AWS Lambda 上でどのように構築するか。を悩んだり、新しいステップの道が開けてるのでは無いかと思います。

参考に、ここまでの要素が入ったコードを github へ置いておきます。
是非、自由にいじってみて下さい。

自分ならではのアイデアを乗せたりしながら、日常がより便利になるようなスキル開発ライフを楽しみましょう!

次回

エイチームライフスタイルアドベントカレンダー2017、明日は新人デザイナ @hsmy さんの "誰でもできるAdobe Lightroomを使った写真編集の楽しみ方" です。

そういえば、クリスマスイブですね!お楽しみに!!!


お知らせ

株式会社エイチームライフスタイルでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
http://www.a-tm.co.jp/recruit/

商標等

  • Amazon, Echo, Alexaと 全ての関連ロゴは Amazon.com, Inc. またはその関連会社の商標です。
  • Amazon Echo デバイス画像は商標ガイドラインに則り、メディアギャラリー に掲載されているものを使用しています。