RAPIRO(ラピロ)に音声合成で喋らせる

一口に喋らせるといっても、やり方はいろいろあると思います。音声合成LSIを使うとか、音声合成APIを使うとか、スピーカーもラピロに内蔵するか外付けするかとか。

ここではできるだけ簡単に実現できるようにするために、スピーカーは外付け、音声合成はWeb APIでやることにします。

スピーカーは、直接Raspberry Piのステレオジャックにミニプラグを挿してもよいのかもしれませんが、コード引き出すのが面倒なのと、後々マイクを使う可能性も考えて、USBオーディオ変換アダプタをかますことにします。

これは挿すだけで認識してくれますが、これ経由で音を出せるようにするにはちょっとだけ設定ファイルをいじる必要があります。以下が参考になります。

$ sudo vim /etc/modprobe.d/alsa-base.conf

で、次のように変更。

# options snd-usb-audio index=-2
options snd-usb-audio index=0

変更できたら、reboot。

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

と表示されれば準備OK。必要であれば、上記リンク先に書かれている、録音と再生のテストをしておきましょう。

ちなみにスピーカーはこれです。USB給電で安くて音質もそれなりに良いのを探していたら、これの評判が良さそうだったので。

ラピロの後ろにスピーカーを並べてみた図。

th_rapiro_speaker

何かごちゃごちゃしている?個人的には、対宇宙戦闘用パックパックを背負っているみたいで気に入っています。

さて、音声合成にはHOYAのVoiceText Web APIを使います。安い音声合成LSI使うより、よほど自然にしゃべらせることができますし、感情とかの設定も可能なすごいAPIです。

利用には登録(APIキーの発行)が必要なので、上記のリンクから先に登録しておきましょう。

サンプルページどおりに、コマンドラインでcurlを実行します。

$ curl "https://api.voicetext.jp/v1/tts" -o "test.wav" -u "取得したAPIキー:" -d "text=こんにちは" -d "speaker=hikari"

これで”test.wav”が生成されるので、

$ aplay test.wav

USBオーディオの設定がちゃんとできていれば、これで再生されるはず。

$ curl "https://api.voicetext.jp/v1/tts" -o "test.wav" -u "取得したAPIキー:" -d "text=こんにちは" -d "speaker=hikari" | aplay -

のようにパイプでやれれば、音声ファイル生成と再生がいっぺんに済んで良かったんですが、これはなぜかaplayのエラーが出てしまいました。原因不明。

さて、次に、以前実装したnode.js+Express Webサーバと組み合わせたいと思います。VoiceText Web APIには便利なnode.js用のライブラリがあるので、それを使わせてもらいます。

インストールは以下のとおり。

$ npm install voicetext --save

“Error: No compatible version found”が出る場合は、npmのバージョンが古いのが原因かもしれませんので、その場合はnpmをアップデートします。

$ npm install -g npm@1.3.0

自分の場合はnpmのバージョンが1.2.17だったので、とりあえず1.3.0系に上げました。

バージョンを上げて再チャレンジすると、今度はError: Invalid name: “Rapiro”というエラーが発生。どうやらnpmの1.3系ではディレクトリ名に大文字は使えないよう。ということで、”Rapiro”ではなく”rapiro”フォルダを作って、その中で

$ npm init
$ npm install express --save
$ npm install voicetext --save

もし、”ERR! File: /home/pi/.npm/debug/2.0.0/package/package.json”のようなエラーが出るなら、指定されているpackage.jsonファイルをチェック。ファイルの中身が空っぽのようなら、ここからdebug 2.0.0のソースを持ってきて、package.jsonのファイルを置き換えて、再度voicetextのインストールを実行。自分の場合は、これで無事インストールできました。

voicetextライブラリでフォローしてくれているのは「音声ファイル(wavファイル)の生成」までなので、生成されるwavファイルを再生する手段は別途用意してやる必要があります。これについては、play.jsを試してみたりしましたが、何かPlay()でエラー出てうまくいかなかったので、とりあえずspawnとやらを使って外部コマンド(aplay)を呼ぶことにします。

では最後、gitのサンプルソースをベースに、VoiceTextをExpress Webサーバに組み込んでみます。「サンプルソースが.jsじゃなくて.coffeeでよくわからん!」という人は、こちらのサイト(CoffeScript公式サイト)の”TRY COFFEESCRIPT”にサンプルソースを貼り付ければ、.jsのコードに変換してくれます。ゆくゆくはCoffeeScriptでサーバも書いてしまいたい気もしますが、とりあえずは.jsで進めます。

とりあえずこんな感じで。

var express = require('express');
var VoiceText = require('voicetext');
var fs = require('fs');
var app = express();
var voice = new VoiceText('取得したAPIキー');
var spawn = require('child_process').spawn;
var Phrases = {
    "ラピロ": "phrases/serve_1.wav",
    "何かご用ですか?": "phrases/serve_2.wav",
    "おはようございます": "phrases/goodmorning.wav"
  };

app.get('/rapiro/control', function (req, res) {
  console.log(req.query);
  var text = req.query.text ? req.query.text : "何かご用ですか?";
  if(text in Phrases){
    playVoice(Phrases);
    res.send(text);
    return;
  }
  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('./temp.wav', buf, 'binary', function(e){
          if(e){
            return console.error(e);
          }
          playVoice("temp.wav");
         })
       });
  res.send(text);
});

var server = app.listen(3000, function () {
  var host = server.address().address
  var port = server.address().port
  console.log('Example app listening at http://%s:%s', host, port)
});

var playVoice = function(path){
  var aplay = spawn('aplay', [path]);
  aplay.stdout.on('data', function(data){
    console.log("stdout: Start playing!");
  });
  aplay.stderr.on('data', function(data){
    console.log("stderr: Start playing!");
  });
  aplay.on('exit', function(code){
    console.log("Finish playing!");
  });
}

getリクエストで、VoiceTextに必要なパラメータを渡すようにしています。こんな感じで。

http://192.168.xxx.xxx:3000/rapiro/control/?text=今日はいい天気ですよ

省略したパラメータについては、デフォルト値で対応します。

また、定型文用にPhrase変数を用意しています。VoiceText Web APIは、その名の通りWeb APIなので、APIを叩けば、都度サーバ側で音声を合成したファイルを作成して送り返してくれます。が、これはやっぱり時間がかかってしまうので、よく出てくるフレーズについては、あらかじめVoiceText Web APIを使って作成しておいたwavファイルを再生するようにしています。これだと、ラピロのWebサーバにアクセスしてから喋り出すまでの時間がだいぶ改善されます。まー、定型文が増えれば増えるほど、どんどん音声ファイルが増えていってしまうという問題がありますが。

さて、これでとりあえず、

「Webサーバにアクセスすればラピロが喋り出す」

というところまではできました。でもまだラピロと対話している感じがするには至らないなあという感じです。

そもそもの問題として、Webサーバにアクセスする側が、ラピロに喋らせる内容を決めているのが気持ち悪いと言えば気持ち悪いのですが、これを解決しようと思うとラピロに人工知能を入れたりとかずいぶんハードルが上がりそうなので、それは一旦保留です。

でも、もう少し改善すれば、対話している感は出せそうな気はします。次にそれに取り組んでみます。