Help us understand the problem. What is going on with this article?

スマートスピーカーに自発的に喋らせよう (IFTTT + Sonos + AWS)

目的

Amazon Echo や Google Home などのスマートスピーカーは基本的にこちらから話しかけないと喋ってくれない。個人的に何か起きたらスマートスピーカー側から話して欲しかったのでそのための仕組みを作る。

必要なもの

  • IFTTT
  • Sonos スピーカー (Sonos One)
  • AWS

Sonos スピーカーは IFTTT に対応してて API 経由で音声ファイルを再生できるスマートスピーカーで、一番安いのでも2.5万円ぐらいするけど便利だし楽しいしみんな買うといい(販促

構成

今回作ったのはGoogle カレンダーの予定を時間になったら喋らせる仕組み。ただし、パラメータ渡して API を叩いてるだけなので IFTTT 上のトリガーだったら Twitter だろうが Slack だろうがなんでも使える。
ifttt_sonos_aws2.png

作ったもの

IFTTT

作るアプレットは3種類
- Google Calendar の発火をトリガーに Web Request を飛ばす
- Webhook を受け取り Sonos に指定ファイルを再生させる
- Webhook を受け取り Sonos を停止させる

Google Calendar の発火をトリガーに Web Request を飛ばす

Any events starts をトリガーにして Webhooks の Make a web request をアクションに設定。リクエストの設定は以下の通り。
- Method: Post
- Content Type: application/json
- Body:

{ "text": "{{Title}}の時間だああああああ", "voiceId": "Takumi", "during": 30 }

Body 内の各パラメータの意味
- text: 再生する音声
- voiceId: Polly の音声の種類 (日本語なら Takumi か Mizuki)
- during: 音声を再生する秒数

Webhook を受け取り Sonos に指定ファイルを再生させる

トリガーを Webhooks の Receive a web request にし、Sonos の Play stream をアクションに設定。
Event Name は適当なものを設定し、What do you want to play のところに {{Value1}} を設定する。

Webhook を受け取り Sonos を停止させる

トリガーを Webhooks の Receive a web request にし、Sonos の Pause をアクションに設定。

AWS

API Gateway

IFTTT から Webhook を受けるための入り口。
統合リクエストの設定は以下のように設定(参考:https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-api-gateway.html)
APIGateway設定.png
マッピングテンプレートは以下のように設定する。stateMachineArn には StepFunction のステートマシンの ARN を指定する。

{
    "input": "$util.escapeJavaScript($input.json('$'))",
    "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld"
} 

Step Functions

音声を再生するだけなら Lambda だけでいいのだが、IFTTT の API だと Sonos が無限リピートで再生を始める。どこかで再生を止めるために、音声再生 → 指定時間後に音声を止める Lambda を実行するステートマシンを作成する。マシン定義は以下の通り。IFTTT から渡ってくる during パラメータの秒数分待機し、その後再生を停止する Lambda を実行する。

{
  "StartAt": "PlayVoiceState",
  "States": {
    "PlayVoiceState": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:1:123456789012:playVoiceOnSonos",
      "ResultPath":"$",
      "Next": "WaitSeconds"
    },
    "WaitSeconds": {
      "Type": "Wait",
      "SecondsPath": "$.during",
      "Next": "PauseVoice"
    },
    "PauseVoice": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:123456789012:function:pauseSonos",
      "End": true
    }
  }
}

Lambda

作ったものは以下の通り。
1. テキストから音声ファイルを作り、再生の Webhook を飛ばす Lambda
2. 再生停止の Webhook を飛ばす Lambda

実装はいずれも Node.js 10.x で実行ロールに Polly と S3 の権限がそれぞれ必要。

テキストから音声ファイルを作り、再生のWebhookを飛ばすLambda
const crypto = require('crypto');
const https = require('https');
const AWS = require('aws-sdk');

const IFTTT_EVENT_NAME = process.env.IFTTT_EVENT_NAME;
const IFTTT_KEY = process.env.IFTTT_KEY;

const polly = new AWS.Polly();
const s3 = new AWS.S3();

const err_response = { statusCode: 500 };

exports.handler = async (event, context, callback) => {
    console.log(event);
    const pollyParams = {
        OutputFormat: 'mp3',
        VoiceId: event.voiceId,
        Text: event.text,
        TextType: 'text'
    };
    const pollyRes = await polly.synthesizeSpeech(pollyParams).promise();
    const s3Params = {
        Body: pollyRes.AudioStream, 
        Bucket: process.env.S3_BUCKET, 
        Key: crypto.randomBytes(32).toString('hex') + '.mp3'
    };
    const s3Res = await s3.upload(s3Params, async (err, data) => {
        if (err) {
            console.log(err.message);
            err_response['body'] = err.message;
            callback(JSON.stringify(err_response));
        }
    }).promise();
    const body = JSON.stringify({ value1: s3Res.Location });
    const httpsOptions = {
        hostname: 'maker.ifttt.com',
        path: `/trigger/${IFTTT_EVENT_NAME}/with/key/${IFTTT_KEY}`,
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        }
    };
    const res = await new Promise((resolve, reject) => {
        const iftttHttpReq = https.request(httpsOptions, (res) => {
            resolve(res);
        });
        iftttHttpReq.write(body);
        iftttHttpReq.end();
    });

    const response = event;
    response.statusCode = res.statusCode;

    context.succeed(response);
};
再生停止のWebhookを飛ばすLambda
const https = require('https');

const IFTTT_EVENT_NAME = process.env.IFTTT_EVENT_NAME;
const IFTTT_KEY = process.env.IFTTT_KEY;

exports.handler = async (event, context) => {
    const httpsOptions = {
        hostname: 'maker.ifttt.com',
        path: `/trigger/${IFTTT_EVENT_NAME}/with/key/${IFTTT_KEY}`,
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        }
    };
    const response = await new Promise((resolve, reject) => {
        const iftttHttpReq = https.request(httpsOptions, (res) => {
            resolve(res);
        });
        iftttHttpReq.end();
    });
    context.succeed(response.statusCode);
};

S3

バケットを用意するだけなのだが、時間が経つと余計な音声ファイルが残り続けるので、ライフサイクルルールで一定期間後に削除するよう設定するのがよい。

課題

  • 再生中に他のイベントが発生すると次の再生が強制的に始まってしまう
    • 今回は実装を楽にするために IFTTT 経由で Sonos を操作しているけど、直接 API を叩けばキューに追加できるので暇があればその対応をするかも
  • リピートしないで一回しか再生しないようにできない
    • 音声ファイルの再生時間を見るようにするか、直接 API で操作すればなんとかなりそう

おわりに

他にもスマートロックをトリガーにおかえりメッセージ流すとか、暇そうにしてるときにツイッターのタイムライン流すとか、それ以外にも応用しがいがある気がするので、こういうアイデアあるぜ!みたいなのを教えてくれると嬉しい。

kaku10
YDD (Yaritaikoto Driven Developer)
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした