するめごはんのIT日記

主にITネタを書いていくのさ

Echo Show、Echo Spotの電源の切り方

ごきげんよう

この記事は ADVENTARの「するめごはんのVUI・スマートスピーカー Advent Calendar 2018」 の14日目の記事です。

今回は明らかに小ネタです。

Echo Spotの時もハマったのですが、Echo Showで再度それをやることになりました。

けど、僕は完全に忘れていてハマったので記事にすることにしました。

電源の切り方がわからない

Echo Showのスタンドも買ってあって、それが昨日届きました。 これ、角度が結構調整できて大変嬉しいのですが、設置するときに(哲学的に?)壁に当たりました。

説明書が「電源ひっこぬけ」の絵しかない。

f:id:surumegohan:20181214073621j:plain
いきなり引っこ抜く説明書

世界中に配るであろうスタンドの説明書・解説書なので、わざわざ日本語はありません。 そこにいきなり「1」で電源を抜く画像です。 初手から電源を引っこ抜く指示です。

もう動いちゃってます。

ところが、Echo Showの画面上部から下に向かって設定メニューを表示して、さ迷いましたが電源の切り方がない。 もちろん電源ボタンもないです。

で、もしかしたらと・・・

ミュートボタンを長押ししたら電源が切れることがわかりました。

f:id:surumegohan:20181214073504j:plain
ミュートボタン長押しで電源を切る

Echo Spotでも同じです

ミュートボタンを長押しです。 ※ ミクさんが写りこんでいるのは僕の家の仕様です

DSC_1577.JPG

さすがにEcho Spot、Echo Showで電源で何かあるのが怖い

それなりのお値段がするので、こんなことで何かあったら嫌なので、改めて記事にしました。

Dotはぶっこぬいてます。。。

以上、小ネタでした。

EchoShowでAPLを使って告白される

ごきげんよう

この記事は ADVENTARの「するめごはんのVUI・スマートスピーカー Advent Calendar 2018」 の13日目の記事です。

ついにきましたね! Echo Show!

というわけで、さっそく試してみるわけです。

でかい

比較のために500mlの空き缶を添えてますがでかいです。

f:id:surumegohan:20181213154055j:plain
開封の儀

動画とかすごい

映画を観ると特に感じますが、ブラウン管かよってレベルの大きさであるだけ音がすごい良いです。 これはもう今後Echo ShowでPrime VideoやYoutubeを観ます。

スキルたちがより見やすくなる

■キャプテン九九

f:id:surumegohan:20181213154132j:plain
キャプテン九九

乙葉の時間

f:id:surumegohan:20181213154222j:plain
乙葉の時間

■ヒロインの告白

f:id:surumegohan:20181213154254j:plain
ヒロインの告白

ヒロインの告白をAPL対応にしてみる

結果、テストバージョンでこうなってます。

f:id:surumegohan:20181213154333j:plain
ヒロインの告白をAPLにしてみる

環境構築から実装まで、偉大なる @zono_0 さんが既にわかりやすい記事を掲載しています。 いつもありがとうございます。

qiita.com

上記、記事ではask cliが前提ですが、そうでなくてもAPLはもちろん使えます。 同じところは省いて、そうでないところを今回は補完していきます。

1.Amazon Developer Portal

僕は今回開いた際にに右下にデバイス検出エラーがでました。

f:id:surumegohan:20181213154445p:plain
バイス検出エラー

が、 今回は 気にしないで大丈夫です。 JSONのフォーマットが欲しいので。

ちなみに、ここで対象のデバイスの大きさを以下のように選べます。

f:id:surumegohan:20181213154519p:plain
小型ハブと中型ハブ

f:id:surumegohan:20181213154612p:plain
大型ハブと超大型TC

2.Lambdaでインラインエディタでも組める

ask cliは僕も使います。 というより、慣れるとそちらの方が開発は大変良いです。 コード管理とかもできますし。

かといって、ask cliの導入に躓く方もいらっしゃるとも思いますので、インラインでがんばってみました。

@zono_0 さんのように、APLのJSONは分割した方が良いと思います。

f:id:surumegohan:20181213154649p:plain
Lambdaのインラインエディタでがんばる

3.ソースコード

とりあえず起動して動いた段階のソースコードです。

■APLでのヒロインの告白のLaunchRequest

'use strict';

const Alexa = require('ask-sdk');


//起動時
const kotoha_smartmacchiato = '<audio src=\"https://s3XXXXXXXXXXXXXX.mp3\" />';

const kotoha_voice = '<audio src=\"https://s3-XXXXXXXXXXXXXX.mp3\" />';


const LaunchRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
  },
  async handle(handlerInput) {

    // ディスプレイ有り(APL対応)の場合
    if (supportsApl(handlerInput)) {
      // APL対応(documentに設定したテンプレートレイアウトを利用し、datasourcesの内容をディスプレイに表示します。)
      handlerInput.responseBuilder
        .addDirective({
          type : 'Alexa.Presentation.APL.RenderDocument',
          version: '1.0',
          document: require('./homepage.json'),
          datasources: require('./data.json')
        });
    }

    const speechText = kotoha_smartmacchiato + kotoha_voice;
    
    return handlerInput.responseBuilder
      .speak('<speak>' + speechText + '</speak>')
      .reprompt('<speak>' + speechText + '</speak>')
      .withShouldEndSession(false)
      .getResponse();

  }
};


/**
 * ディスプレイサポート(APL対応)判定値
 * @author zono_0    いつもありがとうございます!
 */ 
const supportsApl = (handlerInput) => {
  const hasDisplay =
    handlerInput.requestEnvelope.context &&
    handlerInput.requestEnvelope.context.System &&
    handlerInput.requestEnvelope.context.System.device &&
    handlerInput.requestEnvelope.context.System.device.supportedInterfaces &&
    handlerInput.requestEnvelope.context.System.device.supportedInterfaces['Alexa.Presentation.APL'];

  return hasDisplay;
};

/**
 * Echo Spotで使っていたディスプレイかどうかを判定するfunction
   Echo Showでも引き続き使えますが、今回はAPLを使います
*/
function supportsDisplay(handlerInput) {
  var hasDisplay =
    handlerInput.requestEnvelope.context &&
    handlerInput.requestEnvelope.context.System &&
    handlerInput.requestEnvelope.context.System.device &&
    handlerInput.requestEnvelope.context.System.device.supportedInterfaces &&
    handlerInput.requestEnvelope.context.System.device.supportedInterfaces.Display;

  console.log("Supported Interfaces are" + JSON.stringify(handlerInput.requestEnvelope.context.System.device.supportedInterfaces));
  return hasDisplay;
}

//動かす
exports.handler = Alexa.SkillBuilders.standard()
  .addRequestHandlers(LaunchRequestHandler)
  .lambda();

■homepage.json

{
        "type": "APL",
        "version": "1.0",
        "theme": "dark",
        "import": [
            {
                "name": "alexa-layouts",
                "version": "1.0.0"
            }
        ],
        "resources": [
            {
                "description": "Stock color for the light theme",
                "colors": {
                    "colorTextPrimary": "#151920"
                }
            },
            {
                "description": "Stock color for the dark theme",
                "when": "${viewport.theme == 'dark'}",
                "colors": {
                    "colorTextPrimary": "#f0f1ef"
                }
            },
            {
                "description": "Standard font sizes",
                "dimensions": {
                    "textSizeBody": 48,
                    "textSizePrimary": 27,
                    "textSizeSecondary": 23,
                    "textSizeSecondaryHint": 25
                }
            },
            {
                "description": "Common spacing values",
                "dimensions": {
                    "spacingThin": 6,
                    "spacingSmall": 12,
                    "spacingMedium": 24,
                    "spacingLarge": 48,
                    "spacingExtraLarge": 72
                }
            },
            {
                "description": "Common margins and padding",
                "dimensions": {
                    "marginTop": 40,
                    "marginLeft": 60,
                    "marginRight": 60,
                    "marginBottom": 40
                }
            }
        ],
        "styles": {
            "textStyleBase": {
                "description": "Base font description; set color and core font family",
                "values": [
                    {
                        "color": "@colorTextPrimary",
                        "fontFamily": "Amazon Ember"
                    }
                ]
            },
            "textStyleBase0": {
                "description": "Thin version of basic font",
                "extend": "textStyleBase",
                "values": {
                    "fontWeight": "100"
                }
            },
            "textStyleBase1": {
                "description": "Light version of basic font",
                "extend": "textStyleBase",
                "values": {
                    "fontWeight": "300"
                }
            },
            "mixinBody": {
                "values": {
                    "fontSize": "@textSizeBody"
                }
            },
            "mixinPrimary": {
                "values": {
                    "fontSize": "@textSizePrimary"
                }
            },
            "mixinSecondary": {
                "values": {
                    "fontSize": "@textSizeSecondary"
                }
            },
            "textStylePrimary": {
                "extend": [
                    "textStyleBase1",
                    "mixinPrimary"
                ]
            },
            "textStyleSecondary": {
                "extend": [
                    "textStyleBase0",
                    "mixinSecondary"
                ]
            },
            "textStyleBody": {
                "extend": [
                    "textStyleBase1",
                    "mixinBody"
                ]
            },
            "textStyleSecondaryHint": {
                "values": {
                    "fontFamily": "Bookerly",
                    "fontStyle": "italic",
                    "fontSize": "@textSizeSecondaryHint",
                    "color": "@colorTextPrimary"
                }
            }
        },
        "layouts": {},
        "mainTemplate": {
            "parameters": [
                "payload"
            ],
            "items": [
                {
                    "when": "${viewport.shape == 'round'}",
                    "type": "Container",
                    "direction": "column",
                    "width": "100vw",
                    "height": "100vh",
                    "items": [
                        {
                            "type": "Image",
                            "source": "${payload.bodyTemplate3Data.image.sources[0].url}",
                            "scale": "best-fill",
                            "width": "100vw",
                            "height": "100vh",
                            "position": "absolute",
                            "overlayColor": "rgba(0, 0, 0, 0.6)"
                        },
                        {
                            "type": "ScrollView",
                            "width": "100vw",
                            "height": "100vh",
                            "item": [
                                {
                                    "type": "Container",
                                    "direction": "column",
                                    "alignItems": "center",
                                    "paddingLeft": 30,
                                    "paddingRight": 30,
                                    "paddingBottom": 200,
                                    "items": [
                                        {
                                            "type": "AlexaHeader",
                                            "headerAttributionImage": "${payload.bodyTemplate3Data.logoUrl}",
                                            "headerTitle": "${payload.bodyTemplate3Data.title}"
                                        },
                                        {
                                            "type": "Text",
                                            "text": "<b>告白と言えば</b> | <b>やはり学校ですよね</b>",
                                            "style": "textStylePrimary",
                                            "color": "#4dd2ff",
                                            "width": "90vw",
                                            "textAlign": "center"
                                        },
                                        {
                                            "type": "Text",
                                            "text": "<b>${payload.bodyTemplate3Data.textContent.title.text}</b>",
                                            "style": "textStyleBody",
                                            "width": "90vw",
                                            "textAlign": "center"
                                        },
                                        {
                                            "type": "Text",
                                            "text": "${payload.bodyTemplate3Data.textContent.subtitle.text}",
                                            "style": "textStylePrimary",
                                            "width": "90vw",
                                            "textAlign": "center"
                                        },
                                        {
                                            "type": "Text",
                                            "text": "${payload.bodyTemplate3Data.textContent.primaryText.text}",
                                            "paddingTop": 40,
                                            "style": "textStylePrimary",
                                            "width": "90vw",
                                            "textAlign": "center"
                                        },
                                        {
                                            "type": "Text",
                                            "text": "${payload.bodyTemplate3Data.textContent.bulletPoint.text}",
                                            "paddingTop": 50,
                                            "style": "textStylePrimary",
                                            "width": "90vw",
                                            "textAlign": "center"
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                },
                {
                    "type": "Container",
                    "width": "100vw",
                    "height": "100vh",
                    "items": [
                        {
                            "type": "Image",
                            "source": "${payload.bodyTemplate3Data.backgroundImage.sources[0].url}",
                            "scale": "best-fill",
                            "width": "100vw",
                            "height": "100vh",
                            "position": "absolute"
                        },
                        {
                            "type": "AlexaHeader",
                            "headerTitle": "${payload.bodyTemplate3Data.title}",
                            "headerAttributionImage": "${payload.bodyTemplate3Data.logoUrl}"
                        },
                        {
                            "type": "Container",
                            "direction": "row",
                            "paddingLeft": 40,
                            "paddingRight": 72,
                            "grow": 1,
                            "items": [
                                {
                                    "type": "Image",
                                    "source": "${payload.bodyTemplate3Data.image.sources[0].url}",
                                    "width": 340,
                                    "height": 360,
                                    "scale": "best-fit",
                                    "align": "center"
                                },
                                {
                                    "type": "ScrollView",
                                    "height": "60vh",
                                    "shrink": 1,
                                    "item": [
                                        {
                                            "type": "Container",
                                            "items": [
                                                {
                                                    "type": "Text",
                                                    "text": "<b>やはり告白と言えば学校ですよね</b>",
                                                    "style": "textStylePrimary",
                                                    "color": "#4dd2ff"
                                                },
                                                {
                                                    "type": "Text",
                                                    "text": "<b>${payload.bodyTemplate3Data.textContent.title.text}</b>",
                                                    "style": "textStyleBody"
                                                },
                                                {
                                                    "type": "Text",
                                                    "text": "${payload.bodyTemplate3Data.textContent.subtitle.text}",
                                                    "style": "textStylePrimary"
                                                },
                                                {
                                                    "type": "Text",
                                                    "text": "${payload.bodyTemplate3Data.textContent.primaryText.text}",
                                                    "paddingTop": 40,
                                                    "style": "textStylePrimary"
                                                },
                                                {
                                                    "type": "Text",
                                                    "text": "${payload.bodyTemplate3Data.textContent.bulletPoint.text}",
                                                    "paddingTop": 50,
                                                    "style": "textStylePrimary"
                                                }
                                            ]
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                }
            ]
        }
}

■data.json

{
        "bodyTemplate3Data": {
            "type": "object",
            "objectId": "bt3Sample",
            "backgroundImage": {
                "contentDescription": null,
                "smallSourceUrl": null,
                "largeSourceUrl": null,
                "sources": [
                    {
                        "url": "https://s3-学校画像.png",
                        "size": "small",
                        "widthPixels": 0,
                        "heightPixels": 0
                    },
                    {
                        "url": "https://s3-学校画像.png",
                        "size": "large",
                        "widthPixels": 0,
                        "heightPixels": 0
                    }
                ]
            },
            "title": "APLを使って Echo Show で学校で(?)告白されよう!",
            "image": {
                "contentDescription": null,
                "smallSourceUrl": null,
                "largeSourceUrl": null,
                "sources": [
                    {
                        "url": "https://s3-結城琴葉画像.png",
                        "size": "small",
                        "widthPixels": 0,
                        "heightPixels": 0
                    },
                    {
                        "url": "https://s3-結城琴葉画像.png",
                        "size": "large",
                        "widthPixels": 0,
                        "heightPixels": 0
                    }
                ]
            },
            "textContent": {
                "title": {
                    "type": "PlainText",
                    "text": " ヒロイン(結城琴葉)の告白"
                },
                "subtitle": {
                    "type": "PlainText",
                    "text": " 告白メッセージ"
                },
                "primaryText": {
                    "type": "PlainText",
                    "text": " 私、あなたが好きです。世界中の誰よりも・・あなたのことが、本当に好きなんです!私と・・付き合ってください. "
                },
                "bulletPoint": {
                    "type": "PlainText",
                    "text": " 結城琴葉ちゃんから告白されてみよう! "
                }
            },
            "logoUrl": "https://s3-スキルのロゴ.png",
            "hintText": "アレクサ、「ヒロインの告白」を開いて"
        }
    }

まとめ

ask cliで躓いても、なんとかなります。 現時点では僕はこれが実機で動いたレベルですが、APLを使いこなせるようになるとそれだけで職業になる気がします。

以上

スキル作成時のアイコンや音楽の素材サイトおよびツール類

ごきげんよう

この記事は ADVENTARの「するめごはんのVUI・スマートスピーカー Advent Calendar 2018」 の12日目の記事です。

今日はAmazon Echo Showが届くはずなので、その記事を書くつもりでしたが、よくよく考えてみたら何時にくるかわからない。

ですので、別の日に予定していた記事として、僕がVUIのスキル・アプリを作成する時に利用検討する素材サイトを紹介します。

先日、某所のVUIイベントで 「Alexaのスキルは様々なツールで作れるようになってきてるけど、アイコンや効果音はみなさんどうしてますか?」 という話がありまして。

そういえばVUI業界の方々がどういう素材を用いているのか共有されていることを僕は認識していないので、僕が今までスキル作成をしていた時にたどり着いた素材サイトについて貼っておきます。

ライセンスが緩い素材サイト

以下に記載するサイトは著作権などのライセンスが比較的ゆるいサイトです。 ただ、もちろん利用規約はそれぞれのサイトに記載されておりますので、各自判断・自己責任でご利用ください。

1.アイコン

スキル作成時に地味に悩むのがアイコンです。

僕の中では以下の3点が利用しやすいと考えています。

■Alexaのicon-builder

developer.amazon.com

■ICOOON MONO

icooon-mono.com

ICOOON MONOさんはめっちゃおススメです。

■いらすとや

www.irasutoya.com

たまに「使い放題」と認識している方がおりますが 20点以上の利用など、制限が実はありますのでご注意を。

2.ジングル・効果音

VUIなので効果音が欲しい方はいらっしゃると思います。

ちなみに僕のスキル「ヒロインの告白」で起動時に 「しゃらららーーんスマートマキアート」 と、流れますが、あのような効果音を「ジングル」と呼びます。

ですので「効果音」「フリー素材」等でぐぐったりする方も多いと思われますが、「ジングル」を加えると、よりよいかもしれません。

以下が、僕がたどり着いた主なサイトです。

■ポケットサウンド

ご利用規約 – ポケットサウンド – フリー効果音素材・BGMダウンロード

■TURBO X

SOUND EFFECT| 商用利用可能なフリー効果音素材「TURBO X」

■DOVA-SYNDROME

dova-s.jp

■Music is VFR

利用規約|Music is VFR

■魔王魂

maoudamashii.jokersounds.com

3.アイコン画像のサイズ変更

イコン画像を拾ってきたとして、そのサイズ変更はどうしてるの?

との話もあったので僕が使っているツールを記載しておきます。

最近僕がwindowsで使っているのはリサイズ超簡単!Proです。

以下のように、サイズ変更したい画像を変換ファイルリストにいれて、 あとは縦横指定の箇所にAlexaの512×512と108×108を指定しておけば、覚えてくれるのでラジオボタンをポチって、「PNG」で「変換開始」でおしまいです。

本当に超簡単。

f:id:surumegohan:20181212104851p:plain
リサイズ超簡単Pro

4.音楽編集ソフト

音声ファイルの編集はAudacityを使うことが多いです。直感的にわかりやすいです。

f:id:surumegohan:20181212104916p:plain
Audacity

5.マイク

自分の声をVUIのスキル・アプリで流す!となるとマイクが必要になるわけですが、僕は以下です。

SONYさんのエレクトレットコンデンサーマイクロホン

SONY エレクトレットコンデンサーマイクロホン

3000円くらいでいい感じに使えます。 本気声優でもない素人が使う分には十分かと思います。

6.オーディオファイルの変換

Alexaなどプラットフォームに対応したオーディオファイルに変換するならffmpegを使っています。

以下に僕がQiitaに書いた記事があるので適宜ご参照ください。

Windows環境でffmpegを使ってmp3ファイルをAlexa対応形式に複数ファイル一括変換するメモ https://qiita.com/surumegohan/items/63a1b4e9fe404545ed06

まとめ

僕は上記に記載していた素材やツール等を使っていますが、VUIのスキル・アプリ作成をなさっている方で 「自分はここを使うよ!」 という素材やツールがあればコメントもらえると嬉しいです。

以上です。

Node-REDを使ってdialogflowで挨拶する

ごきげんよう

この記事は ADVENTARの 「するめごはんのVUI・スマートスピーカー Advent Calendar 2018」 の11日目の記事です。

昨日はNode-REDを用いてAlexaでの挨拶スキルについて記載しました。

surumegohan.hatenablog.com

今度は同じくNode-Redを使ってdialogflow(要するにGoogle Home)の挨拶アプリを作ってみます。

今回も同じく以下の本に一部従ってみます。 もちろん、そのまま転記してはいけないので、内容は適宜変更、追加してます。

スマートスピーカーアプリ開発入門 3大スマートスピーカー Amazon Echo Google Home LINE Clova対応

作ってみる

1.昨日Alexaスキルを作っているので、まずはIBM Cloudのページに行きます。

www.ibm.com

そして、画面左上の人型マークからサインインします。

f:id:surumegohan:20181211151923p:plain
IBM Cloudの画面

2.サインインをする

2回目以降の利用ユーザーならこの画面になるはず。

f:id:surumegohan:20181211151945p:plain
既存ユーザーのサインイン

3.「起動」を押してダッシュボードを開く

上記画像の「起動」を押してダッシュボードを開きます。

前回のAlexaスキルの情報が表示されています。

f:id:surumegohan:20181211152008p:plain
ダッシュボード

4.過去に作成したインスタンスを削除

liteプランだと無料であるかわりに、インスタンスが1つしか作成できません。 そのため、liteプランを続けるならば作成済みのインスタンスを削除します。

f:id:surumegohan:20181211152032p:plain
liteプランは1つのみしか作成できないので削除

5.インスタンスを再作成してNode-REDを検索

新しいインスタンスを「作成」したら、node-red で検索するとフィルタリングされて表示されるので選択します。

liteプランを継続している場合は、lite:ライトが既にフィルターにかかっているので、その後ろに node-red と入れてあげればOKです。

f:id:surumegohan:20181211152112p:plain
node-redでフィルタリングすると楽

6.あとは画面をポチポチ

Node-REDの画面が開いたら、画面をポチポチします。

HTTPSでPOST
・templeteで fulfillmentText をキーとした挨拶文を入れる
・httpResponse

あとは、この3つを線でつないであげます。

f:id:surumegohan:20181211152219p:plain
線でつなぐ

6.DialogFlowに設定すればおしまい

あとはDialogFlowで、プロジェクトを作り、Webhookに設定してあげれば、画面右のように動いてくれます。

f:id:surumegohan:20181211152242p:plain
DialogFlowでWebhookに設定

まとめ

DialogflowでWebhookとして指定できるので、その先がNode-REDになります。

簡単な挨拶のようなスキルはプログラミングなしに画面ポチポチだけでいけます。 JSONを書くところがプログラミングに該当するなら、そこはプログラミングですが・・

ともあれ、すごく簡単にできるのでNode-REDを使ってGoogle Homeでやりとりできるようなアプリを作ってみてはいかがでしょうか。

以上です。

Node-REDを使ってAlexaスキルを創る

ごきげんよう

この記事は ADVENTARの 「するめごはんのVUI・スマートスピーカー Advent Calendar 2018」 の10日目の記事です。

以前、Alexaスキルを作るツールとしてNOIDについてこのアドベントカレンダーで触れました

surumegohan.hatenablog.com

今回は同じくツールとしてNode-REDを使って挨拶スキルを作ります。

作り方

作り方と言っても、Qiitaや個人ブログ等に情報はあふれていると思います。

今回は以下の本に従ってみます。

f:id:surumegohan:20181210144941p:plain
スマートスピーカーアプリ開発入門

スマートスピーカーアプリ開発入門 3大スマートスピーカー Amazon Echo Google Home LINE Clova対応

1.まずはNode-REDのアカウントを作成

https://nodered.org/

f:id:surumegohan:20181210145035p:plain
Node-REDの画面

2.次にIBM Cloudのアカウント作成

https://www.ibm.com/cloud-computing/jp/ja/lite-account/

ライトで大丈夫です。

f:id:surumegohan:20181210145059p:plain
IBM Cloudの画面

3.IBM Cloudにログイン

ここで、アカウントIDはメールアドレスでログインできます。

f:id:surumegohan:20181210145122p:plain
ログイン画面

4.何も考えないと一部英語になる

本の通りに進めていけば問題ないですが、何も考えないでポチポチやっていくと、画面の一部が英語になります。

が、特に躓くような英単語はないと思います。

f:id:surumegohan:20181210145146p:plain
一部英語になる

5.作成できる画面

商業誌の内容をそのままは書けないのですが、以下のような画面になります。

話しかけられたら、インテントの種類を判別して起動時はLaunchRequestなのでそのまま挨拶をする。 それ以外はインテントの場合分けをswitchをかませて対応を分岐させ、応答を返したらHttpのResponseにつないであげます。

f:id:surumegohan:20181210145205p:plain
Node-REDで作成した最終的な画面

6.Alexa Consoleもほぼ通常通り

Node-REDで作成したインテント名で、挨拶のためのインテントを作成してあげます。

f:id:surumegohan:20181210145316p:plain
Alexa Console画面

ただし、エンドポイントはLambdaではないので、Node-REDで作成したURLをWebhookとして指定します。

f:id:surumegohan:20181210145340p:plain
エンドポイントを設定すること

7.後は話しかけるだけ

きちんと動きます。

f:id:surumegohan:20181210145407p:plain
テスト画面

もし、「応答に時間がかかっている」というエラーになった場合は、Node-REDの画面で、線がすべて結びきれているか確認してみましょう。 JSONの作成はできていても、レスポンスを返すようになっていない場合があります。

まとめ

Node-REDでもスキル作成はできますし、本を参考にすれば、Node-REDもAlexaも初体験だったとしても、ぜいぜい2~3時間で作成できると思います。

Amazonアカウント作成で躓く場合は別

Alexaのスキルをつくることに特化するならNOIDの方が断然楽だと思います。 ただ、Webhookとして、もっと汎用的にいろいろなことをやりたいとなると、Node-REDは選択肢の1つにはなるのではないでしょうか。

今回は以上です。

AMAZON.SearchQueryでビルドが通らない場合のTips

ごきげんよう

この記事は ADVENTARの 「するめごはんのVUI・スマートスピーカー Advent Calendar 2018」 の9日目の記事です。

みなさん、SearchQuery使ってますか?

これを使うとユーザーから何かしら話しかけられ、他のIntentに分類できなかった場合、 このSearchQueryが実装されているIntentに割り振られます。

つまり、自由な単語・発話をSearchQueryに対応させたいという発想になりますが、ビルドが通らない場合あります。 今回はそれを 無理やり通す 方法について記載します。

もちろん、各自の自己責任にて。

一般的?の実装方法

SearchQueryはこの「なんでも該当」という強さから、一般的に他の単語と組み合わせて実装します。

例えば以下のように。

●●●と、サーチクエリ と話しかけれた際の●●の部分に用います。

f:id:surumegohan:20181209084327p:plain
通常の利用方法

ビルドに通らない場合

ただ、上記のような、「●●とサーチクエリ」のようにやりたくはなく、SearchQueryのみで実装したい場合、ビルドが通らない場合があります。

f:id:surumegohan:20181209084349p:plain
SearchQureyのみだとビルドエラーが発生する

エラーメッセージは

Sample utterance "{searchQuerySlot} " in intent "SearchQueryIntent" must include a carrier phrase. Sample intent utterances with phrase types cannot consist of only slots.エラーコード: MissingCarrierPhraseWithPhraseSlot

自由度が高すぎて制御されているのではないかと考えています。 なので通常は他の具体的な話しかけ方と組みあわせて使うべきなのでしょう。

それでもビルドしたい場合

他のフレーズは一切いれず、何がなんでもSearchQueryのみ実装して、想定していないユーザーの発話はすべてここに集約したいという場合も存在します。

その場合、以下のようにするとビルドに通ります。

f:id:surumegohan:20181209084431p:plain
ビルドを通す技

さて、どうやったでしょうか。

答えは 半角スペース + SearchQuery です。

こうすると他のIntentやSlotに割り振られない場合、このように作成したSearchQueryに無理やり分類させることができます。

ご利用は自己責任で

おそらくこれは、不具合に近いバグのような気がします。 仕様といえば仕様なのでしょうが。。。

ともあれ、ユーザーかどのように話しかけられたのかを丸々取得したい等の場合は、このような技で切り抜けることが一応できます。

実際には他のフレーズを組み合わせるべきだとは思いますが、このようなやり方も可能ではあるということを記載してみました。

本日は以上です。

技術書典5にて「スマートスピーカーを遊びたおす本」を執筆し、反響があった話

ごきげんよう

この記事は ADVENTARの 「するめごはんのVUI・スマートスピーカー Advent Calendar 2018」 の8日目の記事です。

今回は技術書典5にむけて 「スマートスピーカーを遊びたおす本」 を執筆・編集し、頒布後に反響があった話について記載します。

f:id:surumegohan:20181208102943j:plain
スマートスピーカーを遊びたおす本

■当日、僕が頒布した本「スマートスピーカーを遊びたおす本」

この本の概要は以下に記載のクラスメソッド社のDevelopersIOに掲載されているので、そちらをご確認ください。

というわけで、僕が記載した章の技術的な話。

本の内容の技術的な話

僕の担当箇所はたまたま1章になりましたが、Echo Spot対応のスキル「ヒロインの告白」の制作プロセスの話です。

本の内容をすべてブログに載せてしまうと、お金を払ってくれた方に申し訳ないので一部のみ記載します。

DynamoDBにputする関数は事前に作成してしまう

僕はDynamoDBにデータを格納する際の形式は自分で決めたい派です。

このアドベントカレンダーの3日目

surumegohan.hatenablog.com

でも取り上げましたが、SDKをいじるくらい自分で操りたい人です。

なので、今ではPut用のfunctionは共通化して作成してしまっています。

//DynamoDBにputする関数
function putDynamo(params) {
    
    console.log("=== putDynamo function ===" + params);
    
    docClient.put(params, function (err, data) {
    
        console.log("=== put ===");
    
        if (err) {
            console.log(err);
        } else {
            console.log(data);
        }
    });
}

2回目以降の起動ユーザーは説明文を省くように実装する

上記のDynamoDBへのPutのfunctionをもちいて、LaunchRequestでの2回目以降の起動ユーザーは、スキルの説明をカットして、告白しても良いかどうかのみを結城琴葉ちゃんが話しかけるようにしています。

これはVUIのデザイン・設計として非常に重要だと僕は考えていて、2回目以降のユーザーは説明文をいちいち聞くのは極めて不快です。

もちろん、スキルの特性にもよりますが、このスキルは告白に対する受け答えをするスキルなので、複雑な操作は不要だと感じています。

また、2回目以降のユーザーでも初回起動時の説明文が流れるように「ヘルプ」では初回起動時のメッセージを流すようにしています。

ちなみに、ここでは余談ですが、僕がスキルを創るときは、ユーザーとVUIアプリケーションの対話の状況によって、「ヘルプ」のメッセージを変更するように実装しています。

// スタート音声
const kotoha_start_01 = '<audio src=\"https://hogehoge/kotoha_start_01.mp3\" />';
const kotoha_start_existuser = '<audio src=\"https://hogehoge/kotoha_start_existuser.mp3\" />';

//中略

const LaunchRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
  },
  async handle(handlerInput) {


    // Amazonから提供されているTemplate 6を使用
    if (supportsDisplay(handlerInput)){
      const myImage1 = new Alexa.ImageHelper()
        .addImageInstance(DisplayImg1.url) // 結城琴葉の画像
        .getImage();

      const myImage2 = new Alexa.ImageHelper()
        .addImageInstance(DisplayImg2.url) // 今回はDisplayImg1.urlと同じ
        .getImage();

      const primaryText = new Alexa.RichTextContentHelper()
        .withPrimaryText('')  // 文字を出力させたくなかったのでこの記載
        .getTextContent();

        handlerInput.responseBuilder.addRenderTemplateDirective({
        type: 'BodyTemplate6',
        token: 'string',
        backButton: 'HIDDEN',
        backgroundImage: myImage2,
        image: myImage1,
        title: "",
        textContent: primaryText
      });
    }
               
    // 起動時メッセージを一旦、2回目以降の起動ユーザー用に代入
    // 初回起動ユーザーで初期化するか迷いましたが、2回目以降のユーザー用に代入
    let kotoha_start = kotoha_start_existuser;

    try{
        
        // DynamoDBにあるテーブルに対して、既に存在するuserIdかどうか検索を実行
        const queryItems = await docClient.query({
          TableName: "kotohaTable", 
          KeyConditionExpression: "#userId = :userId",
          ExpressionAttributeNames: {"#userId": "userId"},
          ExpressionAttributeValues: {":userId": json_userId} // JSONからuserIdを事前取得しておいた値
        }).promise();
                
        try{
            
            // 検索した結果、Items[0]にuserIdが入っているかログ出力を兼ねて確認
            console.log("queryItems.Items[0].userId: " + queryItems.Items[0].userId);
        
        } catch (err){
            
            // 検索した結果がゼロ件ならばログを出力できず、ここのcatchに分岐
            // ここに流れてきた場合は、再生するmp3へのリンクを初回起動のユーザーとして再度代入
            kotoha_start = kotoha_start_01;
            
            console.log("user Nothing");

            // DynamoDBに存在しないuserIdだったため、格納する処理をここから記載
            var item = {
                // JSONから取得しているuserIdをDynamoDBのuserIdとして格納する            
                userId: json_userId
            };

            // DynamoDBに格納する情報をparamsとしてまとめる
            var params = {
                TableName: kotohaTable,
                Item: item
            };

            // DynamoDBに格納する
            // 本来はここでもエラー処理を入れているが割愛
            await putDynamo(params);

        }
        
    } catch(err){
        
        // エラー時の処理を入れているが割愛

    }

    // sessionAttributesに起動後状態に移ったことを示す

    var sessionAttribute = '';
    
    sessionAttribute = {
    "STATE": "after_start"
    };
    
    handlerInput.attributesManager.setSessionAttributes(sessionAttribute); 

    // 起動時の「スマートマキアート」の後に0.7秒の間を開けてから、起動メッセージを流す
    speechText = kotoha_smartmacchiato + '<break time="0.7s"/>' + kotoha_start;

    // 再生する内容はmp3のみであるため、speakもrepromtもspeakタグでくくっておく
    return handlerInput.responseBuilder
      .speak('<speak>' + speechText + '</speak>')
      .reprompt('<speak>' + speechText + '</speak>')
      .withShouldEndSession(false) //セッションを切らないことを明示する
      .getResponse();
  }
};

■各方面での反響

1.委託先のCOMIC ZIN 秋葉原店でド正面に平積み

BOOTHさん、とらのあなさん、ZINさんに売れ残った分を委託しておりますが、特にZINさんの扱いがすごすぎまして、秋葉原店の技術書典コーナーにないので店員さんに確認したら、まさかの入り口から入ってド正面のド真ん中に平積みしてもらっていました。

逆に気づかなかったという。。

f:id:surumegohan:20181208103050p:plain
COMIC ZINさん

2.クラスメソッド社のせーのさんが記事にしてくれた

日ごろ、非常に、大変、ものすごく、お世話になっているクラスメソッド社の清野さん(せーのさん)にDevelopersIOの記事として掲載いただけました。

非常にありがたいことです。

■VUIをトータルに勉強できる本「スマートスピーカーを遊びたおす本」を読んでみた。 #Alexa

dev.classmethod.jp

f:id:surumegohan:20181208103118p:plain
みんな大好きDevelopersIO

3.この本をきっかけに商業誌の執筆依頼が出版社から頂く

この本をきっかけに、某スマートスピーカーおよび関連技術に関する商業誌の執筆依頼がきて、契約しました。

4.イベントでめっちゃ感謝された

先日の某イベントに参加していたら、この本と千代田まどか様(ちょまど)のファンの方から熱烈にお礼を伝えていただきました。

その人はGoogle Homeユーザーのようですが、著者としてもそういった声をいただけるのはうれしい限りです。

■アウトプットはいいぞ

というわけで、アウトプットをし続けていると、いろんな人が自分を観てくれます。 もちろんたまにはディスられます。

それでも、アウトプットをし続けることで、多くの人が応援してくれます。

みなさんもブログやイベントの登壇などでアウトプットをしてみてはいかがでしょうか。

以上です。