16
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

BluemixのNode-REDでText to Speechノードを使って音声を読み上げる

Posted at

目的

Node-REDとWatsonAPIを使いたい!というわけで、BluemixのNode-REDでText to Speechノードを使い、入力したテキスト(英語)を読み上げるというものを作りました。
API経由で呼び出す方法ではなく、あえてTTSノードを使っています。

環境

  • BluemixのダッシュボードでText-to-Speechのサービスを追加し、Node-REDサーバにバインドします。
  • ベースとなるチャット画面はこちらを参考にインポート。

画面

チャット画面

チャット画面上部にオーディオタグを追加しました。
<audio controls id="audio"></audio>

画面下部の"What do you want to say?"と書いてあるボックスにテキストを入力し、Sendボタンを押します。
入力したテキストをTTSで音声データ(wav)に変換し、チャット画面上部のオーディオで再生するように実装します。

実装

Kobito.DnxvJB.png

text to speechノードの呼び出しはWebSocketを使用しています。
websocketには、入力したテキストをmsg.payloadに設定して渡しています。

    var payload = {
      message: message.value,
      user: user.value,
      ts: (new Date()).getTime(),
      payload: message.value
    };

text to speechで変換された音声データは、戻り値のpayload.speech.dataにバイナリデータ(wav)として設定されています。

"speech": { "type":"Buffer", "data":[82, 73, ...]}

このままではaudio.srcに設定できないので、Blobに変換してcreateObjectURL(blob)して設定します。

function playTTS(payload){
    var wavString = payload.speech.data;
    // console.log(wavString);
    var len = wavString.length;
    // console.log(len);
    var buf = new ArrayBuffer(len);
    // console.log(buf);
    var view = new Uint8Array(buf);
    for (var i = 0; i < len; i++) {
      view[i] = wavString[i] & 0xff;
    }
    console.log(view);
    var blob = new Blob([view], {type: "audio/wav"});
    var URL = window.URL || window.webkitURL
    var blobUrl = URL.createObjectURL(blob);
    // console.log(blobUrl);
    var audio = document.getElementById('audio');
    audio.src = blobUrl;
    audio.load();
    audio.preload = 'auto';
    audio.play();
  }

このあたりの実装方法を調べていたところ、viewに値を突っ込んでいるところで
wavString.charCodeAt(i) & 0xff
となっていましたが、今回はwavStringがStringではなくArrayだったため
wavString[i] & 0xff としています。

charCodeAt(i) & 0xffというのをなぜやらないといけないのかというのは、こちらの説明がわかりやすい。

まとめ

  • TTSノードからの戻り値のバイナリデータをaudioで出力させる方法を調べるのに手間取りました。
  • Bluemix Node-REDのTTSノードは日本語対応していないため、API経由で呼び出したほうがいろいろ使えそうですね。Language Translationで翻訳した文を再生してみるのもいいかもしれません。
  • MacbookのSafariでは再生時にエラーになります。Chromeでは再生できました。解析できていないので、もう少し調べてみないといけませんね。

コード

[{"id":"68a12744.975ed8","type":"websocket-listener","z":"52dbf05.fad241","path":"/ws/english","wholemsg":"true"},{"id":"804b9d82.96fcd","type":"http in","z":"800bc702.676838","name":"","url":"/speech","method":"get","swaggerDoc":"","x":236,"y":349,"wires":[["2cbc3872.627ae"]]},{"id":"2cbc3872.627ae","type":"template","z":"800bc702.676838","name":"html","field":"","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<head>\n  <meta name=\"viewport\" content=\"width=320, initial-scale=1\">\n  <title>speech!</title>\n</head>\n\n<body>\n  <div id=\"wrapper\">\n    <div id=\"chat_box\" class=\"content\">\n            <p><audio controls id=\"audio\"></audio></p>\n    </div>\n\n    <div id=\"footer\">\n      <div class=\"content\">\n        <input type=\"text\" id=\"user\" placeholder=\"Who are you?\" />\n        <input type=\"text\" id=\"message\" placeholder=\"What do you want to say?\" />\n        <input type=\"button\" id=\"send_btn\" value=\"Send\" onclick=\"sendMessage()\">\n      </div>\n    </div>\n  </div>\n</body>\n\n<script type=\"text/javascript\">\n  var wsUri = \"ws://{{req.headers.host}}/ws/english\";\n  var ws = new WebSocket(wsUri);\n\n  function createSystemMessage(message) {\n    var message = document.createTextNode(message);\n\n    var messageBox = document.createElement('p');\n    messageBox.className = 'system';\n\n    messageBox.appendChild(message);\n\n    var chat = document.getElementById('chat_box');\n    chat.appendChild(messageBox);\n  }\n\n  function createUserMessage(user, message) {\n    var user = document.createTextNode(user + ': ');\n\n    var userBox = document.createElement('span');\n    userBox.className = 'username';\n    userBox.appendChild(user);\n\n    var message = document.createTextNode(message);\n\n    var messageBox = document.createElement('p');\n    messageBox.appendChild(userBox);\n    messageBox.appendChild(message);\n\n    var chat = document.getElementById('chat_box');\n    chat.appendChild(messageBox);\n  }\n\n  function playTTS(payload){\n    var wavString = payload.speech.data;\n    // console.log(wavString);\n    var len = wavString.length;\n    // console.log(len);\n    var buf = new ArrayBuffer(len);\n    // console.log(buf);\n    var view = new Uint8Array(buf);\n    for (var i = 0; i < len; i++) {\n      view[i] = wavString[i] & 0xff;\n    }\n    console.log(view);\n    var blob = new Blob([view], {type: \"audio/wav\"});\n    var URL = window.URL || window.webkitURL\n    var blobUrl = URL.createObjectURL(blob);\n    // console.log(blobUrl);\n    var audio = document.getElementById('audio');\n    audio.src = blobUrl;\n    audio.load();\n    audio.preload = 'auto';\n    audio.play();\n  }\n\n  ws.onopen = function(ev) {\n    createSystemMessage('[Connected]');\n  };\n\n  ws.onclose = function(ev) {\n    createSystemMessage('[Disconnected]');\n  }\n\n  ws.onmessage = function(ev) {\n    var payload = JSON.parse(ev.data);\n    createUserMessage(payload.user, payload.message);\n    playTTS(payload);\n\n    var chat = document.getElementById('chat_box');\n    chat.scrollTop = chat.scrollHeight;\n  }\n\n  function sendMessage() {\n    var user = document.getElementById('user');\n    var message = document.getElementById('message');\n\n    var payload = {\n      message: message.value,\n      user: user.value,\n      ts: (new Date()).getTime(),\n      payload: message.value\n    };\n\n    ws.send(JSON.stringify(payload));\n    message.value = \"\";\n  };\n</script>\n\n<style type=\"text/css\">\n  * {\n    font-family: \"Palatino Linotype\", \"Book Antiqua\", Palatino, serif;\n    font-style: italic;\n    font-size: 24px;\n  }\n\n  html, body, #wrapper {\n    margin: 0;\n    padding: 0;\n    height: 100%;\n  }\n\n  #wrapper {\n    background-color: #ecf0f1;\n  }\n\n  #chat_box {\n    box-sizing: border-box;\n    height: 100%;\n    overflow: auto;\n    padding-bottom: 50px;\n  }\n\n  #footer {\n    box-sizing: border-box;\n    position: fixed;\n    bottom: 0;\n    height: 50px;\n    width: 100%;\n    background-color: #2980b9;\n  }\n\n  #footer .content {\n    padding-top: 4px;\n    position: relative;\n  }\n\n  #user { width: 20%; }\n  #message { width: 68%; }\n  #send_btn {\n    width: 10%;\n    position: absolute;\n    right: 0;\n    bottom: 0;\n    margin: 0;\n  }\n\n  .content {\n    width: 70%;\n    margin: 0 auto;\n  }\n\n  input[type=\"text\"],\n  input[type=\"button\"] {\n    border: 0;\n    color: #fff;\n  }\n\n  input[type=\"text\"] {\n    background-color: #146EA8;\n    padding: 3px 10px;\n  }\n\n  input[type=\"button\"] {\n    background-color: #f39c12;\n    border-right: 2px solid #e67e22;\n    border-bottom: 2px solid #e67e22;\n    min-width: 70px;\n    display: inline-block;\n  }\n\n  input[type=\"button\"]:hover {\n    background-color: #e67e22;\n    border-right: 2px solid #f39c12;\n    border-bottom: 2px solid #f39c12;\n    cursor: pointer;\n  }\n\n  .system,\n  .username {\n    color: #aaa;\n    font-style: italic;\n    font-family: monospace;\n    font-size: 16px;\n  }\n\n  @media(max-width: 1000px) {\n    .content { width: 90%; }\n  }\n\n  @media(max-width: 780px) {\n    #footer { height: 91px; }\n    #chat_box { padding-bottom: 91px; }\n\n    #user { width: 100%; }\n    #message { width: 80%; }\n  }\n\n  @media(max-width: 400px) {\n    #footer { height: 135px; }\n    #chat_box { padding-bottom: 135px; }\n\n    #message { width: 100%; }\n    #send_btn {\n      position: relative;\n      margin-top: 3px;\n      width: 100%;\n    }\n  }\n</style>\n","x":425,"y":348,"wires":[["73d2e2e0.a0d35c"]]},{"id":"73d2e2e0.a0d35c","type":"http response","z":"800bc702.676838","name":"","x":607,"y":348,"wires":[]},{"id":"9852e538.cee01","type":"websocket in","z":"800bc702.676838","name":"","server":"68a12744.975ed8","client":"","x":187,"y":257,"wires":[["600df703.08308"]]},{"id":"600df703.08308","type":"function","z":"800bc702.676838","name":"","func":"delete msg._session;\n//msg.payload = msg.message;\nreturn msg;\n\n","outputs":1,"noerr":0,"x":351,"y":257,"wires":[["b8226832.18236"]]},{"id":"8a76e158.d0166","type":"websocket out","z":"800bc702.676838","name":"","server":"68a12744.975ed8","client":"","x":737,"y":258,"wires":[]},{"id":"b8226832.18236","type":"watson-text-to-speech","z":"800bc702.676838","name":"","lang":"english","voice":"en-US_MichaelVoice","format":"audio/wav","x":532,"y":258,"wires":[["8a76e158.d0166","b7ce263.5b52858"]]},{"id":"b7ce263.5b52858","type":"debug","z":"800bc702.676838","name":"","active":false,"console":"false","complete":"true","x":731,"y":204,"wires":[]}]
16
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?