できること
MESH*アプリ上で、音声認識を使うことができます。
認識したい言葉を入力すれば、その言葉が認識された時に出力コネクタから次のタグへ信号が送られ、好きなアクションを起こすことができます。
音声認識タグの動作テスト動画:https://youtu.be/Y-4yjgwN9cA
*MESHとは、小さな便利を形にできる、ブロック形状の電子タグです。
- MESH:小さな便利を形にできる、ブロック形状の電子タグ
作成した理由
現状でもIFTTT連携タグやGoogle Assistantタグを持ちいれば、Google Home経由で音声認識はできますが、ラピッドプロタイピング時にMESHを利用する事が多く、下記の理由があったので新たに自分で作ることにしました。
- 認識開始の合図のためのウェイクワード(OK,Google など)を無くしたい
- 音声認識してからアクションが実行されるまでの時間を短くしたい
- MESHアプリとIFTTTアプリの設定の行き来が面倒で、細かい設定はMESHアプリ上で完結したい
実装方法
仕組み
下図のような構成で、Raspberry Pi側で音声認識した内容をGoogleスプレッドシートに逐次記録をしていき、記録されたデータをMESHのカスタムタグ側で定期的にリクエストをしています。レスポンスデータが認識したい言葉として入力したテキストと部分一致するか文字列の類似度が高ければ、アクション側に信号が通知するようになっています。
必要なもの
- MESH SDK アカウント
- MESHアプリが動作するタブレット or スマホ
- Googleアカウント
- Raspberry Pi3 mdoel B(microSDやモバイルバッテリーなども含む)
- USBマイク
- Wi-Fi環境
Raspberry Pi側の実装(Web Speech API)
chromiumのブラウザで開くと下記のような動作で音声認識がされます。
手順
①~③はRaspberry Pi上ではなくwindows上などで作業すると楽です。
①Googleドライブ上で、「新規」> 「その他」> Google Apps Scripts」を選んでください。
②コード.jsは最初からありますが、index.htmlはないので「ファイル」 > 「新規作成」 > 「HTMLファイル」で作成してください。各コードは下記にありますので、必要部分を変更してください。
③コード完成後、「公開」>「ウェブアプリケーションとして導入」で作成して下さい。
④作成されたURLをRaspberry Pi上でchromiumブラウザで開いてください。
⑤chromiumで「開始」ボタンを押して「ERROR:audio-capture 」が出た場合は、ブラウザ右上のビデオカメラアイコンを押して、適切なモノに切り替えて再度トライしてください。
Raspberry Piの初期設定やUSBマイクを利用するに当たって、下記のサイトを参考にしました。
- 第56回「改めましてラズベリーパイの基本!(1) Raspberry Pi NOOBSインストール 2017年度版」
- Raspberry Piで音声認識・音声合成する方法(Raspbian Stretch/Jessie対応)
コード.js
url部分のYOUR_SPREAD_SHEET_IDはWEB API側で用いるスプレッドシートのIDを入れてください。
var url = "https://docs.google.com/spreadsheets/d/YOUR_SPREAD_SHEET_ID/edit#gid=0";
var spreadsheet = SpreadsheetApp.openByUrl(url);
var sheet = spreadsheet.getSheetByName("text");
function doGet(e) {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
function writeSheet(_status, _text, _confidence){
var lastRow;
if(_status == "complete"){
lastRow = sheet.getLastRow();
}
else{
//新たに認識を始めたら次の行に移動する
if(sheet.getRange('D1').getValue() == "complete"){
lastRow = sheet.getLastRow() + 1;
}
//認識中のため同じ行のまま
else{
lastRow = sheet.getLastRow();
}
}
sheet.getRange('D1').setValue(_status).setBackgroundColor('#DDDDDD');
var date = new Date();
sheet.getRange(lastRow, 1).setValue(_text);
sheet.getRange(lastRow, 2).setValue(_confidence);
sheet.getRange(lastRow, 3).setValue(Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy/MM/dd,HH:mm:ss'));
}
index.html
Web Speech API部分の実装は下記のページをかなり参考にして、多少改良しました。
- Web Speech APIで途切れない音声認識
<!DOCTYPE html>
<html>
<body onLoad="vr_function()">
<p>認識されている音声は、確定前は『グレー色』、確定後は『黒色』<br>
このページを起動すると音声認識が自動で開始されます。</p>
<p>現在の状態<br>
<textarea id="status" cols="100" rows="1">
</textarea>
</p>
<p>音声認識されている言葉<br>
<textarea id="result_text" cols="100" rows="10">
</textarea>
</p>
<!-- <input type="button" onClick="vr_function(true);" value="音声認識開始" input id="start_button"> -->
</body>
<head>
<meta charset="UTF-8">
<title>Web Speech API</title>
<script type="text/javascript">
var form = document.forms[ document.forms.length - 1 ];
var progress = form.getElementsByTagName( 'ol' )[ 0 ];
var flag_speech = 0;
function vr_function() {
window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
if( ! window.SpeechRecognition ) {
window.alert("お使いのブラウザでは、Speech APIはサポートされていません。");
}
else{
var recognition = new webkitSpeechRecognition();
recognition.lang = 'ja';
recognition.interimResults = true;
recognition.continuous = true;
recognition.onsoundstart = function() {
document.getElementById('status').innerHTML = "音声認識中";
};
recognition.onnomatch = function() {
document.getElementById('status').innerHTML = "もう一度試してください";
};
recognition.onerror = function() {
document.getElementById('status').innerHTML = "エラー";
if(flag_speech == 0)
vr_function();
};
recognition.onsoundend = function() {
document.getElementById('status').innerHTML = "停止中";
vr_function();
};
recognition.onresult = function(event) {
var results = event.results;
var confidence_value;
for (var i = event.resultIndex; i < results.length; i++) {
confidence_value = results[ i ][ 0 ].confidence;
//認識確定
if (results[i].isFinal)
{
google.script.run.writeSheet('complete', results[i][0].transcript, confidence_value);
document.getElementById('result_text').innerHTML = results[i][0].transcript;
document.getElementById('result_text').style.color = 'black';
vr_function();
}
//認識途中
else
{
google.script.run.writeSheet('working', results[i][0].transcript, confidence_value);
document.getElementById('result_text').innerHTML = results[i][0].transcript;
document.getElementById('result_text').style.color = 'gray';
flag_speech = 1;
}
}
}
flag_speech = 0;
document.getElementById('status').innerHTML = "start";
recognition.start();
}
}
</script>
</head>
</html>
WEB API側の実装(Google Apps Scripts)
Raspberry Piで音声認識を実行中に、下図のようにスプレッドシートに認識結果を追加していきます。
最後に認識したものだけを同じセルに上書きして行っても良いのですが、ログ確認のため次セルに追記するようになっています。
手順
①Googleドライブ上で、「新規」> 「Google スプレッドシート」を選んでください。
②スプレッドシートを開いたら、「ツール」>「スクリプトエディタ」を選んでください。
③コード完成後、「公開」>「ウェブアプリケーションとして導入」で作成して下さい。
④何も問題無ければ、Web Speech Text側で音声認識を行うと、スプレッドシートに認識されたデータが追加されていきます。
コード.js
id部分のYOUR_SPREAD_SHEET_IDはWEB API側で用いるスプレッドシートのIDを入れてください。
スプレッドシートを開いた時のURLの下記の黒塗りの部分です。
function doGet(e) {
//受け取るパラメータを用意する
var param = e.parameter.param;
var rowData = {}; //JSONオブジェクト格納用の入れ物
var result;
if (e.parameter == undefined) {
//パラメータ不良の場合はundefinedで返す
rowData.value = 'undefined';
//return ContentService.createTextOutput(result);
} else {
var id = 'YOUR_SPREAD_SHEET_ID'; //スプレッドシートのID
var sheet = SpreadsheetApp.openById(id).getSheetByName('text');
var rows = sheet.getDataRange().getValues();
var p_name = rows.splice(0, 1)[0];
var last_row = sheet.getLastRow();
switch(param){
case 'latest':
rowData.state = sheet.getRange("D1").getValue();
rowData.text = sheet.getRange(last_row, 1).getValue();
rowData.confidence = sheet.getRange(last_row, 2).getValue();
rowData.time = sheet.getRange(last_row, 3).getValue();
break;
//それ以外のパラメーターは'error'と返す
default:
rowData.value = 'error';
break;
}
}
result = JSON.stringify(rowData, undefined, 2);
//ログ出力と値の出力
Logger.log(result);
return ContentService.createTextOutput(result).setMimeType(ContentService.MimeType.JSON);
}
MESHカスタムタグ側の実装
左エリアにドロップした音声認識のタグをタップして、認識した言葉を下記のように変更します。
Raspberry Piで音声認識を起動させ、マイクに話しかけると下記のように動作します。
下記では「おはようございます」(一致)、「りんごジュースが飲みたい」(部分一致)、「明日の天気は」(部分一致)でそれぞれ対応した出力コネクタから次のタグへ信号が送られています。
準備
①MESH SDKログインページからID等を入力してログインして下さい。
②「Create New Tag」から新しいカスタムタグを作ってください。
③「import」を開き、下記のjsonデータを入力して「Load JSON」を押して読み込んでください。
④必要箇所を修正して、Saveしてください。
⑤タブレットorスマホ上でMESHアプリを開き、MESH SDKアカウントを紐づけて、右のカスタムの文字の下にある『追加』から作成したカスタムタグを追加してください。
⑥音声認識タグが追加されたら、タグを左の場所に移動させて言葉①②③を認識したい言葉に変更して、それぞれのタグをアクションと繋げてください。
MESHカスタグのプログラム
Initialize
secondsは定期的にタグを実行する間隔(秒単位)なので、必要があれば修正してください。
return {
runtimeValues : {
outputIndex : [0],
pre_time : [0]
},
resultType : "pause",
taskConfig : {
mode : "interval",
seconds : 1
}
}
Receive
特に書くことはありません。
Execute
apiURL のYOUR_WEB_APP_URL部分を、WEB API側のプログラムをウェブアプリケーションとして導入した時に作成されるURLに変更してください。
var apiURL = "YOUR_WEB_APP_URL?param=latest";
var THR_CONF_VAL = 0.8; //音声認識された文字の信頼度の閾値
var THR_SIMI_VAL = 0.3; //文字列の揺れの閾値(0.0~1.0で0に近いほど一致度が高い)
ajax ({
url : apiURL,
type : "GET",
timeout : 5000,
})
.then(
//成功時
function(data){
var json_data = JSON.stringify(data);
var json_text = JSON.parse(json_data);
var state = json_text["state"];
var text = json_text["text"];
var conf_val = parseFloat(json_text["confidence"]);
var time = json_text["time"];
log(time);
log(runtimeValues.pre_time);
if( time === runtimeValues.pre_time){
log("前と同じ時間");
}
else if(state == "complete"){
//部分一致を行う場合
if(-1 < text.indexOf(properties.word_1)){
success(0);
}
else if(-1 < text.indexOf(properties.word_2)){
success(1);
}
else if(-1 < text.indexOf(properties.word_3)){
success(2);
}
//テキストを認識した場合(ある程度文字列の揺れを許容する)
else if(conf_val > THR_CONF_VAL){
var dis_1 = levenshteinDistance(text, properties.word_1);
var dis_2 = levenshteinDistance(text, properties.word_2);
var dis_3 = levenshteinDistance(text, properties.word_3);
if( dis_1 < THR_SIMI_VAL){
success(0);
}
else if( dis_2 < THR_SIMI_VAL){
success(1);
}
else if( dis_3 < THR_SIMI_VAL){
success(2);
}
}
}
runtimeValues.pre_time = time;
},
//失敗時
function(){
log('Network error');
}
);
//レーベンシュタイン距離(文字の類似度計測)
//参考URL htp://www.mwsoft.jp/programming/munou/javascript_levenshtein.html
function levenshteinDistance( str1, str2 ) {
var x = str1.length;
var y = str2.length;
var d = [];
for( var i = 0; i <= x; i++ ) {
d[i] = [];
d[i][0] = i;
}
for( var i = 0; i <= y; i++ ) {
d[0][i] = i;
}
var cost = 0;
for( var i = 1; i <= x; i++ ) {
for( var j = 1; j <= y; j++ ) {
cost = str1[i - 1] == str2[j - 1] ? 0 : 1;
d[i][j] = Math.min( d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost );
}
}
//扱いやすいように値を加工(0.0~1.0の範囲で類似度が高いほど数字が0.0に近い)
if(x > y){
cost = d[x][y] / x;
}
else{
cost = d[x][y] / y;
}
return cost;
}
function success(_outputIndex){
runtimeValues.outputIndex = _outputIndex;
callbackSuccess( {
resultType : "continue",
runtimeValues : runtimeValues
} );
}
return {
resultType : "pause"
};
Result
return {
indexes : [ runtimeValues.outputIndex ],
resultType : "continue"
};
importファイル(JOSNデータ)
下記は、準備の③『「import」を開き、下記のjsonデータを入力して「Load JSON」を押して読み込んでください。』に必要なjsonデータです。
{"formatVersion":"1.0","tagData":{"name":"音声認識","icon":"data:image/png;base64,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","description":"認識させたい言葉を入力すると音声認識させることができます。","functions":[{"id":"function_0","name":"入力した言葉を認識する","connector":{"inputs":[],"outputs":[{"label":"言葉①を認識"},{"label":"言葉②を認識"},{"label":"言葉③を認識"}]},"properties":[{"name":"言葉①","referenceName":"word_1","type":"string","defaultValue":"入力してください"},{"name":"言葉②","referenceName":"word_2","type":"string","defaultValue":"入力してください"},{"name":"言葉③","referenceName":"word_3","type":"string","defaultValue":"入力してください"}],"extension":{"initialize":"return {\n\truntimeValues : {\n\t\toutputIndex : [0],\n\t\tpre_time : [0]\n\t},\n resultType : \"pause\",\n taskConfig : {\n mode : \"interval\",\n seconds : 1\n }\n}","receive":"","execute":"var apiURL = \"YOUR_WEB_APP_URL?param=latest\";\n\nvar THR_CONF_VAL = 0.8;\nvar THR_SIMI_VAL = 0.3;\n\najax ({\n url : apiURL,\n type : \"GET\",\n timeout : 5000,\n\t})\n\t.then(\n\t//成功時\n\tfunction(data){\n\n\t\tvar json_data = JSON.stringify(data);\n\t\tvar json_text = JSON.parse(json_data);\n\n\t\tvar state = json_text[\"state\"];\n\t\tvar text = json_text[\"text\"];\n\t\tvar conf_val = parseFloat(json_text[\"confidence\"]);\n\t\tvar time = json_text[\"time\"];\n\t\t\n\t\tlog(time);\n\t\tlog(runtimeValues.pre_time);\n\n\t\tif( time === runtimeValues.pre_time){\n\t\t\tlog(\"前と同じ時間\");\n\t\t}\n\t\telse if(state == \"complete\"){\n\t\t\t//部分一致を行う場合\n\t\t\tif(-1 < text.indexOf(properties.word_1)){\n\t\t\t\tsuccess(0);\n\t\t\t}\n\t\t\telse if(-1 < text.indexOf(properties.word_2)){\n\t\t\t\tsuccess(1);\n\t\t\t}\n\t\t\telse if(-1 < text.indexOf(properties.word_3)){\n\t\t\t\tsuccess(2);\n\t\t\t}\n\t\t\t//テキストを認識した場合(ある程度文字列の揺れを許容する)\n\t\t\telse if(conf_val > THR_CONF_VAL){\n\t\t\t\tvar dis_1 = levenshteinDistance(text, properties.word_1);\n\t\t\t\tvar dis_2 = levenshteinDistance(text, properties.word_2);\n\t\t\t\tvar dis_3 = levenshteinDistance(text, properties.word_3);\n\t\t\t\t\n\t\t\t\tif( dis_1 < THR_SIMI_VAL){\n\t\t\t\t\tlog(\"認識した言葉は、\" + properties.word_1);\n\t\t\t\t\tsuccess(0);\n\t\t\t\t}\n\t\t\t\telse if( dis_2 < THR_SIMI_VAL){\n\t\t\t\t\tlog(\"認識した言葉は、\" + properties.word_2);\n\t\t\t\t\tsuccess(1);\n\t\t\t\t}\n\t\t\t\telse if( dis_3 < THR_SIMI_VAL){\n\t\t\t\t\tlog(\"認識した言葉は、\" + properties.word_3);\n\t\t\t\t\tsuccess(2);\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t\t\n\t\truntimeValues.pre_time = time;\n\n\t\t/*\n\t\tcallbackSuccess( {\n\t\t\tresultType : \"pause\",\n\t\t\truntimeValues : 0\n\t\t} );\n\t\t*/\n\t},\n\t//失敗時\n\tfunction(){\n\t\tlog('Network error');\n\t}\n);\t\n\n//レーベンシュタイン距離(文字の類似度計測)\n//http://www.mwsoft.jp/programming/munou/javascript_levenshtein.html\nfunction levenshteinDistance( str1, str2 ) { \n var x = str1.length; \n var y = str2.length; \n\n var d = []; \n for( var i = 0; i <= x; i++ ) { \n d[i] = []; \n d[i][0] = i; \n } \n for( var i = 0; i <= y; i++ ) { \n d[0][i] = i; \n } \n\n var cost = 0; \n for( var i = 1; i <= x; i++ ) { \n for( var j = 1; j <= y; j++ ) { \n cost = str1[i - 1] == str2[j - 1] ? 0 : 1; \n d[i][j] = Math.min( d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost ); \n }\n }\n\t\n\t//使いやすいように値を加工(0.0~1.0の範囲で類似度が高いほど数字が0.0に近い)\n\tif(x > y){\n\t\tcost = d[x][y] / x;\n\t}\n\telse{\n\t\tcost = d[x][y] / y;\n\t}\n return cost;\n}\n\n\nfunction success(_outputIndex){\n\truntimeValues.outputIndex = _outputIndex;\n\tcallbackSuccess( {\n\t\tresultType : \"continue\",\n\t\truntimeValues : runtimeValues\n\t} );\n}\n\nreturn {\n resultType : \"pause\"\n};","result":"return {\n\tindexes : [ runtimeValues.outputIndex ],\n\tresultType : \"continue\"\n};"}}]}}
使い方
①MESHアプリを起動し、音声認識タグを左の領域にドロップして、認識したい言葉を変更してタグを繋げておきます。
②ラズパイを起動し、chromiumブラウザでWeb Speech API側のURLを開きます。
③ラズパイに話しかけるとそれに応じてカスタムタグが動作します。
感想
目的としていたものが実現できたの良かった(安心した)。この環境を構築するのは少し手間ですが、構築すれば手軽に音声認識を用いたプロトタイピングをテストできるので便利でした。『とりあえず動作させるぞ...!』と作っていたので、後でちゃんとリファクタリングをしたいと思います。
ちなみに、Raspberry Pi側で実装(Web Speech API)しているものは、別にRaspberry Piである必要はなく、Web Speech APIに対応しているブラウザであれば良いです。Raspberry Piにしたのは、Web Speech APIを用いれば音声認識がRaspberry Piでも簡単にできるのと、別機能をRaspberry Piで実装する予定だったので。