Amazon Echoのウェイクワードをカスタマイズすることに成功した(物理)

以前の記事ではAlexaスキルを作成することでココちゃんが応答してくれるようになりました。

romiogaku.hateblo.jp

しかしAmazon Echoで変更できるウェイクワードは、現時点では以下の4つです。

  • Alexa
  • Echo
  • コンピューター
  • Amazon

Amazon Echoを純度100%ココちゃんにするには、ウェイクワードを「ハローココちゃん」にする必要があります。

ここで抜け道があることを発見します。

f:id:romiogaku:20190106232251p:plain

アクションボタンです。

アクションボタンを押すことでウェイクワードを言う必要がなくなります。つまり、「ハローココちゃん」と呼びかけたらアクションボタンが自動で押されるようにすれば良いのです!

f:id:romiogaku:20190106011917p:plain

実現するにあたって、以下が課題となります。

  • どうやってアクションボタンを押すか
  • どうやって音声認識させるか

どうやってアクションボタンを押すか

物理的に押すものが必要になります。

今回はRaspberry Piサーボモータを動かすことにします。「ハローココちゃん」という音声を認識したらモータを動かしてアクションボタンを押すという流れにします。

どうやって音声認識させるか

ググってみると音声認識の選択肢としては以下がありました。

Julius

julius.osdn.jp

フリーの音声認識ソフトウェアです。

本体とは別に音響モデル・言語モデルが入った基本的なディクテーションキットが配布されており、以下のように実行できます。

$ julius -C ~/julius/julius-kit/dictation-kit-v4.4/main.jconf -C ~/julius/julius-kit/dictation-kit-v4.4/am-gmm.jconf

f:id:romiogaku:20190106233427p:plain

「ハローココちゃん」と呼びかけてますが、うまく認識ししてくれません。

認識率を上げるために独自辞書を作成し実行してもみましたが、関係ない音声も認識してしまいました。

良いやり方があると思うのですが、脇道に逸れてしまいそうだったので断念しました。

音声認識 API

qiita.com

GoogleやMSなどが提供している音声認識APIです。

最初は無料だけど基本的にお金がかかります。認識精度に関しては非常に高いです。

試しにGoogleCloud Speech-to-TextAPIを利用してマイク音声を認識させてみました。

golangでの実装例はこちらが参考になりました。というかこれをそのまま動かしました。

go-talks.appspot.com

良い感じでしたが、今回のケースでは常時起動させておく必要があり、すぐ無料枠を超えてしまいそうです。実際そういう方も居るようでした。

blog.be-dama.com

Web Speech API

ブラウザで動作する「音声合成」と「音声認識」機能です。現在Chromeのみ対応してます。 Googleのデモページはこちらです。

www.google.com

初回だけマイク許可する必要がありますが、内部ではGoogle音声認識APIを実装しているらしく精度は非常に高いです。

無料だしJSでさくっとできそうなので今回はこちらを採用することにしました。

用意したもの

物理的に必要なものを買い揃えました。

ラズパイはBTC採掘を夢見て5年程前に購入したものが眠ってましたが動きませんでしたので、マルツの新春セールで最後の一個を買いました。3980円なり。

ラズパイOSインストール

公式に従ってインストールします。

https://projects.raspberrypi.org/en/projects/raspberry-pi-getting-started/2

SDカードフォーマットはこちらを利用します。

www.sdcard.org

USBマイクの優先度変更

デフォルトだと標準のオーディオ出力端子が優先されてしまうので、この順番を変更する必要があります。

順番は以下で確認できます。

$ cat /proc/asound/modules
0 snd_bcm2835
1 snd_usb_audio

変更するためには /lib/modprobe.d/aliases.conf を編集します。

options snd_usb_audio index=0
options snd_bcm2835 index=1
options snd slots=snd_usb_audio,snd_bcm2835

オリジナルのファイルには snd-usb-audio と書かれているのですが、これはタイポだそうで正解はsnd_usb_audio(アンスコ繋ぎ)です。自分はこれで時間を費やしました・・・

参考:

raspberrypi.stackexchange.com

編集後再起動したら

$ cat /proc/asound/modules
0 snd_usb_audio
1 snd_bcm2835

となっていることを確認します。

サーボモータ接続

f:id:romiogaku:20190107205450p:plain

3.3V電源、GND、GPIO18(ピン番号12)をサーボモータの各ケーブルと接続します。SG90のデータシートはこちら

f:id:romiogaku:20190107210018j:plain
バランス悪い

サーボモータをプログラムから動かす

ラズパイで電子工作のプログラミングといえばpythonが多い印象ですが、調べてみるとGobotというロボティクスフレームワークがあるっぽいのでこちらを使いました。(普段Go書くことが多いので...

Gobotにはサーバ機能があります。API化することで特定のエンドポイントにリクエストが来たらサーボモータを動かす、なんてことができます。今回の目的にぴったりです。

github.com

func main() {
    master := gobot.NewMaster()
    adaptor := raspi.NewAdaptor()
    servo := gpio.NewServoDriver(adaptor, "12") //GPIO(18)

    work := func() {
        // 13 ~ 41 = 0度 ~ 180度
        servo.Move(uint8(27)) //center
        gobot.After(1*time.Second, func() {
            servo.Move(uint8(35))
            gobot.After(500*time.Millisecond, func() {
                servo.Move(uint8(27))
            })
        })
    }

    robot := gobot.NewRobot("servoBot",
        []gobot.Connection{adaptor},
        []gobot.Device{servo},
        work,
    )

    server := api.NewAPI(master)
    server.AddHandler(func(w http.ResponseWriter, req *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
        w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
    })
    server.Port = "3000"
    server.Start()

    servoEndpoint := master.AddRobot(robot)
    servoEndpoint.AddCommand("move", func(params map[string]interface{}) interface{} {
        reqParam := fmt.Sprintf("Params: %+v\n", params)
        robot.Start()
        return reqParam
    })

    master.Start()
}

JSから呼ぶのでCORSの設定もしています。

http://localhost:3000/api/robots/servoBot/commands/move にリクエストを送ればサーボモータが動きます!

f:id:romiogaku:20190107203916g:plain
キュイキュイいってる

Web Speech API

サンプルコードはこちらが参考になりました。

https://w3c.github.io/speech-api/#examples-recognition

以下のように実装しました。 github.com

const recognition = new webkitSpeechRecognition();
recognition.continuous = true;
recognition.lang = "ja";
recognition.onresult = event => {
  for (let i = event.resultIndex; i < event.results.length; ++i) {
    if (event.results[i].isFinal) {
      const word = hiraganaToKatakana(
        removeSpace(event.results[i][0].transcript)
      );
      console.log(word);
      if (word.match(/ハローココチャン/)) {
        axios({
          method: "get",
          url: "http://localhost:3000/api/robots/servoBot/commands/move",
          headers: { "Content-Type": "application/json" }
        })
          .then(function(response) {
            console.log(response);
          })
          .catch(function(error) {
            console.log(error);
          });
      }
    }
  }
};

onresult で認識結果を取得します。「ハローココちゃん」にマッチしたらhttp://localhost:3000/api/robots/servoBot/commands/move にリクエストを送っているだけです。

こちらはserve コマンドかなんかでサーバを立ち上げてアクセスし、事前にマイク許可しておきます。

サーボモータを固定する

ココちゃんをぐるぐる巻きにします。

f:id:romiogaku:20190107211437j:plain
急に雑になる

完成!

youtu.be

最後に

まさか本格的IoTがこんなことになろうとは自分でもびっくりです。

ラズパイ初心者ということもあって結構時間かかった割に電子工作の知識はほとんど吸収できなかった気がします。でも普段Web系ばかりなので良い刺激になりました。組み込み系のひとすごいなぁ・・・

WebSpeechAPIに関してですが、こういう使い方(常時付けっぱなし)は想定してないと思うので、そのうち制限されるんじゃないかなと思います。ブラウザリロードするだけで延々と使い続けられるというのは流石に抜け道臭いです。

あとAlexaのスキルですが、著作権的にアウトなのは個人で楽しむとして、ストアに出しても問題無いようなスキルも思いついたのでそのうち出せたらいいなと思います。

メイドインアビス50話を読んだ

つくし卿の予告通り、今年中に50話までの更新がありましたね。

もうこれ、映像化無理でしょう。ボ卿以上の衝撃だよ。
絵的にも辛いけど倫理観の壊れ具合がヤバイ。

以下、ネタバレ注意




  • イルミューイ=ファプタだと思ってたけどどうやら違う?
  • 村(イルぶる)そのものがイルミューイ?
  • イルミューイから産まれると死ぬからイルミューイの子宮を村にして皆で住もうぜ!みたいなことをワズキャンが提案するのだろうか

ほんとに度し難いな。この漫画は

Amazon Echoにアイカツナビのココちゃんを召喚した

年末にAmazon Echoシリーズがセールやってたので買ってしまいました。

自分が買ったのはEcho Dot。3420円なり。

https://www.amazon.co.jp/dp/B0792PG3S9

正直スマートスピーカーってどうなん?ほんとに生活便利になるん?という疑問は今でもあります。ストアにあるAlexaスキルを見ても正直ピンとくるものがないし・・・

www.amazon.co.jp

でもまぁせっかくなのでカスタムスキルを作ることにしました。なにかHelloWorld的なものを。

ところで2018年最後のアイカツフレンズ!38話見ました?

www.youtube.com

総集編感はありましたが、なんとアイカツナビのココちゃんが最後に歌ってくれるサプライズ!(途中で切られたけど・・・というか歌ってる途中に会話被せるなよ・・・)

ココちゃんはAIなのでAlexaの題材としてはぴったりですね。

お題

  • 「ハロー、ココちゃん」で「ココだよ!」と返す(当然ココちゃんの声で)
  • 「ココちゃん、歌って」で「いっしょにA・I・K・A・T・S・U!」を最後まで歌わせてあげる

www.youtube.com

俄然やる気出てきたぞ!

Alexaスキル開発学習方法

developer.amazon.com

公式に素晴らしいチュートリアルがあるので、これを一通りやるだけで基本的な開発は学べます。他のブログ記事なんかは一切見なくて大丈夫です。

ただ前提として

  • Node.jsでの開発経験
  • AWS S3、Lambdaの知識

はなんとなくでもあったほうが良いです。

事前準備

  • ココちゃん音源
  • AWSアカウント

対話モデル

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "アイカツナビ",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "SimpleReplyIntent",
                    "slots": [],
                    "samples": [
                        "ハローココちゃん",
                        "返事して"
                    ]
                },
                {
                    "name": "AMAZON.PauseIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.ResumeIntent",
                    "samples": []
                },
                {
                    "name": "SingIntent",
                    "slots": [],
                    "samples": [
                        "歌ってココちゃん",
                        "ココちゃん歌って",
                        "歌って"
                    ]
                }
            ],
            "types": []
        }
    }
}

Alexa Developer Consoleがとてもわかり易いので、チュートリアル一通りやったらボタンポチポチしてさくっとできます。

Lambda用関数

github.com

こいつをzipで固めてLambdaにUploadします。

SimpleReplyIntent

「ハロー、ココちゃん」に対する挙動を書きます。

const SimpleReplyIntentHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return (
      request.type === "IntentRequest" &&
      request.intent.name === "SimpleReplyIntent"
    );
  },
  handle(handlerInput) {
    const cocodayo = `<audio src='${constants.voiceData.cocodayo}'/>`;
    return handlerInput.responseBuilder.speak(cocodayo).getResponse();
  }
};

Alexaデフォルトの音声ではなく、ココちゃんの「ココだよ!」の声(mp3)を指定します。mp3ファイルは事前にS3にアップロードしておきます。

カスタム音声について

音声合成マークアップ言語(SSML: Speech-Synthesis-Markup-Language)を用いることで、Alexaが生成する音声に追加の制御ができます。 今回は<audio>タグでココちゃんの声を指定しています。 詳しくはこちら。

developer.amazon.com

ココに注目!

音声ファイルは240秒以内でなければなりません。

ビットレートは48 kbpsでなければなりません。

サンプルレートは22050Hz、24000Hz、16000Hzのいずれかです。

結構制限厳しいんです。どこかの動画から抜き出したような音声ファイルだとまず引っかかります。 ffmpeg等のツールで変換してあげましょう。それに関してもドキュメントがあります。

developer.amazon.com

実機で試すと異様にココちゃんの声が遠くに聞こえたのでボリュームも調整してみました。

$ ffmpeg -i ココだよ.mp3 -ac 2 -codec:a libmp3lame -b:a 48k -af volume=20dB -ar 16000 after.mp3

SingIntent

歌わせます。

const SingIntentHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return (
      request.type === "IntentRequest" && request.intent.name === "SingIntent"
    );
  },
  handle(handlerInput) {
    return controller.play(handlerInput);
  }
};
(略)
const controller = {
  play(handlerInput, offset = 0) {
    const { responseBuilder } = handlerInput;
    const song = constants.audioData[0];
    responseBuilder
      .withShouldEndSession(true)
      .addAudioPlayerPlayDirective(
        "REPLACE_ALL",
        song.url,
        song.title,
        offset,
        null
      );
    return responseBuilder.getResponse();
  }
};

SimpleReplyIntentと同様、音源を用意するわけですが前述の制限もあり音質も悪いのでAudio Player インターフェースを利用します。

developer.amazon.com

Audio Playerインターフェースを利用するにはAlexa Developer Consoleの「インターフェース」メニューから、「Audio Player」をONにします。

f:id:romiogaku:20181231001132p:plain

追加すると「AMAZON.PauseIntent」と「AMAZON.ResumeIntent」の実装が必須になるため、音楽停止時・再開時の挙動をLambdaに追加で実装します。

// 停止
const PauseIntentHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return (
      request.type === "IntentRequest" &&
      (request.intent.name === "AMAZON.CancelIntent" ||
        request.intent.name === "AMAZON.StopIntent" ||
        request.intent.name === "AMAZON.PauseIntent")
    );
  },
  handle(handlerInput) {
    return controller.stop(handlerInput);
  }
};

// 再開
const ResumeIntentHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return (
      request.type === "IntentRequest" &&
      request.intent.name === "AMAZON.ResumeIntent"
    );
  },
  handle(handlerInput) {
    const AudioPlayer = handlerInput.requestEnvelope.context.AudioPlayer;
    const offset = AudioPlayer.offsetInMilliseconds;
    return controller.play(handlerInput, offset);
  }
};

今回は一曲流し切りですが、曲をループさせたり、複数の曲を用意してシャッフルしたりといろいろできるみたいです。

完成!

www.youtube.com

www.youtube.com

う〜ん、このキモオタの声が邪魔ですね。誰だよ。 でもちゃんと反応してくれてます。ココ宮先輩やさしい・・・

最後に

ウェイクワードに「Alexa」と言ってしまっているので、しょせんはこいつはアレクサなんです。ココちゃんじゃないんです。 なんとか「ハローココちゃん」で起動してみたいです。

あとココちゃんって公式的に「koko」なんだろうか。それとも「coco」? →cocoだった。

http://www.aikatsu.net/character/coco.html

AWS Lambda@Edge 躓きポイント10連発

この記事は AWS初心者 Advent Calendar 2018 19日目の記事です。

最近業務でLambda@Edge触ったので、自分が引っかかったポイントなど覚えているうちにメモも兼ねて。

1. 東京リージョンは無い

Lambda@Edgeは通常のLambdaと違い東京リージョンはありません。

自分は米国東部(バージニア北部)を使ってました。Lambda@EdgeとLambdaを行ったり来たりする場合は注意です。

早く日本にも来てほしいなぁ〜

2. 同時実行数に制限がある

Lambda@Edgeを使う上で注意しないといけないのは同時実行数の上限を超えないことです。

上限はデフォルトで1000ですが、これはAWSルートアカウント毎に割り当てられています。 つまり同一アカウント上のその他全ての関数とこの1000という数字を奪い合うことになります。

f:id:romiogaku:20181205173012p:plain

また、この数字は制限引き上げのリクエストから緩和することができます。

3. CloudWatchで関数のInvocationsを可視化するとき、「統計」を「合計」にする必要がある

大抵の人は躓かないと思いますが、デフォルトで「平均」になってますので要変更です。

f:id:romiogaku:20181205173217p:plain

また、同時実行数を可視化する場合は統計を「最大」にしておくと良いでしょう。

f:id:romiogaku:20181205173348p:plain

4. ものによってはとんでもない頻度で実行される

これは実際にやっちまった案件なんですが、Lambdaを使うにあたってお試しで適当な関数を作ってたんです。

ある日CloudWatchの同時実行数見たら500を超えていました。

実はお試しで作った関数のイベントトリガーがS3のPUTイベントになっており、誰かがファイルをS3にPUTする度に実行されていました。消し忘れです。

リリース前に気づけて良かった・・・

5. 同時実行数の制限解除してもらっても反映されていない(ように見える)

AWSに問い合わせてLambdaの同時実行数上げてもらったんですが、東京リージョンである通常のLambdaのほうは上がってるのにLambda@Edgeのほうは上がってないように見えました。

aws cliで確認しても関数の置いてあるであろう米国東部(バージニア北部)リージョンは1000のままです。

$ aws lambda get-account-settings
{
    "AccountLimit": {
        "CodeSizeUnzipped": 262144000,
        "UnreservedConcurrentExecutions": 1000,
        "ConcurrentExecutions": 1000,
        "CodeSizeZipped": 52428800,
        "TotalCodeSize": 80530636800
    },
    "AccountUsage": {
        "FunctionCount": 13,
        "TotalCodeSize": 21042275
    }
}

実はこれ、東京リージョンからのリクエストは東京にレプリケートされた関数で処理をされるため、東京リージョンからのみのリクエストであれば東京リージョンの緩和で問題は無いとのこと。 まぎらわしい!

6. CloudWatchにログが出ない(ように見える)

これもリージョンによる引っかかりポイントなんですが、Lambda@Edge関数の「モニタリング」タブをクリックしてもCloudWatchメトリクスの概要は表示されません。

日本からのアクセスは東京にレプリケートされた関数で実行されるので、CloudWatchログ出力先も東京リージョンになります。でもこの画面自体は米国東部(バージニア北部)なので何も表示されてないということですね。

f:id:romiogaku:20181205173516p:plain

ログを見たければ「CloudWatchのログを表示」ボタンを押す必要がありますが、リージョンは「バージニア北部」とかになっているはずなので「東京」に変更してください。

東京リージョン早く来てくれー!

7. 関数の削除は関数のレプリカがCloudFrontによって削除された場合のみ

関数を削除する前に、CloudFrontとの関連付けを削除する必要があります。

f:id:romiogaku:20181205173540p:plain

これやってからじゃないと消せません。

8. イベントによって関数自体の容量制限が異なる

Lambda@Edgeの制限にもあるように、 Origin Request/Responseは50MB、Viewer Request/Responseは1MBという制限があります。

Nodeで書いてもnode_modulesを巻き込むだけで1MBは軽く超えてしまうので注意です。

9. エラー時はViewer Responseイベントに紐づけた関数は実行されない

「エラー時は特定のページへリダイレクトする」という要件がありまして、最初Viewer Responseをトリガーに作ってたんですがうまくいきませんでした。

理由はこちらにありました。 https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html#lambda-requirements-http-status-codes

オリジンから 400 以上の HTTP ステータスコードが返された場合、ビューワーレスポンスイベントに対して Lambda 関数は実行されません。

代わりにOrigin Responseを使いましょう。

ちょっと脇道に逸れますが、エラー時ののCache TTLはデフォルト300なので、CloudFrontのError Pagesタブから必要に応じて設定しましょう。

f:id:romiogaku:20181205173559p:plain

10. ResponseイベントででUser-Agentがリクエスト時のものと異なる

これはLambda@EdgeというよりCloudFrontのデフォルト設定の特徴なのですが、CloudFrontを通過するとUser-Agentの値がAmazon CloudFrontに書き換えられます。つまり、

f:id:romiogaku:20181205173617p:plain

Origin response、Viewer responseでUser-Agentを利用したい場合は一工夫する必要があります。

CloudFrontにはどのリクエストヘッダを引き継ぐか設定できる項目があるので、User-Agentを指定することも出来ます。 ただしこのやり方はキャッシュ効率が悪いので、PC/スマホ/タブレットの判断することだけが目的なのであれば以下のようにしましょう。

f:id:romiogaku:20181205173628p:plain

CloudFrontのBehaviorsから以下のようなヘッダを追加します。

  • CloudFront-Is-Mobile-Viewer
  • CloudFront-Is-Tablet-Viewer
  • CloudFront-Is-SmartTV-Viewer
  • CloudFront-Is-Desktop-Viewer

Lambda@Edgeではこれらの値(true/false)を見て判断します。

サンプルは

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-examples.html#lambda-examples-redirecting-examples

の「デバイスに基づいて異なるバージョンのオブジェクトを供給する」を参考にすると良いでしょう。

おわりに

Lambda@edgeはLambdaと似てるんですが、CloudFrontやリージョンについて注意深く見ていかないと結構ハマるポイントがありました。

AWS re:Invent 2018ではLambdaに関する発表が色々ありましたが、「Lambdaだけだよ、Lambda@Edgeは別だよスマンな」的なものも結構あると思います。 とはいえAWSはどんどんアップデートされていくので、来年の今頃どうなっているか楽しみですね。

アイカツ!画像でLGTMできるChrome拡張機能をストアに公開しました

この記事はアイカツ! Advent Calendar 2018 の2日目です。

いきなりシャイニング・ラインが途絶えてしまいそうですが書きます。

今年、こんなChrome拡張を作りました。

https://raw.githubusercontent.com/romiogaku/algtm-chrome-extension/master/docs/algtm.gif

github.com

 

LGTMというのは "Looks Good To Me" の略でして、プログラムのコードレビューしてもらうときにコメント書いてもらうんですが「まぁ、いいんじゃね?」ってのをいちいちコメントせずに画像貼っつけたりする文化があるんです。

コードを書くことは即ちITエンジニアにとってアイカツすることと同義でありまして、コードレビューでフレンズ(仕事仲間)とギスギスしないためにも気軽にアイカツ画像で意思表示したいということで作った次第です。

Chromeウェブストアで公開しました

せっかくなのでストアに公開しました。アイコンダサくてごめんね

chrome.google.com

仕組み

画像はAikatsUP!から持ってきてます。

APIが公開されてるのでありがたく利用させて頂いてます。

作者の@sakura_metal さんには感謝です。ストア公開も承諾済。

ガワはReactで作りました。JSからAPI叩くのにCORSとかいろいろ考えるの面倒だったのでプロキシAPIをはさみました。

 

github.com

こっちはgolangで作りました。API作るのにはgoaが最高ですね。まぁエンドポイント1個しか無いですが 

おわりに

劇場版アイカツフレンズ!来てくれ〜〜〜〜〜〜〜〜!

Switchでゲーム出してくれ〜〜〜フォトカツ難民なんだよ〜〜!

バンナムの株買ったからさぁ〜〜〜

でも今年はイベントたくさんあって楽しかったですね。STAR☆ANISとAIKATSU☆STARS!の卒業は悲しかったですがMFは最高のステージでしたしツアーでもいろいろ回れて良かったです。

先週はラクーアに無銭ライブ見に行きましたがまだまだアイカツ熱は冷めそうにないです。来年はなるべく筐体プレイしたい・・・筐体がほしい・・・

 

明日は誰かにバトン繋がるかな?繋がらなくてもまたアイカツ!だね!