めあとるーむ記録帳

なんか書く

System.Speechライブラリを使ってみる

github.com

ソースはここに

maretol.hatenablog.jp

前にこの記事読んでおいたほうがいいかも


結構時間がかかったのはMVVMとかデータバインディングとかの勉強も合わせるかー、と言ってなんか次々試行錯誤したため

おかげでデータバインディングの考え方とかはだいぶわかった気がする。気がする

ひとまず形にはなった

f:id:maretol:20180126164705p:plain

コマンドのところに反応したい単語を書き込んでフォーカスを外す(なんか外クリックしてフォーカス外すようにしたかったけどよくわかんなかった。現状下のテキストボックスにフォーカス合わせるしかなさそう)と開始できるようになるので開始する

認識結果や起動・終了時のメッセージは下のテキストボックスに出てくる。認識時は認識系(RecognizedとHypothesized)と一致率の数値が表示される

ソースコード自体のやってることはぶっちゃけサンプルとほとんど変わらない

もっとしっかりとやりたいならちゃんとDisposeとか呼ぶ環境を作るべきかなというぐらい

いずれにせよやっていることは前回(最初の記事のリンク)に書いたとおりで、エンジンの環境を整えて各イベントを登録し文法データを読み込んで起動、終了というだけ

イベントや文法データの取り込み方、結果の取扱などをいじってあげればテストぐらいはできるかも

前回の記事にも書いたとおり本来は音声コマンドとかで使うものだと思うので自由な認識を期待するとあまりよろしくないかもしれない


解説記事を書こうと思ってたんだけど解説することが特にないことに気づくなど

なんかわかんないこと聞いてもらえればなんか答えるかもしれない

C#(.NET)のSystem.Speech.Recognitionライブラリを使う

1ヶ月ぐらいで辞めた仕事(の出向先)で音声認識をゴニョゴニョしてたけどそのときにSystem.Speech.Recognitionライブラリを使った

このライブラリ何がアレって基本的な使い方の記事も少なく細かいところまでアレコレしようとするとだいたい公式ドキュメント(の英語版か機械翻訳版)しかないという状態だった

なんでまあ覚えてる範囲と出来た範囲でメモを残しておく

いやほんと公式ドキュメントも情報量十分かって言うとどうだろうってなっちゃうやつだったからな……イングリッシュ読めるようになりたい

具体的なコードとかはGithubに上げる。完成したら解説記事を書く(現在未完)

github.com


公式

System.Speech.Recognition 名前空間

基本はこの名前空間のクラスを使う。以下主に使うクラスなどなど

  • System.Speech.SpeechRecognitionEngine : 音声認識のエンジン部分。ここに音声認識時のイベントとか設定とかある。
  • System.Speech.SpeechRecognizer : 上とほぼ同じだがこっちはWindowsDesktop向け。上はプロセス上で動くやつ(違いはよくわからない。さっき見てて気づいた)
  • System.Speech.Grammar : 認識時の主軸となる文法データを取り扱う。同名前空間にGrammarBuilderとかもある
  • System.Speech.DictationGrammar : ディクテーション(発する言葉全てを文字起こしする)の場合に使う文法データ
  • System.Speech.Choices : 文法データのもとになる登録語をまとめるクラス
  • 各種 EventArgs : 認識したときのイベントで渡されるやつ

だいたいこのあたり。文法データを外部データにしたい場合は System.Speech.Recognition.SrgsGrammar 名前空間の SrgsGrammarCompile とかでバイナリデータを出してあげたり、Srgs文法ファイルを読ませたりできる

文法ファイルとはなんぞや、ってなる(なった)ので先に説明しておくと、このライブラリは基本的に音声コマンドを実行するためのライブラリなので、そのコマンドをまとめたのが文法というものである

詳細は後述するが、この文法に則って認識するため、自由な認識をさせることはできない。もっとも DictationGrammar であれば認識できる(が、こちらだと単語の登録はできない)


RecognitioEngine / Recognizer が音声認識機能を提供してくれるクラスになる

違いはドキュメントを見た感じだとRecognitionEngineのほうが高機能。言語・地域を設定したり、AudioStreamで音データを受け取ったり、読み込み途中で文法データを再読込させたりもできる

ただ認識自体は同じようで、イベントとか見た感じその辺に違いがあるようには見えなかった(認識以外だと環境に関係するイベントの違いがあるっぽい?)

ここではRecognitionEngineをベースに解説する。具体的なコードはそのうちGithubに上げるか公式ドキュメントで何となく理解できると思う

SpeechRecognitionEngine クラス (System.Speech.Recognition)

まずインスタンスを作ったら

  1. 文法ファイルを読み込む (LoadGrammar メソッドなど)
  2. Eventの受け取りを追加 (SpeechRecognized イベントなど)
  3. 音声のソースを設定 (SetInputToDefaultAudio メソッドなど)
  4. 音声認識の開始開始する (Recognize メソッドなど)

の順で動かす。厳密に言うと順番は関係ないかもしれないが、公式ドキュメントはどのクラス・メソッドでもだいたいこの順にやっているのでこれに従ったほうがいいと思う


文法ファイルと文法ファイルの読み込みについて

特定のコマンドや単語に反応させる、という機能を作るときにその指定をする機能

具体的にコマンドの例を挙げると「リビングの電気をつけて」とか「キッチンの電気を消して」とか

これらを登録することで認識したときに「リビングの電気をつけて」という認識をしたよー、とイベントが返してくれるわけである(ただしディクテーションは除く)

そして当然だが、これらのコマンドを全部登録するのは骨が折れる。「○○(場所)の電気を××(つけて/消して)」と全パターン登録するのは(不可能じゃないけど)面倒だよね、ってわけでちゃんとどうにかする方法がある

1つはSRGSドキュメントを使って記述し、それを読み込ませる

Speech Recognition Grammar Specification Version 1.0

音声コマンドにおけるXML記述の規格らしく、すでに他環境用に書いてる人とかはこれをそのまま XMLReader で読んで System.Speech.Recognition.SrgsGrammar クラスに渡してそのまま Grammar クラスに渡せばもう辞書登録は完了する

もう1つがGrammarBuilderクラスを用いてその場で組み立てる。

どちらの方法でもワイルドカード、単語リストなどを組み合わせることでコマンドのパターンを網羅する仕組みになっている

以下GrammarBuilderでもっと具体的に解説する。

まず単語のリストはChoicesクラスで用意する。例えばさっきの電気のオンオフであれば

Choices choices1 = new Choices(new string[]{"リビング","キッチン","玄関"});
Choices choices2 = new Choices(new string[]{"つけて","消して"});

と登録し、GrammarBuilder で

GrammarBuilder gb = new GrammarBuilder();
gb.Append(choices1);
gb.Append("の電気を");
gb.Append(choices2);

とすると、{choices1}の電気を{choices2} というパターンに対応できる

当然文法は順序によって内容が異なるのでそこは注意が必要。Appendメソッドで後ろに追加していくのがよく使うパターンだと思う

また、同時にワイルドカードを差し込んだり( AppendWildcard )、一部ディクテーション ( AppendDictation )を挟んだりもできる

この2つはどう違うかというとワイルドカードは内容を認識しないけどディクテーションは認識しようとすることだと思う。試したらまた書くね(少なくともワイルドカードは ... となるだけで内容までは出ない)

GrammarBuilderで文法ファイルを形成したらあとはGrammarクラスに渡してあげて、GrammarクラスをLoadGrammarメソッドで読み込んであげるといい

ちなみに非同期でやってくれるというLoadGrammarAsyncメソッドもあるが、Async/Awaitは使えないので自分で別スレッド作ってあげるかTask.Factory.StartNewあたりで渡してあげないといけないっぽい。終わったらLoadGrammarComplitedイベントが発火する


音声認識は認識した段階で各種イベントとして発火するようになっている

反応するイベントは以下の通り

  • SpeechRecognized
  • SpeechHypothesized
  • RecognizeComplited
  • SpeechDetected
  • SpeechRecognitionRejected

これがまた結構めんどくさく、

イベント 内容
SpeechRecognized Grammarに登録した内容を認識した。一致率は高い
SpeechHypothesized Grammarに登録した内容を認識した。一致率は中程度
RecognizeComplited 音声認識終了時にGrammarに登録した内容を認識した
SpeechDetected 何かを認識した(何を認識したかはわからない)
SpeechRecognitionRejected 認識したものがGrammarに登録したものに一致しなかった

Rejectedの一致しなかった、というのがよくわからないと思うが、ドキュメントを見ても実際に動かしてみても本当によくわからなかった。謎のイベントだった

また、SpeechDetectedもよくわからない(反応するだけで内容は不明、EventArgsも特別それっぽい情報がない)ものだった

とりあえず、認識だけで言うとSpeechRecognizedで十分である。SpeechHypothesizedはあまりにも大雑把で認識しすぎる感じである

認識次第終了、というかたちを取るのであればRecognizeComplitedを使うほうがよさそうである

認識結果は各イベントから渡されるEventArgsの中に入っている(Detected除く)。これを取ってくれば認識結果を得られるというわけになる


精度自体はあんまり高くない(コマンドの種類によっては許容内。ディクテーションは正直イマイチ)。音声認識でメモを取るとかなら Google の Cloud Speech API とか Microsoft の Bing Speech API とか使ったほうがいい

今回は音声認識を起動したら人同士の会話を聞き続けてその中でキーワードが出てきたらそれを列挙するというものだったので、最大1分(WebSocketなら10分?APIによる)しか繋げないAPIを使うことは無理だった

ひとまずここまで。ソースが上がったらGithubに上げて解説記事のような物を書くかもしれない


以下愚痴に近い私見

前職でも前前職でも「マイクを起動しっぱなしで人同士の会話を常に録音したい」というのが言われたが正直なところそんな機能がちゃんと提供できるのはあと5年ぐらいかかると思う。というか機械に向かって話しかけるのと人同士の会話を解析するっていうのは別のことで前者は割と形になってきてるけど後者はまだまだだと思うし、一緒にするのも止めてもらいたい

だいたい人同士の会話というのは人の想像以上に文章として破綻しているケースが多いので、それなりに時間を要するのではないかというのが私見

あとめっちゃどうでもいいけど文末の句点の有無が統一されてなさすぎて気持ち悪かった(全部取った

iPhone Xを買った

買った。以下感想とかなんとか


買った理由

  • 面白そうじゃん
  • 発展途上デバイス大好き
  • なんか面白そう

めっちゃ雑に言うとこんな感じ

で、この際だからApple Payとかそのあたりのサービスもがっつり使ってみて、というか端末を2台持ちからiPhone Xの1台運用に変更するかとも思った。

メイン端末としてiPhoneを使ってみた感想はこんな感じ

  • 思ったよりアプリ少ない
  • いろんなところのレスポンスは悪くない
  • でもバグが多いっぽい。早速2回ぐらいバグった
  • 片手操作モードないのつらい
  • 縦長画面のおかげでバナー広告が結構うざくない
  • マジで思ったよりアプリ少ない

Apple Payとかはまだ使ってないのでやんわりと

本当に感想としては思ったよりアプリ少ないの一言に片付く感じ。個人開発のハードル高いのかも。ちょうどいいTwitterクライアントアプリがないなんてiOSで思うとは思わなかった

公式アプリとかの供給は多いのでそういう面は困らない。あとゲーム。Unity製のアプリの動きがとてもいい。

とりあえずTwitterクライアントアプリがないので作る方向で考えつつある。作るとしたらXamarin for iOSで開発することになるかなー、とかビルドにMacいるよなーとか思ってる

MySQLの全部検索の設定で注意すべきところ

MySQLには標準で全文検索機能がある。分かち書きが必要とは言えワイルドカードで検索するよりよっぽどましなので使った方がいいケースも多い。

ただしこの全文検索機能は「検索文字の文字数が4文字以下はヒットしない」という条件がある。まあ高速化のためにはある程度考えるべきなのだろうが、英語ならともかく日本語では4文字以下の単語の比率が結構高いと思うし英語でもcatとかdogとか簡単な単語が引っかからないため、そのあたりは変更しなければならない。

で、ft_min_word_lenの値を1にして再起動、インデックスを張りなおしていざ検索したところ、なぜかヒットしない。

うんうん悩んで設定を見ていくと似たような設定にinnodb_ft_min_token_sizeというのもあった。これはエンジンでInnoDBをつかう場合の検索条件らしく、外部キーの設定などでInnoDBを使っていたのでこれかと思いこちらの設定値を0にしたところうまくいった。

多くのfulltext関係の資料ではft_min_word_lenの変更で設定が変わると書いていたので勘違いした。ただ、もしかするとこちらの数値も変更する必要があるのかもしれない。とはいえいちいち変更して試すのがめんどいので申し訳ないが誰かやってまとめておいてほしい

GolangのAppEngineをデプロイしようとしたら失敗した。どうやらファイル名に日本語は使わない方がいいらしい

golang で AppEngine 開発してさあデプロイするぞ!とやったところエラーが出た。

PS > gcloud app deploy
Services to deploy:

(プロジェクト情報略)

Do you want to continue (Y/n)?  Y

Beginning deployment of service [news]...
Some files were skipped. Pass `--verbosity=info` to see which ones.
You may also view the gcloud log file, found at
(出力されるログファイルの場所情報略)
#============================================================#
#= Uploading 0 files to Google Cloud Storage                =#
#============================================================#
File upload done.
ERROR: gcloud crashed (UnicodeDecodeError): 'utf8' codec can't decode byte 0x82 in position 4: invalid start byte

If you would like to report this issue, please run the following command:
  gcloud feedback

To check gcloud for common problems, please run the following command:
  gcloud info --run-diagnostics

今までできてて突然できなくなったので変更した場所が悪いのかな?と思うもエラー内容(UnicodeDecodeError)はPythonのエラーらしく、どうやらgcloudコマンド関係がクラッシュした模様(gcloudコマンドはPythonで実装されてる)

とはいえアップデートなどもなかったはずなのでやはりこちらが何か変更したのが悪いはずと思いいろいろと試したところ、どうやらプロジェクトファイル内のドキュメント関係を入れたディレクトリに日本語名のファイルがあることに気づく。

以前のデプロイでは存在していなかったファイルの名前を日本語から適当な英語のファイル名にしたところ無事デプロイ成功

どうやら関係ないファイルでも日本語名のファイルがあると問題が起こるらしい。確かに文字コード関係のエラーだもんね。

なお環境がWindowsなので要はこれShift-JISで名前が入ってるらしくその問題もあるのかもしれない。今後気を付けよう

CircleCIでMavenを使ってGoogle AppEngineをデプロイするのを自動化したい

追記(2017-09-08)

解決してないのでググってたどり着いた人ごめん


タイトルの通り。

GAEのデプロイは2通りあって、appcfgを使うかMavenを使うか

Deploying a Java App  |  App Engine standard environment for Java  |  Google Cloud Platform

おススメ(recommended)なのはMavenらしいので、それを使っていたのだが、せっかくだし自動化したいなーと思ってCircle CIでデプロイコマンドに mvn appengine:update をそのまま入れたところ、まあ当然なのだが認証が通らず止まってしまった。デフォルトではOAuthで認証してキーを取ってきてそれを入れると通るのだが、当然入力する場所はないのでどうしようもない。


というところで止まっている。お盆が明けたら再開予定……

可能ならサービスアカウントを登録させるか引数で渡すべきなのだと思う……

ちなみにほかのデプロイ方法ではどうやらgcloudコマンド用の設定がCircle CIに用意されているらしい。


追記(2017-09-08)

ようやく追記(タイトルもちょっと修正)

とりあえず結論は出来なかった。できなかったというよりわからなかったが近い。

サービスアカウントの情報を渡して認証させてもどうやら maven appengine コマンドが読んでくれない。

認証関係のオプションを探してみたがそれらしいものはなかった。一応auth関係のオプションも探したがそれらしいものも見つけられなかった。

pythonコマンドを用いたデプロイやらコンテナベースのサービス?なら手段もそれなりにあるっぽいが、残念ながらわからないというのが実情。むなしい

値を返してから代入する

ちょっと前にTwitterで人が死ぬバグといって流れてきたツイートがそれなりに話題になっていたと思う。

これね。出典元は 非推奨だった bool 型に対するインクリメント演算子を削除 - cpprefjp C++日本語リファレンス

これは古いCのコードをもとにしていて、かつ当時はbool型もなかったのでこうなっていたようだが、配列の要素を並べて表示し、それぞれの要素間に,を表示するといったパターンではおそらく昔はよくあったコードだと思う。


この「配列の要素を並べて、各要素間にはコンマをうって表示する」というケースに類似した処理はよくあるものだと思う。

地味に面倒なことに、この場合「最初は飛ばすがそのあとからは毎回つける」というパターンで記述することが多い。

で、その場合(下のコードはJava

boolean first = true;
for(Object obj : objList){
    if(!first){
        System.out.print(",");
    }else{
        first = false;
    }
    System.out.print(obj.toString());
}

みたいなコードが必要になったりする。

はっきり言って面倒である。

ちなみにC++では上のリンクであるように昔はインクリメントがあった。bool型(boolean型)のインクリメントはJavaにはないし、C++も今はない。

後述するがインクリメントははっきり言ってわかりにくいものなので最近だとSwiftでは方にかかわらずインクリメント自体がなくなった。

ではこのインクリメントなしで書くにはどうするか、というと、まあ上のように一つフラグを用意してあげてとなるのだが、C++では割と便利な関数ができていた。

それがstd::exchangeというので、std::exchange - cppreference.comにある。

これは「値を返してそのあと代入する」というものである。

ただこれはJavaにはない。Javaにそんな便利なものがあると思ったら大間違いだ。欲しければ作れ。という感じでこんな感じになるだろうか

public class Exchanger<T>{
    T value;
    public Exchanger(T value){
        this.value = value;
    }

    public T get(T newValue){
        T oldValue = value;
        value = newValue;
        return oldValue;
    }
}

使うときはこう

Exchanger<Boolean> first = new Exchanger<Boolean>(true);
for(Object obj : objList){
    if(!first.get(false)){
          System.out.print(",");
    }
    System.out.print(obj.toString());
}

ちょっとはキレイになった。

問題点はJavaなんでプリミティブ型が使えないこととか、Integer型のときに +1 で回せないという点あたりか。後者の問題はそのままの値を返すget()メソッドで値を取って+1したものをnewValueとしてget(T newValue)に渡す感じにするだろうか。ジェネリクスを使う以上あまり入り込んだ処理は行いたくない。

いずれにせよJavaだと面倒。ほかの言語であればだいぶ楽(C#とかならLINQ使ったりできるかもしれない)なので調べる価値はあるはず。


ところでインクリメント(と、デクリメント)について。

いわゆる++だが、先述の通りはっきり言ってややこしい。i++++iの違いなど意識していては可読性が下がるしバグの温床になりかねない。

SwiftではSwift2からインクリメントとデクリメントがなくなったし、正直forループの処理以外で使うこともほとんどないだろう。可能な限り使わないに越したことはない。

C++の場合演算子オーバーロードでは前置か後置かの判定で描き方も面倒になっている。ついでに言うと += などの書き方も正直あまりいい物ではないと思う。

省略することが良いとは限らないし、現在なら基本はすべて記述すべきだと思う。


追記(2017年11月24日)

文字列を合体させて要素の間にコンマを入れたいケースとかは String.join() とかつかうといいよ(なおJavaは8以降にしかないらしい)

こういう多数の人間が必要になりそうな機能はだいたい供給されている(たまにされてない)のでAPIをどんどん調べていこうと思った