はじめに
こんにちは、キャスレーコンサルティングLS(リーディング・サービス)部の青木です。
今回は Node.js + socket.ioで、簡単なチャットアプリを作成し、
さらに、Web Speech API を使用して
音声のみによる、チャットメッセージの送信と
受信メッセージの自動読み上げを実現してみようと思います。
たとえキーボードの入力の仕方が分からなくても、
両手が荷物でいっぱいでも、このアプリを使えばチャットを行うことができます。
いつでも簡単に、チャットをしましょう。
目次
・チャット機能の作成
・メッセージ読み上げ機能の作成
・音声によるメッセージ送信機能の作成
・おわりに
音声チャットアプリの全体像
構成図
音声チャットアプリの構成は、下記となります。
図1.音声チャットアプリ構成図
Client1 の役割
Speech Recognition API がマイクを通して音声を受信し、発話内容をテキスト形式へ変換します。
2. 送信メッセージの確認
Speech Synthesis API がテキストを読み上げ、送信メッセージの確認を行います。
3. ChatServerにテキストメッセージを送信
socket.io がテキストメッセージを、ChatServerに送信します。
ChatServer の役割
socket.io が、メッセージの送受信を行います。
Client2 の役割
socket.io が、ChatServerからのテキストメッセージを受信します。
2. 受信したテキストメッセージの発話
Speech Synthesis API が、受信したテキストメッセージを読み上げます。
チャットメッセージ送信の流れ
チャットメッセージを送信するまでの、UserとClientとのやり取りの流れは下記となります。
図2.チャットメッセージ送信の流れ
ディレクトリ構成と主要ファイル
【Web】 ドキュメントルート ┃ ┗━ chatRoom/ ┃ ┗━ index.html ┃ ┗━ room.html ┃ ┗━ js/ ┃ ┗━ SocketIOClass.js ┃ ┗━ room.js 【ChatServer】 プロジェクトルート ┃ ┗━ chat.js
チャット機能の作成
以下ではチャット機能を実現する各ファイルと、その内容についての説明を行っていきます。
Client側のファイル
index.html
[役割]
入室するチャットルームの選択と、入室者情報の入力を行います。
※画面側(HTML,CSS)の実装内容は、省略します。

room.html
[役割]
チャットメッセージの送受信を行います。
※画面側(HTML,CSS)の実装内容は、省略します。

また、room.htmlでは下記のJavaScriptファイルを、インクルードします。
━ SocketIOClass.js
━ room.js
※socket.io.js socket.ioを使用するための、クライアント用ライブラリ。 Nodeサーバーのファイルを取得して、読み込みます。
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D%22text%2Fjavascript%22%20charset%3D%22UTF-8%22%20src%3D%22https%3A%2F%2Fxxx.xxx.xxx.xxx%3A3000%2Fsocket.io%2Fsocket.io.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" /> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D%22text%2Fjavascript%22%20charset%3D%22UTF-8%22%20src%3D%22.%2Fjs%2FSocketIOClass.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" /> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D%22text%2Fjavascript%22%20charset%3D%22UTF-8%22%20src%3D%22.%2Fjs%2Froom.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" />
SocketIOClass.js
[役割]
'use strict' class SocketIOClass { constructor(userId) { // ChatServerへ接続します (xxx.xxx.xxx.xxx にはChat ServerのIPアドレスを指定します) this.socket = io.connect('xxx.xxx.xxx.xxx:3000'); this.userId = userId; this.eventInit(); } eventInit() { // ChatServerからメッセージを受信した際に実行される処理を定義します this.socket.on('message', (msg) => { // 受信したメッセージを画面に表示します addMsgToScreen(msg, this.userId); }); } emit(evenName, msgObj) { // チャットメッセージをChatServerに送信します this.socket.emit(evenName, msgObj); } }
room.js
[役割]
'use strict' $(function() { // チャットルームへの入室者情報を生成します const userId = /* 入室ユーザのIDを取得します */, userName = /* 入室ユーザの名前を取得します */, roomId = /* 入室するチャットルームのIDを取得します */, joinMessage = { userId : userId, userName : userName, roomId : roomId }; // チャットルームに入室します const socketIO = new SocketIOClass(userId); socketIO.emit( 'join', joinMessage ); // チャットメッセージのsubmitボタンを押下した際の処理を定義します $("#messge-submit-btn").on("click", () => { const message = /* 画面で入力されたチャットの送信メッセージを取得します */, msgObj = { from : userId, to : 'all', message : message }; // ChatServerにメッセージを送信します socketIO.emit( 'message', msgObj ); }); }); /** * ChatServerから送信されてきたメッセージを画面に表示します *(SocketIOClass.jsから呼び出される関数) */ function addMsgToScreen(msg, myUserId) { // msg.messageに格納されているテキストメッセージを画面に表示します // 処理が画面側の実装に依存するため、詳細は省略します }
ChatServer側(Node.js)のファイル
chat.js
[役割]
const https = require('https'), express = require('express'), app = express(), fs = require( 'fs' ), options = { key : fs.readFileSync( './ssl/server_key.key' ), cert : fs.readFileSync( './ssl/server_crt.pem' ) }, server = https.createServer(options,app), // socket.ioにChatServerのポートをListenさせます io = require('socket.io').listen(server); // socketの接続ユーザ情報を保持する領域を初期化します let dataStore = {}; io.sockets.on("connection", (socket) => { // Clientがsocketへの接続を確立した際に実行されます // 以下で、確立したsocketに対し各種のイベントが発火した際の処理を定義します socket.on( 'join', (msg) => { // roomIdの値をIDとするチャットルームへの入室を行います socket.join(msg.roomId, () => { // socket.ioの入室処理後のコールバック関数の処理を定義します const usrObj = { userId : msg.userId, userName : msg.userName, roomId : msg.roomId } // 入室者情報を保持します dataStore[socket.id] = usrObj; }); }); socket.on( 'message', (msg) => { // 今回のサンプルソースでは、[msg.to]の値は全て'all'になります(クライアント側でメッセージ送信時に設定を行っています) if(msg.to === 'all') { // 自分自信が入室しているチャットルーム全体にメッセージを送信します io.sockets.to(dataStore[socket.id].roomId).emit('message', msg) } }); socket.on( 'disconnect', () => { // disconnect が発火した時点でsocketのroomからの退室処理は完了しています // 入室者情報を削除します delete dataStore[socket.id] }); }); // ChatServerにListenポートを設定します server.listen(3000);
チャットルーム画面でメッセージを入力し、submitボタンをクリックすることで、
同じチャットルームに入室しているClientに、チャットメッセージを送信することができます。


メッセージ読み上げ機能の追加
続いて、音声機能の作成を行っていきたいと思います。
まずは受信したメッセージの読み上げ機能を、作成します。
メッセージの読み上げ機能は、W3Cコミュニティグループによって仕様が策定され、
各ブラウザへの実装が行われている、 Web Speech API の Speech Synthesis API を使用して作成していきます。
room.js
以下の関数に処理を追加します。
/** * ChatServerから送信されてきたメッセージを画面に表示します *(SocketIOClass.jsから呼び出される関数) */ function addMsgToScreen(msg, myUserId) { // msg.messageに格納されているテキストメッセージを画面に表示します // 処理が画面側の実装に依存するため、詳細は省略します const from = msg.from, isMyselfMessage = (from === myUserId); // 受信したテキストを読み上げます(自分が送信したメッセージの読み上げは行いません) if (!isMyselfMessage) { let synthesis = new SpeechSynthesisUtterance(); synthesis.text = msg.message; speechSynthesis.speak(synthesis); } }
以上です。
上記の処理の追加のみで、ブラウザが自動でチャットの受信メッセージを読み上げてくれます。

PC:「こんにちは」
おおー。
とても簡単です。
音声によるメッセージ送信機能の追加
音声によるメッセージ送信機能は、Web Speech API の Speech Recognition API を使用して作成していきます。
room.js
room.jsに処理を追加し、以下のようにします。
'use strict' $(function() { // チャットルームへの入室者情報を生成します const userId = /* 入室ユーザのIDを取得します */, userName = /* 入室ユーザの名前を取得します */, roomId = /* 入室するチャットルームのIDを取得します */, joinMessage = { userId : userId, userName : userName, roomId : roomId }; // チャットルームに入室します const socketIO = new SocketIOClass(userId); socketIO.emit( 'join', joinMessage ); // submitボタンを押下した際の処理を定義します $("#messge-submit-btn").on("click", () => { const message = /* 画面で入力されたチャットの送信メッセージを取得します */, msgObj = { from : userId, to : 'all', message : message }; // ChatServerにメッセージを送信します socketIO.emit( 'message', msgObj ); }); /** * 以下の処理が音声によるメッセージ送信機能の追加部分 */ /** 音声認識用インスタンス */ let recognition = new webkitSpeechRecognition(); recognition.lang = "ja"; /** 送信メッセージ */ let submitMsg; // 会話の段階を制御するフラグ /** apiが発話するために音声認識が終了されたかどうか */ let stopForApiSpeech = false; /** チャットの送信メッセージの音声認識中かどうか */ let chatInputStart = false; /** チャットの送信メッセージの確認中かどうか */ let chatSubmitConfirmed = false; // 音声認識中にマイクからのinputを受信した際の処理を定義します recognition.addEventListener('result', (e) => { let inputMsg = e.results[0][0].transcript; if (!chatInputStart) { // チャット初期状態の場合 if ( inputMsg == "チャット スタート" ) { execApiSpeech("スピークしてくれ。"); chatInputStart = true; return; } // メッセージが "チャット スタート" ではない場合は何もしないで処理を終了させます return; } // 以下、チャット初期状態ではない場合 if (inputMsg == "チャットエンド") { execApiSpeech("スピーク終わり。"); submitMsg = ""; chatInputStart = false; chatSubmitConfirmed = false; return; } if (!chatSubmitConfirmed) { // チャットメッセージの受付中の場合 execApiSpeech(inputMsg + "。を送信しますよ。"); submitMsg = inputMsg; chatSubmitConfirmed = true; return; } // 以下、チャットメッセージの受付中ではない場合(チャットメッセージ確認中の場合) if ( inputMsg == "送信" ) { const msgObj = { from: userId, to: 'all', message: submitMsg } // ChatServerにメッセージを送信します socketIO.emit( 'message', msgObj ); submitMsg = ""; chatSubmitConfirmed = false; } else if ( inputMsg == "キャンセル" ) { execApiSpeech("メッセージを取り消しました。"); submitMsg = ""; chatSubmitConfirmed = false; } else { execApiSpeech("もう一度言ってください。"); } }); // 音声認識を終了した際の処理を定義します recognition.addEventListener('end', () => { if (stopForApiSpeech) { // APIが発話するために音声認識を停止した場合は // 音声認識の自動再スタートは行いません stopForApiSpeech = false; return; } // 音声認識を自動再スタートさせます recognition.start(); }); /** * SpeechSynthesisUtteranceインスタンスを取得します */ function getSpeechSynthesisUtterance(msg) { // SpeechSynthesisUtteranceインスタンの設定を行います let synthesis = new SpeechSynthesisUtterance(); synthesis.lang = 'ja'; synthesis.text = msg; // 発話が終了した際の処理を定義します synthesis.addEventListener('end', () => { // 発話終了後に音声認識を再スタートさせます recognition.start(); }); return synthesis; } /** * SpeechSynthesisUtteranceインスタンスに発話をさせます */ function execApiSpeech(msg) { // 発話をする前に、音声認識を停止します stopForApiSpeech = true; recognition.stop(); // 発話します let synthesis = getSpeechSynthesisUtterance(msg); speechSynthesis.speak(synthesis); } // 音声認識を開始します(js読み込み時に開始します) recognition.start(); }); /** * ChatServerから送信されてきたメッセージを画面に表示します *(SocketIOClass.jsから呼び出される関数) */ function addMsgToScreen(msg, myUserId) { // msg.messageに格納されているテキストメッセージを画面に表示します // 処理が画面側の実装に依存するため、詳細は省略します const from = msg.from, isMyselfMessage = (from === myUserId); // 受信したテキストを読み上げます(自分が送信したメッセージの読み上げは行いません) if (!isMyselfMessage) { let synthesis = new SpeechSynthesisUtterance(); synthesis.text = msg.message; speechSynthesis.speak(synthesis); } }
以上、です。
上記の処理の追加で、音声のみでチャットを行うことができるようになります。
私:「チャットスタート」
PC:「スピークしてくれ。」
私:「こんにちは」
PC:「こんにちは。を送信しますよ。」
私:「送信」

おおー。
PCに一回も触らずに声だけで、チャットメッセージを送信することができました。

PC:「こんにちは」
受信側では、もちろんメッセージを読み上げてくれます。
おわりに
チャットは、キーボードで文字を入力するのが普通ですが、
今回作成したアプリではキーボードに触らずに、チャットを行うことができました。
また、PCにメッセージを読み上げてもらうことも、実現することができました。
これまでは「手」と「目」を使わないとできなかったことが、
今回のチャレンジで「声」と「耳」のみで行うことが、できるようになりました。
もしかしたら数年後には、「手」も「目」も「声」も「耳」も使わなくても
メッセージのやり取りが行える日が来るかもしれませんね。
以上となります。
最後まで読んでくださり、ありがとうございました。