人工知能
Watson

WatsonのSTTのWebScoketをやってみた

Watson STTのストリーミング

以前、Watsonをバッチ的に呼び出したので今度はWebSocketを用いたストリーミングを実装してみます。
まずはマニュアルを読み進めていくが、SDKを使います。言語はJavaで。
ソースはここ

事前準備

Bluemixとの契約。クレジットがある方はクレジットを用意してアカウントを作って下さい。
ここでは既にあるものとして、進めます。

ログイン後、左のメニューからWatsonを選び「SpeechToText」を選択します。

  • サービス名 Test
  • 資格情報名 Credentials-1
  • デプロイする地域 米国南部
  • 組織の選択 test1
  • スペース develop

として作成をします。作成が完了するとサービス資格情報の先ほどの資格情報名をクリックすると
* url
* username
* password

が表示されるので、メモっておきます。

プログラム

ドキュメントを読むとWebSocketでWatsonに接続は可能だが、音声ファイルを作る必要があるっぽい。
ということでJavaScriptからサーバへWebSocketを使い、さらにサーバからWatsonへWebsocketで接続をするという方式で行う。
まずはHTMLは以下の通りです。

index.html
<!doctype html>
<html>
<head>
    <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
    <script src="https://cdn.WebRTC-Experiment.com/MediaStreamRecorder.js"></script>
    <script src="https://cdn.WebRTC-Experiment.com/gumadapter.js"></script>
</head>
<body>
    <button id="start">Start</button>
    <button id="stop">Stop</button>
    <div id="result"></div>

    <script type="text/javascript">

    let websocket;
    const timeInterval = 1000;
    let mediaRecorder;

    function start(onSuccess,onError){
        navigator.mediaDevices.getUserMedia({video: false, audio: true})
        .then(onSuccess).catch(onError);
    }


    function initwebSocket(){
        let protocol = (document.location.protocol == 'https:' ? 'wss:' : 'ws:');
        const Uri=protocol+'//'+document.location.hostname + ':'
        + document.location.port
        + document.location.pathname
        + 'Recognize';
        websocket = new WebSocket(Uri);
        websocket.binaryType = 'arraybuffer';
        websocket.onconnect = function (e) {
            console.log('connect:' + e.msg);
        };
        websocket.onerror = function (e) {
            console.log(e.msg);
        };
        websocket.onclose = function (e) {
            console.log('close:' + e.msg);
        };
        websocket.onmessage = function (e) {
            showResult(e.data)
        };
    }
    function showResult(result){
        var json = JSON.parse(result);
        var get_data=JSON.parse(json['result']);
        if(get_data['results'].length >0){
            for(var i=0; i< get_data['results'].length; i++){
                var row=document.querySelector('#result');
                var str = get_data['results'][i]['alternatives'][0]['transcript'].replace( /D_/g , "" ) ;
                row.textContent = row.textContent+str;
            }

        }
    }

    //Stop stream
    function AudioStop(){
        mediaRecorder.stop();
        mediaRecorder.stream.stop();
    }
    document.querySelector('#start').onclick=function(){
        initwebSocket();
        start(onMediaSuccess,onMediaError);
    }


    document.querySelector('#stop').onclick=function(){
        AudioStop();
        websocket.close();
    }

    function onMediaSuccess(stream) {
        var audio = document.createElement('audio');
        audio.controls = 'controls';
        audio.muted = 'muted';
        audio.src=window.URL.createObjectURL(stream);
        audio.play();

        mediaRecorder = new MediaStreamRecorder(stream);
        mediaRecorder.stream = stream;
        mediaRecorder.recorderType = StereoAudioRecorder;
        mediaRecorder.mimeType = 'audio/wav';

        mediaRecorder.audioChannels = 1; //mono
        mediaRecorder.ondataavailable = function (blob) {
            websocket.send(blob);
        };
        mediaRecorder.start(timeInterval);
    }

    function onMediaError(error){
        console.log("error:"+error);
    }
  </script>
</body>
</html>

次にサーバ側。
```java:Recognize
package com.sou.ai;

import java.nio.ByteBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.websocket.CloseReason;
import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import com.sou.ai.util.SoundUtil;

@ServerEndpoint("/Recognize")
/**
* サーバ側のWebSocketの受け側
*
*
*/
public class Recognize {
private static final Logger LOGGER = Logger.getLogger(Recognize.class.getName());

@OnOpen
public void onOpen(Session session, EndpointConfig ec) {
    LOGGER.log(Level.INFO, "Connect");
}
/**
 * WebSocketのデータを受け取る
 *
 * @param session WebSocket session
 * @param buffer ブラウザからのwavファイルのバイナリ
 */
@OnMessage
public void onMessage(Session session, ByteBuffer buffer){
    LOGGER.log(Level.INFO, "Get ByteMessage Size:"+buffer.capacity());
    sendWatsonStt(session,buffer);

}


@OnClose
public void onClose(Session session, CloseReason reason) {
    LOGGER.log(Level.INFO, "Close WebSocket Session:"+session+":"+reason);
}

@OnError
public void onError(Session session, Throwable t) {
    LOGGER.log(Level.WARNING, "WebSocket Error:", t);
}

/**
 * Watsonにデータを送る
 *
 * @param session WebSocket session
 * @param buffer ブラウザからのwavファイルのバイナリ
 */
private void sendWatsonStt(Session session,ByteBuffer buffer)  {
    LOGGER.log(Level.INFO, "Create SoundData:");

    byte[] orignData = buffer.array();
    SoundUtil soundUtil = new SoundUtil();
    String filepath  = soundUtil.changeMonoSoundData(orignData);

    WatsonSttService wt = new WatsonSttService();
    wt.sendSttData(session,filepath);
}

}
```

音声ファイルまわり

SoundUtil.java
package com.sou.ai.util;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.sound.sampled.AudioFileFormat.Type;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;

/**
 *
 * @author sou
 * 音声データを変換する
 */
public class SoundUtil {
    private static final Logger LOGGER = Logger.getLogger(SoundUtil.class.getName());

    private final static float SAMPLE_RATE=16000;
    private final static int CHANNEL=1;
    private final static int SAMPLE_SIZE=16;
    private final static String  WAV_DIR="\tmp\";
    private final static String WAV_FILE="temp.wav";

    public String changeMonoSoundData(byte[] orignData) {
        LOGGER.log(Level.FINE, "changeMonoSoundData");
        AudioInputStream stream = null;
        try {
            stream = AudioSystem.getAudioInputStream(new ByteArrayInputStream(orignData));
            AudioFormat af =stream.getFormat();
            float sample_rate=af.getSampleRate();
            int channels = af.getChannels();
            int sample_size=af.getSampleSizeInBits();
            if((sample_rate ==SAMPLE_RATE) && (channels ==CHANNEL ) && (sample_size ==SAMPLE_SIZE ) ){
                saveWavFile(stream);
            }
            else{
                //サンプリングが異なる場合はモノラルチャンネル1、16000Hz,16ビットにデータを変換する
                CreateMonoSoundData(stream);
            }
        } catch (UnsupportedAudioFileException | IOException e) {
            LOGGER.log(Level.SEVERE, "changeMonoSoundData Can't get Audio:"+e.getMessage());
        }

        return WAV_DIR+WAV_FILE;
    }

    private boolean saveWavFile(AudioInputStream stream) {
        LOGGER.log(Level.FINE, "saveWavFile");
        boolean rec_flag=false;
        try {

            AudioSystem.write(stream, Type.WAVE, new File(WAV_DIR+WAV_FILE));
            rec_flag=true;
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "saveWavFile Can't carete audio file:"+e.getMessage());
        }
        return rec_flag;



    }

    private boolean CreateMonoSoundData(AudioInputStream stream) {
        AudioFormat targetformat = new AudioFormat(SAMPLE_RATE, SAMPLE_SIZE, CHANNEL, true, false);
        AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(targetformat, stream);
        boolean rec_flag=saveWavFile(audioInputStream);
        return rec_flag;

    }

}

Watsonにデータを送る部分。これのUsingWebSocketがポイント

WatsonSttService.java
package com.sou.ai;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.websocket.Session;

import com.google.gson.Gson;
import com.ibm.watson.developer_cloud.http.HttpMediaType;
import com.ibm.watson.developer_cloud.speech_to_text.v1.SpeechToText;
import com.ibm.watson.developer_cloud.speech_to_text.v1.model.RecognizeOptions;
import com.ibm.watson.developer_cloud.speech_to_text.v1.model.SpeechResults;
import com.ibm.watson.developer_cloud.speech_to_text.v1.websocket.BaseRecognizeCallback;

/**
 *
 * @author sou
 * Watsonのサービスを呼び出す
 */
public class WatsonSttService {
    private static final Logger LOGGER = Logger.getLogger(WatsonSttService.class.getName());
    private String watson_username;
    private String watson_password;

    SpeechToText service;
    public WatsonSttService()  {
        LOGGER.log(Level.FINE, "WatsonSttService");
        MessageProperties m = new MessageProperties();
        try {
            this.watson_username = m.get_watson_username();
            this.watson_password = m.get_watson_password();
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Can't get Propeties:"+e.getMessage());
        }


        service = new SpeechToText();
        service.setUsernameAndPassword(this.watson_username, this.watson_password);
    }

    public void sendSttData(Session session, String filepath) {
        LOGGER.log(Level.FINE, "sendSttData");

        RecognizeOptions options = new RecognizeOptions.Builder()
                  .model("ja-JP_BroadbandModel").contentType(HttpMediaType.AUDIO_WAV)
                  .interimResults(false).build();

                BaseRecognizeCallback callback = new BaseRecognizeCallback() {
                  @Override
                  public void onTranscription(SpeechResults speechResults) {
                    Response response = new Response();
                    response.setResult(speechResults.toString());

                    Gson gson = new Gson();
                    try {
                        session.getBasicRemote().sendText(gson.toJson(response));
                    } catch (IOException e) {
                        LOGGER.log(Level.SEVERE, "sendSttData Falied Message to Client:"+e.getMessage());
                    }
                  }
                  @Override
                  public void onDisconnected() {

                  }
                };

                try {
                  service.recognizeUsingWebSocket
                    (new FileInputStream(filepath), options, callback);
                }
                catch (FileNotFoundException e) {
                    LOGGER.log(Level.SEVERE, "sendSttData Can't Read Filet:"+e.getMessage());
                }


    }

}

Response.java
package com.sou.ai;


public class Response {


    private String result;


    public String getResult() {
        return result;
    }
    public void setResult(String result) {
        this.result = result;
    }


}
MessageProperties.java
package com.sou.ai;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class MessageProperties
{
    private static Properties conf = new Properties();

    private static MessageProperties instance = new MessageProperties();

    /**
     * コンスラクタ
     */
    public static MessageProperties getInstance()
    {
        return instance;
    }

    /**
     * ファイルから内容を読み込む
     */
    MessageProperties()
    {

        String fileName =
                MessageProperties.class.getClassLoader().getResource("appconfig.properties").getPath();
        try (FileInputStream fi = new FileInputStream(fileName)) {
            // ファイルから読み込み
            conf.load(fi);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * message.peropertiesから値を取得する
     *
     * @throws IOException ファイルが開けない
     */
    public String get_watson_username() throws IOException
    {
        return conf.getProperty("watson_username");
    }
    public String get_watson_password() throws IOException
    {
        return conf.getProperty("watson_password");
    }
}

という感じで作成する。詳細はgithubを参照。
これを実行すると
音声のがテキストで返ってくるのがリアルタイムに出てくる。
微妙に認識が弱い気もするけど・・・。