Bluemix의 Node-RED에서 Text to Speech 노드를 사용하여 음성을 읽습니다.

11405 단어 BluemixWatsonnode-red

목적



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)로 변환하여 채팅 화면 상단의 오디오로 재생하도록 구현합니다.

    구현





    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":[]}]
    

    좋은 웹페이지 즐겨찾기