サーバを持っていない人がGoogle Home Notifierで音声変更してみるテスト

Google Homeに自由に喋らせてみたい場合、Google Home Notifierを使う必要があると知って、早速自分も試してみた。
いくつか記事を見ると、その手のテクニックやさらに音声を変更できるテクも存在して楽しそう。
というわけで自分もやってみた。

# 参考にさせていただいた記事・サイト
1 [Google Home開発入門 / google-home-notifier解説](https://qiita.com/SatoTakumi/items/c9de7ff27e5b70508066)

2 [google-home-notifierを使ってGoogleHomeに喋らせる](https://qiita.com/johnny1204/items/ab0e9bbd6887746631a2)

3 [google-tts-apiからHOYA社のVoice Text Web APIへ]
(https://www.miki-ie.com/raspberry-pi/google-home-notifier-hoya-voice-text-web-api/)

4 [【node.js】Google Home Notifierを使ってGoogle Homeにしゃべらせる](http://makky12.hatenablog.com/entry/2018/01/28/221600)

5 [Windows10のストアアプリUbuntuでgoogle-home-notifierを使う](https://qiita.com/fstyle/items/7d333e3340667b77af56)

# 自分の環境
　多くの記事で使用しているRaspberry Pi を自分は持っていないため、手頃なサーバを用意できない！買うのもいいけど諸々下準備がめんどくさい。というわけで自分は、Microsoft Azureを契約しているので、ストレージアカウントにBlobデータとして保存し、共有URLを確保した上でGoogle Homeに連携してみようと考えた。

## 必要なもの・環境
* Google Home Mini（Nest Hubでもなんでも可）
* Node.jsの環境
* Google Home Notifier
* Microsoft Azure（の契約）
* VoiceText Web API　（リンク： https://cloud.voicetext.jp/webapi）

## 作業前にやるサービス加入など
* Google Homeの購入、Googleアカウントからの連携の設定など
* Microsoft Azureの契約
* VoiceText Web APIの利用登録
* Appleアカウントの取得（Bonjour SDKのダウンロードに必要）

なおAzureのストレージアカウントを使うため、使用状況によって課金される点に注意。
https://azure.microsoft.com/en-us/pricing/details/storage/blobs/

# 作業

## Google Home Notifier導入まで

　基本的には参考先記事通りに導入し設定していく。通常のGoogle Home Notifierで喋らせる確認までしておく。

### つまづきやすいポイント
　準備していて結構つまづいたのがnode-gypあたりである。mdnsで結構つまづく・・・。
Python 2.7系インストールも必要とエラーが出たのでエラーが有るたびに調べてはインストール。
結構このあたり、きちんと書いている記事は少ない・・・。
他の開発等ですでに入っていて意識していないせいなのかも。（自分もそうだった）

というわけで、google-home-notifierのインストールまでに必要で特に注意なのは次。
1, Python 2.7系のインストール
2, npm i -g node-gyp
3, npm i -g --production windows-build-tools
4, Bonjour SDK for Windowsのインストール（ダウンロードにはAppleアカウントの登録・ログインが必要）

　別に用意した自分のまっさらな環境ではこれらを入れてやっとgoogle-home-notifierが動いたので、簡潔にいえばこれらをちゃんとやれば大丈夫なはず。

### Ubuntu Linux の環境では

参考サイトの5番目を参考にした。その前の作業として、

```
sudo apt install build-essential
```
は忘れずに実行しておこう。
それ以外は参考サイトそのままでOK。あと、当然？だが上記で実施したwindows-build-toolsは不要である。

## エラーの対処

　Google Home Notifierで参照しているgoogle-tts-apiのバージョンが古いためか、そのまま実行するとエラーになる。そこはpackage.jsonでバージョンを変えると良いらしい。

```json:package.json
"dependencies" : {
...
  "google-tts-api" : "0.0.4",    -> 2020/5現在ではこのバージョンにする
...
}
```

ただ、今回は音声を変更するため、この修正作業自体はしなくてもよい。


## VoiceText Web APIを使用したバージョンに変更する

　ここからが本目的のメインというべきところ。
　上記3番目の参考サイトを基本的に行っていく。おさらいのために記載する。

### VoiceText Web APIの無料利用登録

　これをしないことには始まらない。
　HOYA社のサイト（https://cloud.voicetext.jp/webapi）にて利用登録をする。
　curlでテストコードが書かれているため、自分の環境に合わせてテストをしておく。

### voicetextのインストール

```
npm install voicetext
```

### google-home-notifierのソースコード修正

　掲載されているコードをコピペし、google-home-notifier2.jsなどとして保存する。

### VoiceTextWriter.jsの作成

　ここからがポイント。コードをそのまま引用させていただく。

```javascript:VoiceTextWriter.js
var fs = require('fs');
var VoiceText = require('voicetext');
var voice = new VoiceText('@APIKEY@');  //利用登録して取得したAPI キーを指定
var OUT_PATH = '@WEBPAHT@';  //音声ファイルの保存先パス（PC）
var OUTPUT_URL = '@URL@';    //Web上から参照できる音声ファイルのURL
 
class VoiceTextWriter{
 
 convertToText(text){
 return new Promise(function(resolve,reject){
 voice
 .speaker(voice.SPEAKER.HIKARI)
 .emotion(voice.EMOTION.HAPPINESS)
//	.emotion_level(voice.EMOTION_LEVEL.HIGH)
 .emotion_level(2)
        .speed(100)
 .volume(120)
 .speak(text, function(e, buf){
     if(e){
       console.error(e);
       reject(e);
 
     }else{
    fs.writeFileSync(OUT_PATH, buf, 'binary');  //<1>修正ポイント
    resolve(OUTPUT_URL);
     }
   });
 });
 }
}
module.exports = VoiceTextWriter;
```

　のちほど修正箇所を示す。
　ちなみにこのコードを保存してnodeで動かせば、curlを使わなくてもVoiceText Web APIを手軽に試して音声ファイルも保存できる。それ用にjsファイルを保存しておくのもよいだろう。


## Azureの準備

　ここからはAzureの設定をしていく。無料になる期間を利用してもいいし、その他目的でガッツリ使いたいなら通常の契約をしてもよいだろう。

### ストレージアカウントの作成と設定

Azure のポータルにログインしたら、左のメニューから「ストレージアカウント」を選ぶ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38416/12771084-e655-941a-aa61-78b1edf88eb6.png)

次に左上の「追加」ボタンを押す
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38416/65ad3177-5d14-0bef-6e7a-654dfbe05a4b.png)
※作成の仕方等細かい設定は公式のドキュメントを参照

とりあえず作成した例：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38416/6e460ace-eafc-5d74-266f-4a9f9ce7a205.png)

アクセスキーを生成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38416/174f5a7c-1309-b25c-d1f3-4558026581ed.png)

※作られた段階ですでにあるかもしれないが、念の為キーが存在することを確認する。


#### Blob serviceでコンテナを作成する

　ストレージアカウントの概要ページにNode.jsのツールとSDKが確認できるので、そこからサンプルをダウンロードしておこう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38416/172267ec-32d7-ff7a-dfdf-e09748071cfe.png)

　↓

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38416/5326a6f0-8184-c97c-ff99-11fe720f2dcb.png)

合わせてライブラリサンプルをZIPでダウンロードしておく。

#### コンテナを作成する

Blob serviceの「コンテナー」から、新しいコンテナを作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38416/37c61f96-ae4d-6e7b-cb3f-7922d69621fe.png)

サンプルスクリプトでもコンテナーは作成できるので、余力があれば覚えておくと良い。
今回はこうして事前に作成しておく。

#### VoiceText Web APIで生成したwavファイルをアップロードする

Azure のAPIで共有などできるのだろうが、そうした準備が面倒くさいため、使用するファイルを事前にアップロードして共有を設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38416/08567d45-eea0-9926-4333-c291391fbaee.png)

基本的には同じファイル名で上書きでアップロードする流れになる。

#### SASの生成をする

アップロードが終わったら、SAS(Shared Access Signature）を設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38416/05046192-56f0-a365-3b9d-c16f2d95e205.png)

アクセス許可は「読み取り」でよい。
開始日時と有効期限は必要と思われる期間を設定しておく。無期限にしたいなら適当に設定しよう。

署名キーは、アクセスキーのこと。キー1とキー2があるので好きな方を選ぶ。

そして「SASトークンおよびURLを生成」ボタンを押せば共有URLが生成される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38416/0a7804f6-6f5d-f8ed-7e45-e9f6e13a6cd8.png)

このうち、使用するのはBLOB SAS URLの方だ。


## スクリプトの最終準備

ストレージアカウントのBlobのサンプルをダウンロードしたら、注目すべきは次のファイルだ。

* basic.js
* sample.env

Azureのサンプルは.envファイルを参照し、必要な情報を読み込んで実行する形だ。その方式をそのまま使う。

やろうとしたのは次の流れ。
google-home-notifies2.js
↓
VoiceTextWriter.js
↓
Azure にblobとして保存するスクリプト

このスクリプトを作るのにbasic.jsを使った。

### .envファイルの編集

sample.envを.envに書き換えて次に用意するファイルと同じ場所に保存する。

```
# Used in most samples. Retrieve these values from a storage account in the Azure Portal.
ACCOUNT_NAME=ストレージアカウント名
ACCOUNT_KEY=ストレージアカウントのキー（key1かkey2のキー）

# Used for withConnString
STORAGE_CONNECTION_STRING=ストレージアカウントの接続文字列（key1かkey2の接続文字列）
※この例では使わない

# Used for the advanced and anonymousCred tests. Create a SAS token for a storage account in the Azure Portal.
ACCOUNT_SAS=<shared-access-signature>　※この例では使わない

# Used to authenticate using Azure AD as a service principal for role-based authentication.
#
# See the documentation for `EnvironmentCredential` at the following link:
# https://docs.microsoft.com/javascript/api/@azure/identity/environmentcredential
AZURE_TENANT_ID=アプリのテナントID　　※この例では使わない
AZURE_CLIENT_ID=アプリのクライアントID　　※この例では使わない
AZURE_CLIENT_SECRET=アプリのクライアントシークレット　　※この例では使わない

# To run the proxyAuth sample, set up an HTTP proxy and enter your information:
# HTTP_PROXY=http://localhost:3128　　※この例では使わない
```
サンプルコードを見る限りはACCOUNT_NAMEとACCOUNT_KEYしか参照していないが、これら以外は多分問題ないだろう。


basic.jsをコピーするなどして次のように作った。

### Azure用のスクリプト

```javascript:azureope.js
const { BlobServiceClient, StorageSharedKeyCredential } = require("@azure/storage-blob");

// Load the .env file if it exists
require("dotenv").config();

async function azure_upload(filename, data) {
	// Enter your storage account name and shared key
	const account = process.env.ACCOUNT_NAME || "";
	const accountKey = process.env.ACCOUNT_KEY || "";

	// Use StorageSharedKeyCredential with storage account and account key
	// StorageSharedKeyCredential is only avaiable in Node.js runtime, not in browsers
	const sharedKeyCredential = new StorageSharedKeyCredential(account, accountKey);

	// List containers
	const blobServiceClient = new BlobServiceClient(
	// When using AnonymousCredential, following url should include a valid SAS or support public access
	`https://${account}.blob.core.windows.net`,
	sharedKeyCredential
	);

	var containerClient = null;
	
	let i = 1;
	for await (const container of blobServiceClient.listContainers()) {
		console.log(`Container ${i++}: ${container.name}`);
		if (container.name == "mediatest1") {
			//containerClient = container;
			containerClient = blobServiceClient.getContainerClient(container.name );
		}
	}

	// Create a container
blobServiceClient.getContainerClient(containerName);
	if (containerClient) {


		// Create a blob
		const blobName = filename;
		const blockBlobClient = containerClient.getBlockBlobClient(blobName);
		const uploadBlobResponse = await blockBlobClient.upload(data, Buffer.byteLength(data));
		console.log(`Upload block blob ${blobName} successfully`, uploadBlobResponse.requestId);
	}

}

// A helper method used to read a Node.js readable stream into string
async function streamToString(readableStream) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    readableStream.on("data", (data) => {
      chunks.push(data.toString());
    });
    readableStream.on("end", () => {
      resolve(chunks.join(""));
    });
    readableStream.on("error", reject);
  });
}


exports.azure_upload = azure_upload;
```

basic.jsからコンテナーの列挙や作成、削除を取り除いたものである。VoiceTextWriter.jsから適切に呼び出せるように、filenameとdataを引数に取る関数とした。
このスクリプトをVoiceTextWriter.jsと同じフォルダに保存する。


### VoiceTextWriter.jsを修正する

　ここでやっとVoiceTextWriter.jsを修正していく。上記コードを次のように修正した。

```javascript:VoiceTextWriter.js
var fs = require('fs');
var VoiceText = require('voicetext');
var azureope = require("./azureope");  //追加
//var voice = new VoiceText('@APIKEY@');  //利用登録して取得したAPI キーを指定
//var OUT_PATH = '@WEBPAHT@';  //音声ファイルの保存先パス（PC）
var OUTPUT_URL = 'https://azuremydrive.blob.core.windows.net/mediateste1/voice-text.wav?*******';    //Web上から参照できる音声ファイルのURL
 
class VoiceTextWriter{
	constructor () {
                //利用登録して取得したAPI キーを指定、こちらに持ってきた
		this.voice = new VoiceText('@APIKEY@'); 
		this.voice.speaker(this.voice.SPEAKER.HARUKA);
		this.voice.emotion(this.voice.EMOTION.SADNESS).emotion_level(2);
		this.voice.volume(100);
	}
	speaker(speaker) {
		this.voice.speaker(speaker);
	}
	emotion(emotion,level) {
		this.voice.emotion(emotion).emotion_level(level);
	}
	volume(vol) {
		this.voice.volume(vol);
	}
	speed(speed) {
		this.voice.speed(speed);
	}
	pitch(pitch) {
		this.voice.pitch(pitch);
	}
	convertToText(text){
		return new Promise((resolve,reject)=> {
			this.voice
			//.speaker(voice.SPEAKER.HARUKA)
			//.emotion(voice.EMOTION.SADNESS)
			//.emotion_level(2)
			//.volume(100)
			.speak(text, function(e, buf){
				if(e){
					console.error(e);
					reject(e);
				}else{
					azureope.azure_upload("voice-text.wav",buf);
					//fs.writeFileSync(OUT_PATH, buf, 'binary');
					resolve(OUTPUT_URL);
				}
			});
		});
	}

}
module.exports = VoiceTextWriter;
```

便利に呼び出せるようにしておきたいので、設定用のメソッドを付け足した。
あと、google-home-notify2.jsに次を付け足しておく。

```javascript
exports.voiceTextWriter = voiceTextWriter;
```

OUTPUT_URLを、Azureの設定で取得したBLOB SAS URLにする。
それからazure_upload関数に受け渡すファイル名はAzureの設定で事前にアップロードしたファイル名にする。これを間違えると音声は鳴らない。


### 本実行用のスクリプトを準備して実行する

google-home-notifierに付属しているサンプルなどから本実行用のスクリプトを作成する。

```javascript:test.js
const mydevices = {
	familyfloor : "IPアドレス",	//"Google-Nest-Hub-****"
	familyroom : "IPアドレス"	//"Google-Home-Mini-****"
};
var googlehome = require('./google-home-notifier2');
var language = 'ja'; // if not set 'us' language will be used

//googlehome.device('Google Home', language); // Change to your Google Home name
// or if you know your Google Home IP
googlehome.ip(mydevices["familyroom"], language);

googlehome.voiceTextWriter.speaker("haruka");
googlehome.voiceTextWriter.emotion("happiness",1);
googlehome.voiceTextWriter.pitch(10);
googlehome.voiceTextWriter.speed(100);
googlehome.notify('五月雨って言います。提督、よろしくおねがいしますね！', function(res) {
  console.log(res);
});

```

修正したgoogle-home-notifier2.jsを読み込むのがポイントだ。

これを実行すると、コンソールに特にエラーなく表示され、めでたくGoogle Homeが変更された音声で喋ってくれる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38416/6993948d-213e-e081-1965-47e91807d22b.png)

※確認のためconsole.logをいくつか付け足した例

ちゃんとAzure ストレージアカウントのBlob serviceのコンテナにアップロードし、それをGoogle Homeが参照して音声ファイルを再生してくれた。

あとは組み込みたい方法でスクリプトを組み込み、自分の好みのGoogle Home環境を整えれば完璧だ。


# 終わりに

 多くのサンプルであるように、Raspberry Piを用意したほうが安心して色々できるのだろうが、いざサクッとやりたい場合、実際のマシンの構築からサーバ化やらと事前の準備が多くなって諦めてしまう可能性が高いはず。
　今回、Azureにあるファイル保存のサービスを使って実現したわけだが、きっと一般のオンラインストレージやAWSなどでも同様に実現できると思われる。

　Google Homeで可愛い声（あるいはカッコいい声、感情豊かな声）で喋らせたい！けど必要なサーバを用意できない・・・という方々の参考になれば幸いである。