前回
前回の投稿では、ハンドラにあるインテントの外部モジュール化を行いました。
いよいよ、実装編は最後になります。残りは発話についてです。
はじめに
Alexa スキルは日本以外の他の国でも提供できるように、スキル毎に複数の言語が選択できるようになっています。
alexa-skills-kit-sdk-for-nodejs
にはローカライズする機能がi18next
で実装されています。
将来的に多言語対応することを見据えて、本投稿でもこの機能を利用する前提で進めていきます。(サンプルスキルでは既に日本語だけ利用しています。)
さて、今回の投稿では、発話を外部モジュール化していきます。
さいごのじっそうだ
index.ts
とfirst-handler.ts
で実装されている発話部分を外部モジュール化します。
const languageStrings = {
'ja-JP': {
'translation': {
// ~~~省略~~~
}
}
};
'FirstIntent': function (this: Alexa.Handler<any>) {
var sayNumber = this.event.request.intent.slots.number.value;
if (!sayNumber || !newsContents.hasOwnProperty(sayNumber)) {
this.emitWithState('Unhandled');
}
getNewsAsync(sayNumber)
.then((content) => {
this.emit(':ask', this.t('ASK_ANSWER_NUMBER', content), this.t('ASK_ANSWER_NUMBER_REPROMPT'));
})
.catch((error) => {
this.emitWithState("Unhandled");
});
},
まずは、発話ディレクトリをつくろう
$ cd ~/custom-skill-sample-to-convert/skill/lambda/custom/src
$ mkdir -p utterances
i18nextに食わせるローカライズデータを外出しする
index.ts
にそのまま置き去りにしていたローカライズデータ(languageStrings
変数)を外出ししてあげましょう。
$ touch ./utterances/language-strings.ts
export const languageStrings = {
'ja-JP': {
'translation': {
'TELL_WELCOME_MESSAGE': 'タイろうへようこそ。',
'ASK_HELP_MESSAGE': '声を聞かせて、と言ってください。',
'ASK_START_MODE': 'ファーストモードに変更します。数字を言ってください。',
'ASK_START_MODE_REPROMPT': '数字を言ってください。',
'ASK_UNHANDLED_MESSAGE': 'すみません、よく聞きとれませんでした。もう一回言ってください。',
'TELL_GOOD_BYE': 'さようなら',
'TELL_UNHANDLED_MESSAGE': 'すみません、よく聞きとれませんでした。',
'ASK_ANSWER_NUMBER': '%s。他に聞きたい数字はありますか?',
'ASK_ANSWER_NUMBER_REPROMPT': '数字を言ってください。',
'TELL_HELLO_WORLD': 'こんにちは、世界',
'ASK_ANYTHING_ELSE': '他になにかありますか?',
}
}
};
これはもう、カット&ペーストしてエクスポートするだけなので単純ですね。
インポートも忘れずに。
import { languageStrings } from './utterances/language-strings';
発話クラスの作成
発話クラスでは、FirstIntent
の以下の部分で必要な最初の発話
と追加の発話
を返す機能を実装します。
getNewsAsync(sayNumber)
.then((content) => {
this.emit(':ask', this.t('ASK_ANSWER_NUMBER', content), this.t('ASK_ANSWER_NUMBER_REPROMPT'));
})
.catch((error) => {
this.emitWithState("Unhandled");
});
準備
まずは、ファイルを準備しましょう。
発話クラスもインテント同様、コンテキストに依存する為基底クラスを作成します。
$ touch ./utterances/utterance-base.ts
import * as Alexa from 'alexa-sdk';
/**
* 発話基底クラス
*/
export abstract class UtteranceBase {
/**
* プロパティ - Alexaハンドラコンテキスト
*/
protected alexaContext: Alexa.Handler<any>;
/**
* コンストラクタ
* @param context Alexaハンドラコンテキスト
*/
constructor(context: Alexa.Handler<any>) {
this.alexaContext = context;
}
}
では、これを基に発話クラスを実装していきましょう。
まずは、形だけ作ります。
$ touch ./utterances/first-utterance.ts
import * as Alexa from 'alexa-sdk';
import { UtteranceBase } from './utterance-base';
/**
* ファースト発話クラス
*/
export class FirstUtterance extends UtteranceBase {
/**
* コンストラクタ
* @param context Alexaハンドラコンテキスト
*/
constructor(context: Alexa.Handler<any>) {
super(context);
}
/**
* 発話内容取得
*/
public respond() {
}
}
発話クラスのrespond
メソッドを実装する
準備が終わったので、respond
メソッドを実装していきます。
せっかく、TypeScriptを使うのでインタフェースも使っていきましょう。
respond
メソッドで最初の発話
と追加の発話
を返す為には、ユーザから発話された数字が必要になります。
ここでは、2つのインタフェースが必要になります。
- 発話内容を得る為の条件(
ユーザが発話した数字
) - 発話内容(
最初の発話
と追加の発話
)
これらをインタフェースで定義します。
$ mkdir -p conditions
$ mkdir -p models
$ touch ./conditions/first-utterance-condition.ts
$ touch ./models/first-utterance-result.ts
/**
* 発話条件 インタフェース
*/
export interface IFirstUtteranceCondition {
/**
* 数字
*/
sayNumber: string,
}
/**
* 発話結果 インタフェース
*/
export interface IFirstUtteranceResult {
/**
* 初回の発話
*/
speech: string;
/**
* 追加の発話
*/
repromptSpeech: string;
}
これを加えると、FirstUtteranceクラスはこんな感じになります。
import * as Alexa from 'alexa-sdk';
import { IFirstUtteranceCondition } from '../conditions/first-utterance-condition';
import { IFirstUtteranceResult } from '../models/first-utterance-result';
import { UtteranceBase } from './utterance-base';
/**
* ファースト発話クラス
*/
export class FirstUtterance extends UtteranceBase {
/**
* コンストラクタ
* @param context Alexaハンドラコンテキスト
*/
constructor(context: Alexa.Handler<any>) {
super(context);
}
/**
* 発話内容取得
* @param condition 条件
*/
public respond(condition: IFirstUtteranceCondition): IFirstUtteranceResult {
const result: IFirstUtteranceResult = {
speech: '',
repromptSpeech: ''
};
return result;
}
}
あとは、result
変数に値をセットするだけですね。
getNewsAsync
メソッドの外部モジュール化
これもTypeScriptを使うので、async, awaitを使った実装に変えていきましょう。
News
となってるが、ニュースを返すものではないですが。。。
$ touch ./models/news-repository.ts
/**
* ニュースリポジトリクラス
*/
export class NewsRepository {
/**
* コンストラクタ
*/
constructor() {
// 処理なし
}
/**
* 内容取得
* @param sayNumber 数字
* @returns 内容
*/
public getAsync(sayNumber: string): Promise<string> {
// データ(便宜上このクラスに定義しているが、本来はデータベースからエンティティを返す想定)
let newsContents: {[key: string]: string } = {
'1': '1番です',
'2': '2番です',
'3': '3番です',
};
return new Promise((resolve, reject) => {
if (newsContents[sayNumber]) {
resolve(newsContents[sayNumber]);
} else {
reject();
}
});
}
}
大きく処理は変わっていませんが、内容が取得できなかった場合はreject
を呼び出しています。
では、これを発話クラスに盛り込みましょう。
発話クラスでasync, awaitを使う
import * as Alexa from 'alexa-sdk';
import { IFirstUtteranceCondition } from '../conditions/first-utterance-condition';
import { IFirstUtteranceResult } from '../models/first-utterance-result';
import { NewsRepository } from '../models/news-repository';
import { UtteranceBase } from './utterance-base';
/**
* ファースト発話クラス
*/
export class FirstUtterance extends UtteranceBase {
/**
* リポジトリ
*/
private repository: NewsRepository;
/**
* コンストラクタ
* @param context Alexaハンドラコンテキスト
*/
constructor(context: Alexa.Handler<any>) {
super(context);
// リポジトリ作成
this.repository = new NewsRepository();
}
/**
* 発話内容取得
* @param condition 条件
* @returns 結果
*/
public async respond(condition: IFirstUtteranceCondition): Promise<IFirstUtteranceResult> {
const result: IFirstUtteranceResult = {
speech: '',
repromptSpeech: ''
};
// データ取得(非同期)
const speech = await this.repository.getAsync(condition.sayNumber);
// 結果をセット
result.speech = this.alexaContext.t('ASK_ANSWER_NUMBER', speech);
result.repromptSpeech = this.alexaContext.t('ASK_ANSWER_NUMBER_REPROMPT');
return result;
}
}
ここで1つ問題が出ました。。。
this.alexaContext.t
の戻り値がvoidになってるんですよね。。。
結果をセットできない。。。
ここの戻り値はstring
であるべきだと思ってるんですが、どうなんでしょうか。
仕方がないので、インタフェースのspeech
とrepromptSpeech
の型をstring
からany
にすることで対応しました。
発話クラスはこれで出来上がり!
あとは、ハンドラの呼出部分を変更しましょう。
発話クラスをハンドラから呼ぶ
まずは使う為に、インポートです。
import { IFirstUtteranceCondition } from '../conditions/first-utterance-condition';
import { IFirstUtteranceResult } from '../models/first-utterance-result';
import { FirstUtterance } from '../utterances/first-utterance';
次にFirstIntent
の呼出部分です。
非同期呼出メソッドでの戻り値の型はPromise
なので、async
をつけてます。
発話クラスから取得した結果をthis.emit
の第2、3引数にセットして終わりです。
'FirstIntent': async function (this: Alexa.Handler<any>) {
var sayNumber = this.event.request.intent.slots.number.value;
// 発話取得条件設定
const condition: IFirstUtteranceCondition = {
sayNumber: sayNumber
};
try {
// 発話内容取得
const result = await (new FirstUtterance(this)).respond(condition);
this.emit(':ask', result.speech, result.repromptSpeech);
} catch (error) {
this.emitWithState("Unhandled");
}
},
まとめ
この投稿では、発話に関して外部モジュール化を行いました。
実装編は以上になります。いかがでしたでしょうか?
設計に合わせ分類して実装を行いました。
TypeScriptの使い方、私もまだまだ勉強中ですが、これから始める人の参考になれば幸いです。
細かい説明が無いので分かり辛いかもしれませんが。。。
今回の投稿のソースは以下にあります。
発話外部モジュール化
次回は、いよいよ最後。単体テストです。