【MESH カスタムタグ】Raspberry Pi3を用いて音声合成を手軽に使えるように

#できること
MESH*アプリ上で、音声合成の設定をして**「任意の言葉を喋らせる」というアクション**を行うことが出来ます(Raspberry Pi3から音声が出ます)。例えば、人感タグと組み合わせると、家を出るタイミングで挨拶をしてもらうことが出来ます。


![名称未設定-5.png](https://qiita-image-store.s3.amazonaws.com/0/159447/0b7675da-b002-d084-67bd-67ec71e19cb2.png)
実際に動作している動画を公開しようと思いましたが、VoiceText Web APIの規約に反するようなので止めました。写真では分からないですが、レスポンス速度はトリガー実行されて1秒以内とかなり良いです(Wi-Fi環境に左右されると思いますが)。


![IMG_0146.jpg](https://qiita-image-store.s3.amazonaws.com/0/159447/53728d0e-4888-2e6f-3f85-3ac7c591dd80.jpeg)
上記は音声合成タグの設定画面で、話す言葉・話者・感情を変更できます。

*MESHとは、小さな便利を形にできる、ブロック形状の電子タグです。
- [MESH：小さな便利を形にできる、ブロック形状の電子タグ](http://meshprj.com/jp/)

#作成した理由
以前、『[【MESH カスタムタグ】google home miniに好きな言葉をしゃべらせる](https://qiita.com/glory/items/a464872b9655a21e9f2f)』という記事を書いたのですが、下記の点があり、新たに作り直しました。
- google homeだと音声合成した言葉の前に「**ポンッ**」といった効果音が強制的?に入ってしまった。やりたいことは、単純に**言葉だけ発話させること**。
- Raspberry Piとgoogle homeの2台が必要になるので、**もっと仕組みを単純化したい**。
- 音声の品質がやや微妙なので、**音声の品質を改善をしたい**。

#実装方法
##仕組み
MESH側からRaspberry Pi側にクエリを送って、そのクエリ情報を基にVoicetextで音声ファイルを作成して取得します。取得したファイルはnode-aplayで再生指示され、Raspberry Piに繋がれたスピーカーから音声が再生されます。仕組みを図化したものが下記になります。
![音声合成のしくみ.png](https://qiita-image-store.s3.amazonaws.com/0/159447/60cdec96-52df-d30f-ebf1-550bb3f9c861.png)

##用意するもの
- [MESH SDK アカウント](https://meshprj.com/sdk-jp/)
- MESHアプリが動作するタブレット or スマホ
- [VoiceText Web APIキー](https://cloud.voicetext.jp/webapi)
- Raspberry Pi3 mdoel B(NOOBSで「Raspbian」をインストール)
- [スピーカー](https://www.amazon.co.jp/dp/B002VITVE0)
- Wi-Fi環境

##Raspberry Pi3側(音声合成側)
ターミナルを開いて、下記のコマンドを実行して下さい。
基本的には(ほぼ)下記のサイトを参考にさせて頂きました。
- [Raspberry Pi をNode.jsを使ってブラウザから喋らせる。](https://qiita.com/PonDad/items/ef22c9b6cef15a791643)

```
#始める前のおまじない
$ sudo apt update
$ sudo apt upgrade

#作業用のディレクトリの作成
$ mkdir talk

#必要なパッケージをインストール
$ npm init
$ npm install express --save
$ npm install ejs --save
$ npm install voicetext --save
$ npm install node-aplay --save

#作業用ディレクトリに移動
$ cd talk

#Node.jsで動作させるjsファイルの作成(中身は後述記載)
~/talk $ sudo nano app.js

#ブラウザで動作できるように(今回の場合最終的に不要だが、事前に動作確認を行うために利用)
~/talk $ mkdir views
~/talk $ cd views
~/talk/views $ sudo nano talk.ejs(中身は後述記載)
```

###app.js
**YOUR_VOICETEXT_WEB_API_KEY**部分は発行されたVoicetext Web APIキーに置き変えてください。また、**console.log('This app listening at http://192.168.x.xx:',port)**部分は、ifconfigコマンドで割り振られているIPアドレスを確認して置き変えておくと良いです。

DHCPでIPアドレスを自動割り振られて変わってしまうのが困る方は、下記を参考にして他と被らないIPアドレスを割り当ててください。
- [固定IPアドレスの設定](http://www.hiramine.com/physicalcomputing/raspberrypi3/wlan_howto.html)



```app.js
//パッケージの読み込み許可
var express = require('express');
var ejs = require("ejs");
var VoiceText = require('voicetext');
var Sound = require('node-aplay');

//ファイル読み込み許可
var fs = require('fs');

//ブラウザ操作用のejsファイルのレダリング
var app = express();
app.engine('ejs',ejs.renderFile);

app.get('/', function(req, res){
    res.render('talk.ejs',
        {title: 'Raspberry Pi Talk'});
})

//合成音声の読み込みとwavファイル作成・再生
var voice = new VoiceText('YOUR_VOICETEXT_WEB_API_KEY');

app.get('/control', function (req, res) {
  console.log(req.query);
  var text = req.query.text ? req.query.text : "";
  var speaker       = req.query.speaker ? req.query.speaker : voice.SPEAKER.HARUKA;
  var emotion       = req.query.emotion ? req.query.emotion : voice.EMOTION.HAPPINESS;
  var emotion_level = req.query.emotion_level ? req.query.emotion_level : voice.EMOTION_LEVEL.LOW;
  var pitch         = req.query.pitch ? req.query.pitch : 100;
  var speed         = req.query.speed ? req.query.speed : 100;
  var volume        = req.query.volume ? req.query.volume : 100;
  voice.speaker(speaker)
       .emotion(emotion)
       .emotion_level(emotion_level)
       .pitch(pitch)
       .speed(speed)
       .volume(volume)
       .speak(text, function(e, buf){
         return fs.writeFile('./talk.wav', buf, 'binary', function(e){
          if(e){
            return console.error(e);
          }
          new Sound('talk.wav').play();
         })
       });
  res.send(text);
});

//ローカルサーバーの起動
var server = app.listen(3000, function () {
  var host = server.address().address
  var port = server.address().port
  console.log('This app listening at http://192.168.x.xx:',port)
});
```

###talk.ejs
**$.get("http://192.168.x.xx:3000/control", { text: msg } );**部分のIPアドレスはapp.jsと同様に各自適切なものに置き変えてください。

```talk.ejs
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta http-equiv="content-type"
        content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0,user-scalable=no">
    <title><%=title %></title>
     <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" type="text/css" />
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
    <style>
     article{
       margin: 20px;
     }
    </style>
    </head>
<body>
    <header>
        <h1 class="text-center h2"><%=title %></h1>
    </header>
    <article>
    <form>
    <div class="form-group">
    <input class="form-control" type="text" id="query">
    </div>
    <input  class="btn btn-default" type="reset" value="しゃべるよ" onclick="send()" />
  </form>
    </article>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>

    <script>
        var send = function() {
                var msg = document.getElementById('query').value;
                $.get("http://192.168.x.xx:3000/control", { text: msg } );
            };

    </script>
</body>
</html>
```


###スピーカーの設定
今回はイヤホンジャック端子のスピーカーを利用したので、音の出力をそれに固定しました。
HDMIにする場合は、最後の1を2に変更してください。

```
#出力をアナログ（=イヤホンジャック）に固定
$ amixer cset numid=3 1

```
###音声合成の動作確認
下記のコマンドでサービスを立ち上げたまま**http://192.168.x.xx:3000/** にスマホでアクセスして動作確認してください。この際、他に特殊な設定をしていなければRaspberry Piと同じWi-Fi環境下に繋がっている必要があります。

```
~/talk $ node app.js
```
![image_123986672.png](https://qiita-image-store.s3.amazonaws.com/0/159447/c71ba20c-81e6-adc0-4870-790e0fd7c098.png)

##MESHカスタムタグ側の設定
###MESHカスタムタグを利用できるまでの流れ
①[MESH SDKログインページ](https://meshprj.com/signin/)からID等を入力してログインして下さい。
②「Create New Tag」から新しいカスタムタグを作ってください。
③「import」を開き、ページ下部にあるjosnデータ(speech.json)をコピペして「Load JSON」を押して読み込んでください。
④必要箇所を修正して、Saveしてください。
⑤タブレットorスマホ上でMESHアプリを開き、MESH SDKアカウントを紐づけて、右のカスタムの文字の下にある『追加』から作成したカスタムタグを追加してください。
⑥音声合成タグが追加されたら、タグを左の場所にドラッグ＆ドロップして項目を設定してから、トリガーとつなげてください。

###MESHカスタムタグのプログラム
![名称未設定-2.png](https://qiita-image-store.s3.amazonaws.com/0/159447/4a953500-a22d-1a18-dc6b-76d3ed328147.png)

####Initialize, Receive, Result
特に書くことはありません。
####Execute
**var raspiURL = "http://192.168.x.xx:3000/control";**のx.xx部分は適切なものに置き変えてください。また、話す言葉・話者・感情の3つの項目を調整できるようにしています。感情レベルなど別の設定もアプリ上で調整したい方は、Propertyを追加して、text_dataの所に必要なパラメータを加えてください。

```Execute.js
var raspiURL = "http://192.168.x.xx:3000/control";

var text_data = {
	"text" : properties.speakText,
	"speaker" : properties.speaker,
	"emotion":properties.emotion
};

ajax ({
    url : raspiURL,
	data:text_data,
    type : 'get',
    timeout : 5000,
})
.then(
	//成功時
	function(data){
		log('trigger success');
        callbackSuccess( {
            resultType : 'continue'
        } );_
 	},
	//失敗時
	function(){
		log('getLatLng: Network error');
	}
);	

return {
    resultType : 'pause'
};
```

###動作確認
前提として、Raspberry PiとMESHアプリを動作している端末は同じWi-Fi環境下にしてください。その後、Raspberry Pi3側で下記のコマンド入力してサービスを立ち上げたままにしてください。

```
~/talk $ node app.js
```

![IMG_0148.jpg](https://qiita-image-store.s3.amazonaws.com/0/159447/55acc0ee-5fa5-bc76-79b9-b77506dc2feb.jpeg)
音声合成タグに何かトリガーを繋げて、トリガーを実行して下さい。

![image_50404865.jpg](https://qiita-image-store.s3.amazonaws.com/0/159447/f0f7eacc-d222-a28e-7773-e703008b107c.jpeg)
http通信が成功していると、Raspberry Pi3のターミナルに上記のようなメッセージが表示されていくと思います。

###impotするJSONファイル
下記は、準備の③『「import」を開き、下記のjosnデータを入力して「Load JSON」を押して読み込んでください。』に必要なjsonデータです。

```speech.json
{"formatVersion":"1.0","tagData":{"name":"音声合成","icon":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAE6ElEQVR4nO3bX2/TVhgG8KDRFiGGYBJXu0GDa/YJ2EeAi6kXk0pBZYuEpnaio2ISVYBsHZQ/YhWoE0MDdZtUIQaTViakClWbqFJCi0ZV0m6kIKCkqdMmtEnYgMZ+d5ElNHFsH9vHfn2S9+K5aW23fn62T2If+0D6CkSINHMajocHhMnL+DGm/fJhF8uSc2PX0Au1kjvR8+IDpGe70Yu0E+EBsAu0m8GpSwSAmbNj1wgAMz2jvxAAARAAARAAAVQ/QODQPtdCABXS6W90LQRAAARAAAKEAAiAAAiAANwBoEGYAAiAABABAh17XQsBCBICIAACIAACIAACQErfvctiA1yd+Am9RDuRZk6LDSD6WWC0bw4CdAEkvgFl4TtQFi6CMt8DILHNGK4WBJb94gogpwYgl33EFHlp2PT207Fu+P2v72Fw8lJJzJRSvi7vhB5cAHmui3mfuADkMpPMxasz7col6pzBHE2s2AJQkj/YKL4s6THHALBLdgQgl3nAr/wV4QlwPdKHXrAjALnsQ0fKt4og2lFvC8DJ4q0irCx+fLoXvVTHAOTnN1wDMDM4Hw8PQLdAR71lAPfK//8sSJxi+r8WYyfQi3QcwO3yrY4HooUR4GtHS36dntb8nZL0/icZxwFy2ahj5Rcecnd91lyTZwEjgDPlv0hOlcw0IABOABMjvxqWWsiRTz+CTn8jPI78oX0Zmv8WvSg0AGXhouXLSqe/EV4t8rh8PUQvCg0glx43XVjvl/uZzwDWYBeFB2DrTie/YBeFCPA3evk1DhBxr+iM9vcB7KLQAOSlYUdLzwwFId7uy6ejjgDUOca18OXUfZACm96UXpZK68jpMHpRiAD2v4il+nZqFr4y2VsnKh/9iZPoRQkN8M/oeSaAWrv8MAMoqZ9tI8QPvKVbfuZmQGPdKHpJ6AAg8bkfJAXfrVj+v5NXavLoNwUgLw5xG4hfRgcgGzoDy4tTBstW99FvCgAkhCdiHijIUwBuIsC8WA/XXQNQ5nsdL19euoVejGcBQLJ2i5o5NmbIiRgbUxO7uJevJPvRCxEIIB9ed0uxixAWIB/rsyYgof8GSbWH+wsay4l+WM7ozx19tTCEvuNeCRcAZTYIoaY6CDXVQSw6DLHpkGEKy7/4sxW9BKEBRpobimUWEYzK31WvWkeOHUUvQyiApd92qEpkQbjdslZznbttm9ALEQIgEXwH4u0+XYCR5gZV+eOB9/XROuoh3u5DL8XTAHMH64t3MfXKDDXVQdi/sVh+9McWw+VX3iHFLsaTAIngxpKSwh+vMyz1fvd2mBntN1yuHKBWEJgBXt/bpyoo1qEeTK0m0rZOtf3kmc3oBXkGQOtJll6pYy3sQFrbxy7IEwBS4G1zALvy48TdPatVJYdb1pgCqHYEJgC9ciKtpePARNv64u8qAcTbffD44PrST0y71+j+DeVZAL0oNIDnF7YZzmYoFDn7eemDdy2AfFYVf/7kaOVnxbVwFhgCsEwnKVxyyqMPkM/oJ+rBlwBMAmiFBYA12EWhAChPOz0DkOx5D70s1wHSlz/gDlB4cWOwdYupbc190YBelusAqbNbuQI83b+65PUlugwZACSObHDsDDjs/5AAjACkQ2s9MwYQAAEQAHZZBOCBwgigykIABEAABEAABEAABEAABEAABEAABFBbAP8BGHqId/HrA4sAAAAASUVORK5CYII=","description":"音声合成を行うことができます。","functions":[{"id":"function_0","name":"音声合成をする","connector":{"inputs":[{"label":""}],"outputs":[{"label":""}]},"properties":[{"name":"話す言葉","referenceName":"speakText","type":"string","defaultValue":"言葉を入力してください"},{"name":"話者","referenceName":"speaker","type":"string","defaultValue":"hikari"},{"name":"感情","referenceName":"emotion","type":"string","defaultValue":"happiness"}],"extension":{"initialize":"","receive":"","execute":"var raspiURL = \"http://192.168.x.xx:3000/control\";\n\nvar text_data = {\n\t\"text\" : properties.speakText,\n\t\"speaker\" : properties.speaker,\n\t\"emotion\":properties.emotion\n};\n\najax ({\n    url : raspiURL,\n\tdata:text_data,\n    type : 'get',\n    timeout : 5000,\n})\n.then(\n\t//成功時\n\tfunction(data){\n\t\tlog('trigger success');\n        callbackSuccess( {\n            resultType : 'continue'\n        } );_\n \t},\n\t//失敗時\n\tfunction(){\n\t\tlog('getLatLng: Network error');\n\t}\n);\t\n\nreturn {\n    resultType : 'pause'\n};","result":""}}]}}
```


#作ってみて感想
結構簡単に実装出来ました。また、nodeコマンドでapp.jsのサービスを毎回起動するのが面倒だったのでシェルスクリプトを作成して、デスクトップからshファイルをダブルクリックで立ち上がるようにすると便利です(ラズパイ起動時に立ちあがるように設定しても良いかもしれません)

ちなみに、このページで出てくる**192.168.x.xx**の部分は、初期設定のままなら**raspberrypi.local**に変更しても動きます。