kun432's blog

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

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

日本語に対応したAmazon Lex(V1)を試してみる④ Lambdaを使う

f:id:kun432:20210427220639p:plain

Amazon Lexの第4回です。今回は受け取った発話をバックエンドのLambdaに渡してみたいと思います。

目次

LexでHello World

引き続き"BookTrip"のサンプルを使って、LexからLambdaにユーザからの入力を渡すようにしてみましょう。

まずはかんたんなHello Worldをやってみます。以下のようなLambda関数を作成します。

'use strict';

exports.handler = async (event, context, callback) => {
    console.log(JSON.stringify(event));
    const response = {
        sessionAttributes: event.sessionAttributes,
        dialogAction: {
            type: 'Close',
            fulfillmentState: 'Fulfilled',
            message: {
                contentType: 'PlainText',
                content: 'ハローワールド'
            }
        }
    };
    callback(null, response);
};

responseがLexに返すレスポンスの中身です。この中で、dialogActionというのが必須になっています。見た感じ、なんとなくわかりますよね。

  • type: 'Close' セッションが終了することを伝えて、ユーザからの入力を受けないようにします。AlexaでいうところのSessionEndedRequest: trueみたいなものですかね。
  • fulfillmentState: 'Fulfilled'で収集中のスロットが全て満たされたことを伝えます。スロットを使っていないとしてもこのステータスは必須のようです。
  • messageがLexの応答になるようです。なければLexのインテント側に設定されたレスポンスが使われるようです。

セッションアトリビュートなども使えるようですが、詳細はドキュメントを御覧ください。

関数名は"LexTest"にしました。

f:id:kun432:20210503200848p:plain

Lex側でLamdaのレスポンスを使うように設定します。「Fulfillment」で「AWS Lambda function」を選択して、Lambdaの関数名とバージョンを設定します。

f:id:kun432:20210503205121p:plain

下にスクロールして「Save Intent」をクリックして保存、「Build」をクリックして対話モデルをビルドします。

f:id:kun432:20210503205356p:plain

f:id:kun432:20210503205406p:plain

ではテストしてみましょう。

f:id:kun432:20210503205614p:plain

最後に「ハローワールド」が返ってきているのがわかりますね。

ちなみに、LexからLambdaに渡されたイベントの中身はこんな感じでした。

  • イベント
{
    "messageVersion": "1.0",
    "invocationSource": "FulfillmentCodeHook",
    "userId": "XXXXXXXXXXXXXXXXXXXXXXXXXX",
    "sessionAttributes": {},
    "requestAttributes": null,
    "bot": {
        "name": "BookTrip_jaJP",
        "alias": "$LATEST",
        "version": "$LATEST"
    },
    "outputDialogMode": "Text",
    "currentIntent": {
        "name": "BookCar_jaJP",
        "slots": {
            "PickUpDate": "2022-05-01",
            "ReturnDate": "2022-05-02",
            "DriverAge": "40",
            "CarType": "エコノミー",
            "PickUpCity": "神戸"
        },
        "slotDetails": {
            "PickUpDate": {
                "resolutions": [
                    {
                        "value": "2022-05-01"
                    }
                ],
                "originalValue": "5/1"
            },
            "ReturnDate": {
                "resolutions": [
                    {
                        "value": "2022-05-02"
                    }
                ],
                "originalValue": "5/2"
            },
            "DriverAge": {
                "resolutions": [
                    {
                        "value": "40"
                    }
                ],
                "originalValue": "40"
            },
            "CarType": {
                "resolutions": [
                    {
                        "value": "エコノミー"
                    }
                ],
                "originalValue": "エコノミー"
            },
            "PickUpCity": {
                "resolutions": [
                    {
                        "value": "神戸"
                    }
                ],
                "originalValue": "神戸"
            }
        },
        "confirmationStatus": "Confirmed",
        "nluIntentConfidenceScore": 1
    },
    "alternativeIntents": [
        {
            "name": "AMAZON.FallbackIntent",
            "slots": {},
            "slotDetails": {},
            "confirmationStatus": "None",
            "nluIntentConfidenceScore": null
        },
        {
            "name": "BookHotel_jaJP",
            "slots": {
                "RoomType": null,
                "CheckInDate": null,
                "Nights": null,
                "Location": null
            },
            "slotDetails": {
                "RoomType": null,
                "CheckInDate": null,
                "Nights": null,
                "Location": null
            },
            "confirmationStatus": "None",
            "nluIntentConfidenceScore": 0.15
        }
    ],
    "inputTranscript": "はい",
    "recentIntentSummaryView": [
        {
            "intentName": "BookCar_jaJP",
            "checkpointLabel": null,
            "slots": {
                "PickUpDate": "2022-05-01",
                "ReturnDate": "2022-05-02",
                "DriverAge": "40",
                "CarType": "エコノミー",
                "PickUpCity": "神戸"
            },
            "confirmationStatus": "None",
            "dialogActionType": "ConfirmIntent",
            "fulfillmentState": null,
            "slotToElicit": null
        }
    ],
    "sentimentResponse": {
        "sentimentLabel": "NEUTRAL",
        "sentimentScore": "{Positive: 0.052890804,Negative: 0.0028756545,Neutral: 0.9416867,Mixed: 0.002546887}"
    },
    "kendraResponse": null
}

スロットはevent.currentIntent.slots.スロット名で取れますね。nluIntentConfidenceScoreでインテントの信頼度や、sentimentResponseで感情分析が見えるのも面白いです。

スロットを使う

ということでスロットの値を使って、BookCarインテントを実装してみましょう。上に書いたとおり、スロットはevent.currentIntent.slots.スロット名で取れますので、こういう感じ。

'use strict';

exports.handler = async (event, context, callback) => {
    const slots = event.currentIntent.slots;
    
    const PickUpCity = slots.PickUpCity;
    const PickUpDate = slots.PickUpDate;
    const ReturnDate = slots.ReturnDate;
    const CarType    = slots.CarType;
    const DriverAge  = slots.DriverAge;
    
    let message;
    if (DriverAge >= 20 ) {
        message = `${PickUpCity} で ${CarType} を、${PickUpDate} から ${ReturnDate} までご予約いたしました。`;
    } else {
        message = `申し訳ございません。レンタカーのご利用は20歳以上とさせていただいております。`;
    }
    
    const response = {
        sessionAttributes: event.sessionAttributes,
        dialogAction: {
            type: 'Close',
            fulfillmentState: 'Fulfilled',
            message: {
                contentType: 'PlainText',
                content: message
            }
        }
    };
    callback(null, response);
};

すこし動的な要素を含めるために、年齢でレスポンスの内容を変えてみました。テストしてみるとこんな感じです。

f:id:kun432:20210503223451p:plain

最後まで聞いてから20歳未満はNGというのはインタフェース的にはイマイチなので、年齢を確認するインテントを別に用意して先に確認するような会話フローにすべきだと思いますが、まあ一応動いてますね。

予約完了した場合はこちらです。

f:id:kun432:20210503223502p:plain

ちゃんと動いてますね。

インテントのルーティング

上の例では1インテントに1つのLambda関数になっていました。1つのLambda関数の中でインテントごとに呼び出す処理をルーティングしてまとめて書きたいですよね。公式のサンプルによいものがありました。

上記を参考に書いてみたのがこれ。

'use strict';


function close(sessionAttributes, fulfillmentState, message) {
    return {
        sessionAttributes,
        dialogAction: {
            type: 'Close',
            fulfillmentState,
            message,
        },
    };
}

function BookCarIntent(intentRequest, callback) {
    const sessionAttributes = intentRequest.sessionAttributes || {};
    const slots = intentRequest.currentIntent.slots;
    const PickUpCity = slots.PickUpCity;
    const PickUpDate = slots.PickUpDate;
    const ReturnDate = slots.ReturnDate;
    const CarType    = slots.CarType;
    const DriverAge  = slots.DriverAge;
    
    let message;
    if (DriverAge >= 20 ) {
        message = `${PickUpCity} で ${CarType} を、${PickUpDate} から ${ReturnDate} までご予約いたしました。`;
    } else {
        message = `申し訳ございません。レンタカーのご利用は20歳以上とさせていただいております。`;
    }

    callback(close( sessionAttributes, 'Fulfilled', {
        contentType: 'PlainText',
        content: message
    }));
}

function BookHotelIntent(intentRequest, callback) {
    const sessionAttributes = intentRequest.sessionAttributes || {};
    const slots = intentRequest.currentIntent.slots;
    const Location    = slots.Location;
    const Nights      = slots.Nights;
    const CheckInDate = slots.CheckInDate;
    const RoomType    = slots.RoomType;

    
    let message = `${Location} で ${CheckInDate} から ${Nights} 泊、${RoomType} タイプのお部屋を予約しました。`;

    callback(close( sessionAttributes, 'Fulfilled', {
        contentType: 'PlainText',
        content: message
    }));
}

function dispatch(intentRequest, callback) {
    console.log(JSON.stringify(intentRequest, null, 2));
    console.log(`dispatch userId=${intentRequest.userId}, intentName=${intentRequest.currentIntent.name}`);
     
    const intentName = intentRequest.currentIntent.name;
 
    if (intentName === "BookCar_jaJP") {
        return BookCarIntent(intentRequest, callback);
    } else if (intentName === "BookHotel_jaJP") {
        return BookHotelIntent(intentRequest, callback);
    } else {
      throw new Error(`Intent with name ${intentName} not supported`);
    }
}

exports.handler = (event, context, callback) => {
    try {
        dispatch(event, (response) => callback(null, response));
    } catch (err) {
        callback(err);
    }
};

1インテント=1関数として作成して、function dispatchでインテントごとに別の関数を呼び出す形ですね。ASK-SDK v2とかに慣れているとこちらのほうがなんとなくしっくりきますが、LexのほうがレスポンスをJSONでガッツリ書かないといけない印象があります。このあたりは上のfunction closeのようにレスポンスをテンプレートとしてラップしてあげればいいかなと思います。

テストしてみましょう。LexではインテントごとにFulfilmentの設定が必要なのでそれぞれ設定変更するのをお忘れなく。

f:id:kun432:20210503231653p:plain

ちゃんと動いてますね!

まとめ

Lambdaで動的にレスポンス生成できるようになるといろいろ夢が広がりますね。ASK-SDK v2に慣れていれば、比較的違和感なく書けるのではないかと思います。

いや、もっとASK-SDKっぽく書きたい!という人は以下も試してみてはいかがでしょうか?

日本語に対応したAmazon Lex(V1)を試してみる③ Amazon Connectとつなげてみる

f:id:kun432:20210427220639p:plain

Amazon Lexの第3回です。今回はV1でしか使えないAmazon Connectのコンタクトフローとの統合を試してみたいと思います。

なお、予め注意すべき点として、「Amazon Connectで日本の電話番号個人で取得することは現時点ではできない」という点があります。以下の手順ではアメリカの電話番号を取得しています。つまり国際電話になりますので、電話料金についてはくれぐれもご注意ください。

目次

Amazon Connectコンタクトセンターインスタンスの作成

まずはAmazon Connectの設定を行っていきましょう。マネジメントコンソールからAmazon Connectの画面を開き、「今すぐ始める」をクリック。

f:id:kun432:20210430002558p:plain

Amazon Connectのリソースの設定を行います。まず、ID管理。

Amazon Connectは、マネジメントコンソールとは別にConnect専用の管理画面が作成されます。その際のユーザの管理方法と管理画面のURLを設定します。

  • ID管理については、AWS Directory ServiceやSAMLを使った外部アカウントとの連携も可能なようですが、今回は「Amazon Connect内にユーザーを保存」を選択しました。
  • アクセスURLは管理画面にアクセス適当なものを設定します。これがコンタクトセンターのインスタンス名となるようです。今回は"lex-booktrip-sample"としました。

設定したら「次のステップ」をクリック。

f:id:kun432:20210430002925p:plain

次にコンタクトセンターの管理者アカウントを設定します。おそらく「Amazon Connect内にユーザーを保存」を選択したためですね。適宜入力して「次のステップ」をクリック。

f:id:kun432:20210430003703p:plain

テレフォニーオプションでは、着信・発信の有効無効を設定します。着信のみ、発信のみ、ということもできるということですかね。デフォルトでは両方有効になるようです。今回は両方有効にしておきます。「次のステップ」をクリック。

f:id:kun432:20210430003803p:plain

ストレージの設定画面です。通話やチャットの記録はAmazon Connect用にS3バケットが作成され保存されます。カスタマイズもできるようですが、今回はデフォルトで。「次のステップ」をクリック。

f:id:kun432:20210430004024p:plain

確認画面です。ここまでの設定に問題がなければ「インスタンスの作成」をクリックします。

f:id:kun432:20210430004259p:plain

インスタンスが作成されます。数分程かかりますのでしばらく待ちましょう。

f:id:kun432:20210430004347p:plain

以下のような画面が表示されればインスタンスの作成が完了しました。「今すぐ始める」をクリックします。

f:id:kun432:20210430004451p:plain

新しいタブがひらいて以下の画面が表示されます。これがAmazon Connectの管理画面です(初回のみセットアップ画面が出てくるようで、2回目以降はちょっと変わります)。ちょっと面倒なので日本語にしましょう。

f:id:kun432:20210430022102p:plain

日本語になりました。ではセットアップします。「今すぐ始める」をクリックします。

f:id:kun432:20210430022354p:plain

通知とマイクの有効化を促すメッセージが出ます。多分ブラウザフォンを使うためには必要なのではないかと思います。「許可」しておきます。

f:id:kun432:20210430022614p:plain

f:id:kun432:20210430022625p:plain

最初に電話番号を取得します。「国/地域」で「Japan」を選択します。「タイプ」は、通常の電話番号である「DID(直通ダイアル)」といわゆるフリーダイアルである「料金無料電話」から選択できますが、「DID(直通ダイアル)」のままでよいです。

f:id:kun432:20210430022802p:plain

するとこういうメッセージが表示されて、先に進めません・・・

f:id:kun432:20210430023126p:plain

調べてみるとありました。

どうやら使える電話番号の在庫が枯渇しているということ、そして法改正により書類提出が義務となっているため、電話番号が取得できないようですね。

どうやらアメリカの電話番号であれば取得できるようなので、今回はあくまでも検証用ということでそれで進めます。

f:id:kun432:20210502172902p:plain

以下のように表示されれば電話番号が取得できています。他にも色々表示されていますが、とりあえず"Continue"をクリック。

f:id:kun432:20210502173018p:plain

Amazon Connectのダッシュボード画面が表示されれば完了です。

f:id:kun432:20210502174234p:plain

Amazon ConnectのコンタクトフローにAmazon Lexのボットを紐付ける

ではAmazon Connectの応答にAmazon Lexのボットを使うようにしてみましょう。この設定はAWSマネジメントコンソール側で行います。検索からAmazon Connectの設定画面を開きます。

f:id:kun432:20210502175649p:plain

先ほど作成したコンタクトセンターのインスタンス名が表示されていますので、これをクリック。

f:id:kun432:20210502180625p:plain

左のメニューの「問い合わせフロー」をクリックします。

f:id:kun432:20210502181001p:plain

Amazon Lexのところで、作成したLexボットのリージョンおよびボット名を選択して「+Lexボットの追加」をクリックします。(前々回で作成したBookTripのサンプルをそのまま使っています)

f:id:kun432:20210502181304p:plain

以下のように表示されればOKです。これでAmazon ConnectのコンタクトフローからAmazon Lexを呼び出せるようになりました。

f:id:kun432:20210502181103p:plain

Amazon Connectのコンタクトフローの作成

ではAmazon Connectのコンタクトフローを作成していきましょう。Amazon Connectの管理画面に戻り、左の「ルーティング」メニューから「問い合わせフロー」をクリックします。

f:id:kun432:20210502182754p:plain

予め登録されているコンタクトフローの一覧が表示されていますが、新規に作りましょう。右上の「コンタクトフローの作成」をクリックします。

f:id:kun432:20210502183013p:plain

コンタクトフローの作成画面が開きます。Amazon ConnectではGUIでブロックを並べて線をつなげることで会話フローを作成します。Voiceflowを使ったことがあれば理解しやすいのではないかなと思います。

f:id:kun432:20210502183727p:plain

図にも記載してますが、「エントリポイント」が会話フローの起点になります。電話をかけるとここからフローがスタートするということです。そして左のメニュー内にいろいろな機能のブロックが用意されているので、これらを組み合わせていくことで会話フローが構築されます。

では早速やってみましょう。

最初にフローの名前を入力します。なんでもよいです。

f:id:kun432:20210502184801p:plain

そして基本的な音声の設定を行います。「設定」をクリック。

f:id:kun432:20210502221355p:plain

下のほうにある「音声の設定」をドラッグ・アンド・ドロップでエントリポイントの右側あたりに配置します。

f:id:kun432:20210502221458p:plain

エントリポイントの「開始」の白い丸からドラッグすると線が引けますので、先程配置した「顧客の入力を取得する」につなげます。

f:id:kun432:20210502221530p:plain

ブロックを設定するとその設定が行なえます。「音声の設定」では言語と音声を選択します。この設定は日本語の場合は必ず必要になりますので忘れないようにしてください(設定しない場合のデフォルトは英語になります)。設定したら「Save」をクリック。

f:id:kun432:20210502221722p:plain

次に「操作」から「顧客の入力を取得する」をさらに右側に配置して、線でつなげます。

f:id:kun432:20210502222123p:plain

「顧客の入力を取得する」ブロックの設定はこんな感じになっています。

f:id:kun432:20210502200748p:plain

「顧客の入力を取得する」は、ユーザの音声発話やダイヤル押下(「〜の場合は1を」みたいなやつですね)などの入力を受け取るためのブロックです。Amazon Lexで作成したボットはインテントにマッチしたサンプル発話を受け取ることで実行されますので、Amazon Connect側でユーザの発話が受け取ってLexに渡してあげる必要があるわけですね。

また、Amazon Connectからの音声発話も設定することができます。電話をかけてきたユーザに何も言わずにいきなり喋ってもらうというのは、ユーザからするとどうすればいいかわかりませんので、導入となるAmazon Connectからの発話もここで設定しましょう。

まず、Amazon Connectからの応答です。「プロンプトライブラリ(音声)」と「テキスト読み上げまたはチャットテキスト」から選択します。「プロンプトライブラリ(音声)」は予め用意された音声データを再生、「テキスト読み上げまたはチャットテキスト」は設定したテキストを読み上げます。ここでは、「テキスト読み上げまたはチャットテキスト」を選択して、「テキストの入力」を選択、最初の発話をAmazon Connectから行うようにします。

f:id:kun432:20210502201709p:plain

そしてユーザからの発話の取得の設定です。「DTMF」はダイヤルからのプッシュ信号、「Amazon Lex」はユーザの発話を入力として受け取ります。「Amazon Lex」をクリックして、Amazon Lexのボット名を指定します。インテントは受け取る事が可能なLexのボットのインテントを指定します。BookTripサンプルでは2つのインテントが用意されているので療法家取れるようにしてみました。最後に「Save」をクリックします。

f:id:kun432:20210502201916p:plain

「顧客の入力を取得する」の表示が変わりました。ユーザからの発話に応じて、2つのインテント、デフォルト、エラーと、合計で4つのフロー分岐が行えるようになります。デフォルトとエラーは、それぞれインテントにマッチしない場合、またはLexからエラーが返ってきた場合になるようです。

f:id:kun432:20210502222604p:plain

では分岐のフローを作りましょう。会話のインタフェースとしては良くないですが、あくまでも検証なので以下のような簡易なものにしたいと思います。

  • Lexの2つのインテントが処理されたら、「ご利用ありがとうございました」と発話して、電話を切る。
  • どのインテントにもマッチしない場合は、「最初からやりなおしてください」と発話して、電話を切る。

少し説明は割愛しますが、Amazon Connectに発話だけを行わせるブロックは「操作」にある「プロンプトの再生」、電話の切断は「終了/転送」にある「切断」を使います。最終的にはこういう感じになります。

f:id:kun432:20210502222751p:plain

フローができたら、右上の「保存」をクリックして「公開」をクリックします。

f:id:kun432:20210502203641p:plain

作成したフローを電話番号と紐付けます。左の「ルーティング」メニューから「電話番号」をクリックします。

f:id:kun432:20210502203818p:plain

取得した電話番号が表示されます。デフォルトでは「Sample inbound flow」という予め用意されたフローに紐付いているようです。電話番号をクリックして変更します。

f:id:kun432:20210502204102p:plain

先ほど作成したコンタクトフローに変更して「保存」をクリックします。

f:id:kun432:20210502204155p:plain

これでAmazon ConnectからAmazon Lexを呼び出せるようになりました。

テスト

実際に電話を掛けて試してみました。国際電話になるのでちょっと怖いですがw

ちゃんと動いてますね。

まとめ

あくまでも検証なので、会話フローやエラーハンドリングなどいろいろ改善の余地はありますが(エラーになったらもう一度最初に戻すとか、あとはユーザが発話するターンということを促すために効果音を入れたり、とかは必要かなと思いました)、とてもかんたんに電話と組み合わせることができました。

Amazon Connect単体だと(そこまで深く触ってないのでわかってないだけかもですが)複雑な会話は難しい気がしますが、Amazon Lexと組み合わせることでより自然な対話が実現可能になるのではないかなと思います。

あとは、

  • コンタクトセンター統合は現状Amazon Lex API v1でしか使えない。今後v2が移行していくことを考えると早く対応してほしい。
  • 個人で日本の電話番号が取得できるともっとお試ししやすくていいんですが・・・

あたりは今後に期待したいところです。

日本語に対応したAmazon Lex(V1)を試してみる② Alexaのダイアログモデルと比較してみる

f:id:kun432:20210427220639p:plain

前回の記事で、LexがAlexaのダイアログモデルと似ているという話をしました。

今回は少し補足的にダイアログモデルと似てるところをもう少し見てみたいと思います。

目次

BookTripサンプルの問題

BookTripサンプルのテストはこんな感じでした。

f:id:kun432:20210430222824p:plain

少し意地悪なテストをしてみましょう。

f:id:kun432:20210430222550p:plain

場所を聞かれた際に自然な感じで回答すると正しく理解できていません。

もう一つ。

f:id:kun432:20210430222610p:plain

日付のところはAlexaと同じようにちゃんと理解していますね。Inspect Responseを見るとちゃんと今日を起点に「来週の火曜日」というのを日付で認識できていることがわかります。

f:id:kun432:20210430222623p:plain

だめなのは年齢のところです。「四十歳」のように「歳」がつくと正しく認識できていません。(漢数字だけならちゃんと認識します)

つまり、個々のスロット収集時のサンプル発話が正しく設定されていないということですね。

スロット収集時のサンプル発話の修正

ではこれを修正します。Slotsの各スロットの設定の右にある歯車アイコンをクリックします。まずは都市名の部分から。

f:id:kun432:20210430123225p:plain

スロットの設定画面が表示されます。ここにCorrresponding uttterancesというのがありますが、これがスロット収集時のサンプル発話になります。デフォルトだと何も入ってないので、{PickUpCity}だけが入っているのと等価だということですね。

f:id:kun432:20210430123852p:plain

なので、ここにサンプル発話をいくつか追加してあげます。あと、Promptsがスロット収集時のLexの発話になります。正しく収集できない場合はMaximum numbers of retriesにある2回まで聞き直します(Alexaと同じですね)。ユーザが正しい言い方を認識しやすいように、Promptsで説明してあげるのもアリだと思います。設定したら「Save」をクリックします。

f:id:kun432:20210430124441p:plain

同様にして年齢のところも変更してあげましょう。

f:id:kun432:20210430124646p:plain

ではもう一度テストしてみます。テストの前に「Save Intent」して「Build」することをお忘れなく。

f:id:kun432:20210430125045p:plain

うまくいきました!ちなみにスロットだけの発話(前後のキャリアフレーズがなくて、{PickUpCity}だけが入っているようなケース)は明示しなくてもOKのようです。

インテントにマッチしない場合

最初の例は、スロット収集時のサンプル発話にマッチしない場合でした。ではそもそもインテントにマッチしない場合の設定(AlexaでいうFallbackIntentのようなもの)はどこで行うのでしょうか?Lexでは、左のメニューにある"Error Handling"で設定を行います。

f:id:kun432:20210430224055p:plain

Clarification promptsがどのインテントにもマッチしない場合に、ユーザに再度発話を促すためのLexのプロンプトです。ここにもMaximum number of retriesがありますね。つまり、2回まで再度確認して、それでもマッチしないようであれば、Hang-up phraseで最後のメッセージを発話してセッションが切れる、というような感じになっているようですね。

f:id:kun432:20210430223024p:plain

まとめ

Alexaでダイアログモデルを使ったことがある方なら、とてもスッキリ頭に入ってくるのではないでしょうか。やっぱりとても似ていますね。

もう少しいろいろやってみます。