Node.js
Heroku
voicetext
GoogleHome
dialogflow

Google Homeで変声機を作ってみた

MicrosoftのちょまどさんがGoogle Homeを使って面白い事をされていたので、
それを参考にして、もう少しスマートスピーカーっぽく声で操作出来るようにしました。

システム概要

言った言葉を違う声でオウム返ししてくれるシステムです。
男性/女性/クマの声を設定する事が出来ます。
音声ファイルはVoiceText Web APIというサービスがあるのでそちらを利用しました。

完成動画

環境

構成図

Node.jsが動いているHerokuサーバーへ受け取った文字列をVoiceTextWeb APIへ渡します。
返ってきたバイナリデータをGoogle Cloud Storageへ保存して、
そのアクセスURLをGoogle Homeへ伝えます。

仕組み.001.png

手順

1.Dialogflowにプロジェクトを新規作成する

Dialogflowへログインして、プロジェクトを新規作成しましょう。
プロジェクト名はVoiceChangerとしました。

s200.png

2.HerokuにNode.jsをセットアップする

コチラを参考にしてHerokuの設定を行ってください。
https://qiita.com/daiki7nohe/items/035c39c1e538551b1f6c

3.ローカルPCでの作業

3-1.NPMパッケージをインストールする

必要なNPMパッケージをインストールします。

// express
$ npm install express --save

// firebase
$ npm install firebase --save

// firebase-admin
$ npm install firebase-admin --save

// VoiceText Web API
$ npm install voicetext --save

3-2.Firebaseにアクセスし、Adminの秘密鍵を取得する

Firebaseにアクセスして、Adminの秘密鍵をダウンロードしましょう。

s100.png

s101.png
秘密鍵をダウンロードします。

3-3.Google Cloud Platformにアクセスしてファイルの読み取り権限を追加する

デフォルトではファイルの読み取り権限がありません。
Google Cloud Platformへログインして権限を付与します。

s102.png

s103.png

s104.png

パケットの権限を編集して、オブジェクトの閲覧者にallUsersを追加します。

3-4.VoiceTextWebAPIのAPIキーを取得する

VoiceTextWebAPIにアクセスして、
APIキーを取得しておきましょう。

3-5.VoiceTextWebAPIの処理

VoiceTextWebAPIで行う処理をモジュール化しておきます。

VoiceTextWriter.js
var VoiceText = require('voicetext');

class VoiceTextWriter {
    constructor(voiceKey) {
        this.voice = new VoiceText(voiceKey);
    }

    /**
     * VoiceTextWebAPIにアクセスしてMP3のバイナリデータを取得する
     * @param {*} text 喋らせたい言葉
     * @param {*} voiceType 音声の種類
     */
    convertToTextBinary(text, voiceType) {
        var self = this;
        var myVoice = self.voice.SPEAKER.HIKARI;

        if (voiceType === 'bear') {
            // クマの声
            myVoice = self.voice.SPEAKER.BEAR;
        } else if (voiceType === 'takeru') {
            // 男性の声
            myVoice = self.voice.SPEAKER.TAKERU;
        }

        return new Promise(function(resolve, reject) {
            self.voice
                .speaker(myVoice)
                .emotion(self.voice.EMOTION.HAPPINESS)
                .emotion_level(self.voice.EMOTION_LEVEL.HIGH)
                .volume(150)
                .speak(text, function(e, buf) {
                    if (e) {
                        console.error(e);
                        reject(e);

                    } else {                        
                        resolve(buf);
                    }
                });                        
        });
    }
}

module.exports = VoiceTextWriter;

3-6.メインの処理

実際の処理はコチラです。

index.js
'use strict';

var firebase = require("firebase");
var admin = require("firebase-admin");
const express = require('express');
const request = require('request');
const bodyParser = require('body-parser');
const app = express();
app.set('port', (process.env.PORT || 5000));
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());

// ダウンロードした秘密鍵をこの名前にして保存している
var serviceAccount = require("./serviceAccountKey.json");
const VoiceTextWriter = require('./VoiceTextWriter');

// bucket名はGoogle Cloud Storageにあるものを入力してください
const bucketName = 'voicechanger-xxxxxxxx.appspot.com';

// Cloud StorageのAdmin認証を行う
admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    storageBucket: bucketName
});

// Getメソッド(Heoku起動に使うけど、要らない)
app.get('/', (req, res) => {
    res.send('Hello World!');
});

// Postメソッド
app.post('/webhook/', function (req, res) {

    // Dialogflowから来るパラメータ取得
    let parameters = req.body.result.parameters;

    // VoiceTextWebAPI初期化
    const voiceTextWriter = new VoiceTextWriter('/* 取得したAPIキーを入力 */');

    voiceTextWriter.convertToTextBinary(parameters.say, parameters.VoiceType).then( function(mp3) {

        // バケット取得
        var bucket = admin.storage().bucket();

        // 保存するファイル名
        const file = bucket.file('voice.mp3');

        // audioのメタデータにする
        const stream = file.createWriteStream({
            metadata: {
              contentType: "audio/mpeg"
            }
        });

        // バイナリデータ書き込み
        stream.write(mp3, function(err) {
            if (err) {
                console.error('ERROR:', err.message);
            }
        });

        // エラー時の処理
        stream.on('error', (err) => {
            console.error('ERROR:', err.message);
            next(err);
        });    

        stream.on('finish', () => {
            console.log('finish!');
        });

        // 書き込み終了
        stream.end(mp3, () => {
            console.log('end');

            // MP3ファイルのアクセスURL
            const url = `https://storage.googleapis.com/voicechanger-xxxxxxxx.appspot.com/voice.mp3`;
            const speak = `<speak><audio src="${url}"/></speak>`;

            let responseJson = {
                "speech": speak,                /* Google Homeにしゃべらせる言葉 */
                "displayText": parameters.say   /* 携帯など画面に表示させる文字 */
            };

            res.header('Content-Type', 'application/json; charset=utf-8');
            res.send(responseJson);
        });

    },
    function (error) {
        console.error('ERROR:', error.message);
    });


});


app.listen(app.get('port'), function() {
    console.log('running on port', app.get('port'))
})

3-7.デプロイする

Herokuサーバーにデプロイしましょう。

4. Dialogflowでの作業

ここからGoogle Homeと連携していくためにDialogflowの設定を行います。

4-1.Fulfillmentを設定する

さきほど作成したプログラムを動かすため、Webhookのリンク先を指定します。

s201.png

4-2.Entitiesを設定する

声の設定を切り替えるため、Entitiesで言葉の登録を行います。
この場合hikariという音声にする場合は「女性」や「女子」と言うとhikariと言ったのと同じ事になります。
その為の登録です。

s202.png

項目
Entities名 VoiceType
hikari hikari, 女性, 女子, 女の子, 女
bear bear, おっさん, オッサン, おじさん, くま, 熊, クマ
takeru takeru, 男性, 男, 男子, 男の子

4-3.起動時のIntentを指定する

アプリ起動時に最初に呼ばれるIntentを設定します。VoiceTypeのデフォルト値をhikariにしています。

Contexts
Contextsを使うことで、Intentをまたがっても設定の引き継ぎを行うことが出来ます。
この場合VoiceTypeを引き継がせています。
Add output context欄にVoiceTypeと入力しましょう。

s203.png

項目
Contexts VoiceType
Action
PARAMETER NAME VoiceType
ENTITY @VoiceType
VALUE hikari

4-4.メインの処理を行うIntent

ここからメインの処理を行うIntentの設定をします。
@sys.anyとする事で、ユーザーの言った言葉をそのまま受け取ることが出来ます。
VoiceTypeはContextsから取得するようにしています。取得は#VoiceType.VoiceTypeで取得することが出来ます。

s204.png

4-5.声の切り替えに対応するIntent

「男性の声にして」というと声を切り替えられるようにしましょう。
VoiceChangeのIntentを作ります。
サーバープログラムではsayというパラメータに反応するように作っているので、④のパラメータsayを使っています。

s205.png

5. シミュレーターで確認する

ここまで出来たら一度シミュレーターで確認してみましょう。
IntegrationsでGoogle Assistantの設定を行います。

s300.png

5-1.初回起動時のIntentを設定する

初回起動時のIntentを指定しましょう。ここで最初に作ったWelcomeIntentを指定しています。
最後にTESTをクリックします。するとActions on Googleの画面が起動します。

s301.png

5-2.アプリ名を設定する

Actions on Googleの画面が起動したら、アプリ名の設定を行います。
画面中央にあるEDITボタンをクリックして設定を行います。

s302.png

アプリ名の設定をしましょう
s303.png

5-3.シミュレーターで確認する

設定が終わったらTEST DRAFTボタンをクリックします。
するとシミュレーター画面に切り替わります。

s304.png

s305.png

6.終了に対応させる

このままではアプリを終了させることが出来ないので、「終了」と言った時にアプリを閉じるようにします。

6-1.終了フレーズを登録する

Entitiesに終了に反応するための言葉のフレーズを登録します。

s206.png

どんな言葉にも対応出来るように考えられる終了フレーズを登録していきます。

項目
Entities名 Finish
Finish Finish, 終了, さようなら, バイバイ, またね, 終わる, 終わり, もういい, 終わって, ばいばい

6-2.FinishのIntentを設定する

終了というフレーズに反応してアプリを終了するIntentの設定を行います。
ContextsのVoiceTypeはもう使わないので、設定を0にします。
Google AssistantのEnd conversationのチェックを入れるようにしましょう。

s207.png

まとめ

これでGoogle Homeを使って変声機を作ることが出来ました。
実際にリリースするためにはVoiceTextWebAPIの契約が必要になります。
実機でテストする際は「変声機」がうまく変換されないので、Google Homeアプリからショートカット設定で登録するとうまくいきます。

アイデア次第で面白いアプリが作れると思います!
どんどん作ってリリースしましょう!