コマンドラインからの音声入力






##はじめに

これは何をしているのかというと、音声によるAPIとのやり取りを通知機能を使って表示しているという感じのことをしています。


これを実現する要素は、まず、コマンドラインから使える音声認識システムである[Julius](http://julius.sourceforge.jp/juliusbook/ja/desc_install.html)を使います。

そして、そこで出力された結果を[雑談対話API](https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_docs_id=3)に渡して、結果を[Growl](http://growl.info/downloads)のような通知ツールで表示し、会話を実現しています。

ただし、見てもらえば分かるように、APIとの会話も音声コマンドを設定して実行しています。具体的には、パソコンに向かって`起動`としゃべると、それが会話を開始する合図になります。反対に、`おやすみ`としゃべると、それが会話を終了する合図になります。


ちなみに、ここでいう合図というのは、音声入力をAPIに渡す処理を停止したり、開始したりすることを意味します。

画像では、終了していないようにも見えますが、終了の音声も送ってしまうため、これについては、返信が来ます。しかし、バックグラウンド処理は明らかに停止しているため、これ以降、会話は行いません。

##何が嬉しいのか

一つに、コミュニケーションというものは、音声による会話形式が、文字入力に比較して、楽である事が挙げられます。上の画像を見てもらえればわかると思いますが、文字入力よりも、楽に開始し、楽に終了できます。

また、やり取りを分かりやすく通知で表示しているわけですが、実際は、音声に変換することもできます。例えば、Macには、`say`コマンドがあります。したがって、視覚を邪魔されたくなければ、全てを音声に変換して実行するというやり方もできます。

したがって、今回やるようなことを実行できるようにしておくと、色々と応用ができて、便利なことがあるかもしれません。

##音声認識システムを使いこなす上で重要なこと

ただし、便利になる可能性があると言っても、はじめから便利に使用できるわけではありません。

私の場合も、実は、会話を始める際の音声コマンドの認識が難しく、`Julius`の認識精度が低すぎるが故の多少の苦労を感じました。

具体的には、`起動`と発音しても、`記号`とか`気道`、その他全く関連性もないような単語が出力されるがために、なかなか使えない状態でした。

しかし、これに関しては、ある調整を行うと、うまくいきました。その一つに、`単語を減らす、改ざんする`ということです。

例えば、起動コマンドにおいて使用する単語には、`広雄+名詞	@-3[起動]	h i r o o`などの表示を付け加えたり、場合によっては、`記号`を`起動`に改ざんするやり方をしました。

ちなみに、ここで言う`@-3`は優先順位です。単語ファイルをみればどのように指定するのかは分かるでしょう。

したがって、よく間違えて出力される単語においては、このように個別の対応が必要になると思います。

更に、起動コマンドと会話コマンドで指定する単語ファイルを別々に指定するということをやると、使いやすくなるのではないかと考えて、実行しています。

私の場合、例えば、起動コマンドと会話コマンドは以下の様な分け方をしています。

```main.conf
-v model/lang_m/bccwj.60k.htkdic
```

```sub.conf
-v model/lang_m/sub.60k.htkdic
```

更に、このような指定の場合、実行コマンドにも違いが出てきます。

```
$ julius -C main.conf

$ julius -C sub.conf
```


##簡単な説明

###Juliusのインストール


では、簡単に使えるようにするまで、具体的には、APIとの会話ができる手順を簡単に説明したいと思います。

まず、必要っぽそうなものをインストールします。

```
$ sudo pacman -S portaudio
```


次に、Juliusをインストールするわけですが、これは、以前作った[airjulius.vim](https://github.com/syui/airjulius.vim)というVimプラグインに処理を書いておきましたので、それを使うと、面倒な作業が簡単に終わります。

具体的には、ダウンロードからテストまでを自動実行しています。

```vim:
NeoBundle 'Shougo/VimShell'
NeoBundle 'syui/airjulius.vim'
```

`:AirJuliusVimShell`

さて、これがちゃんと実行でき、発音した単語がVim上に表示されていれば、問題なく使えるということです。

ちなみに、私の場合は仮想環境下にあるArchで実行した所、何故か`そのディレクトリは存在しません`などというわけのわからないことになりましたので、以下の手順で独自に持ってきた`julius`ファイルをコピー上書きすることで使えるようになりました。

```bash:
$ sudo pacman -S julius

$ cp /var/cache/pacman/pkg/julius-4.3.1-1-x86_64.pkg.tar.xz ~/Downloads

$ cd ~/Downloads

$ aunpack julius-4.3.1-1-x86_64.pkg.tar.xz

$ sudo cp julius-4.3.1-1-x86_64.pkg.tar/usr/bin/julius ~/.vim/bundle/airjulius.vim/tool/bin
```


###Docomo APIを使う

会話型のAPIは、以下のようにして使えます。ただし、APIキーが必要なので、[登録](https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_docs_id=3)して発行されるそれを`xxxxx`の部分に使いましょう。

```bash:
$ curl -s -H "Content-type: application/json" -X POST -d "{ \"utt\": \"こんにちは\", \"context\": \"548968da92614\", \"nickname\": \"光\", \"nickname_y\": \"ヒカリ\", \"sex\": \"女\", \"bloodtype\": \"B\", \"birthdateY\": \"1997\", \"birthdateM\": \"5\", \"birthdateD\": \"30\", \"age\": \"16\", \"constellations\": \"双子座\", \"place\": \"東京\", \"mode\": \"dialog\" }" https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY=xxxxx
```

これが実行でき、返事がjson形式で返されます。これは、`jq`というツールを使って加工できますので、インストールしましょう。あと、通知ツールもない場合は、インストールしておきます。

```bash:
$ sudo yaourt -S jq

$ sudo pacman -S notify-send
```

これで準備が整いました。あとは、以下のコマンドで再確認です。

```bash:
# Linuxの場合
$ curl -s -H "Content-type: application/json" -X POST -d "{ \"utt\": \"こんにちは\", \"context\": \"548968da92614\", \"nickname\": \"光\", \"nickname_y\": \"ヒカリ\", \"sex\": \"女\", \"bloodtype\": \"B\", \"birthdateY\": \"1997\", \"birthdateM\": \"5\", \"birthdateD\": \"30\", \"age\": \"16\", \"constellations\": \"双子座\", \"place\": \"東京\", \"mode\": \"dialog\" }" https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY=xxxxx | jq . -M -r | xargs -I 1 notify-send -i ~/.vim/bundle/airjulius.vim/icon/syui.png 1

# Macの場合
$ curl -s -H "Content-type: application/json" -X POST -d "{ \"utt\": \"こんにちは\", \"context\": \"548968da92614\", \"nickname\": \"光\", \"nickname_y\": \"ヒカリ\", \"sex\": \"女\", \"bloodtype\": \"B\", \"birthdateY\": \"1997\", \"birthdateM\": \"5\", \"birthdateD\": \"30\", \"age\": \"16\", \"constellations\": \"双子座\", \"place\": \"東京\", \"mode\": \"dialog\" }" https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY=xxxxx | jq . -M -r | xargs -J 1 growlnotify -i ~/.vim/bundle/airjulius.vim/icon/syui.png -m 1
```

###Julius Plugin

ここで、[Julius Plugin](http://julius.sourceforge.jp/juliusbook/ja/desc_plugin.html)を使って、音声認識を会話APIに渡すということをやってみたいと思います。

具体的には、以下のようなコードを書いて、それをコンパイルし、コマンド実行の際に指定します。

コードは、例えば、以下のようになります。APIキー`xxxxx`が必要です。

####Linux

```c:~/.vim/bundle/airjulius.vim/t/test.c.linux
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>

int
get_plugin_info(int opcode, char *buf, int buflen)
{
  switch(opcode) {
  case 0:
    strncpy(buf, "simple output plugin", buflen);
    break;
  }
  return 0;
}
void
result_best_str(char *result_str, char *stm)
{
  char use[1024];
  char cmd[1024];
  char key[1024] = "xxxxx";
  if (result_str == NULL) {
    printf("[failed]\n");
  } else {
    printf("%s\n", result_str);

    sprintf(use, "notify-send -i ~/.vim/bundle/airjulius.vim/icon/syui.png '%s'", result_str);
    system(use);
    sprintf(cmd, "curl -s -H \"Content-type: application/json\" -X POST -d \"{ \\\"utt\\\": \\\"%s\\\", \\\"context\\\": \\\"548968da92614\\\", \\\"nickname\\\": \\\"唯\\\", \\\"nickname_y\\\": \\\"平沢唯\\\", \\\"sex\\\": \\\"女\\\", \\\"bloodtype\\\": \\\"O\\\", \\\"birthdateY\\\": \\\"1997\\\", \\\"birthdateM\\\": \\\"11\\\", \\\"birthdateD\\\": \\\"27\\\", \\\"age\\\": \\\"16\\\", \\\"constellations\\\": \\\"射手座\\\", \\\"place\\\": \\\"東京\\\", \\\"mode\\\": \\\"dialog\\\" }\" https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY=%s | jq .utt -M -r | xargs -I 1 notify-send -i ~/.vim/bundle/airjulius.vim/icon/twitter.png 1", result_str, key);


    system(cmd);

  }
}
```

コンパイル:

> gcc -shared -fPIC -o test.jpi test.c

コマンド:

> cd ~/.vim/bundle/airjulius.vim/t/

> ./run-origin.sh

通知:awesomeを使っている人の設定例

```~/.config/awesome/rc.lua
naughty.config.defaults.timeout          = 5
naughty.config.defaults.position         = "bottom_right"
naughty.config.defaults.margin           = 4
naughty.config.defaults.width            = 300
naughty.config.defaults.height           = nil
naughty.config.defaults.hover_timeout    = nil
```



####Mac

```c:~/.vim/bundle/airjulius.vim/t/test.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>

int
get_plugin_info(int opcode, char *buf, int buflen)
{
  switch(opcode) {
  case 0:
    strncpy(buf, "simple output plugin", buflen);
    break;
  }
  return 0;
}
void
result_best_str(char *result_str, char *stm)
{
  char use[1024];
  char cmd[1024];
  char key[1024] = "xxxxx";
  if (result_str == NULL) {
    printf("[failed]\n");
  } else {
    printf("%s\n", result_str);

    sprintf(use, "growlnotify --image ~/.vim/bundle/airjulius.vim/icon/syui.png -m '%s'", result_str);
    system(use);

    sprintf(cmd, "curl -s -H \"Content-type: application/json\" -X POST -d \"{ \\\"utt\\\": \\\"%s\\\", \\\"context\\\": \\\"548968da92614\\\", \\\"nickname\\\": \\\"唯\\\", \\\"nickname_y\\\": \\\"平沢唯\\\", \\\"sex\\\": \\\"女\\\", \\\"bloodtype\\\": \\\"O\\\", \\\"birthdateY\\\": \\\"1997\\\", \\\"birthdateM\\\": \\\"11\\\", \\\"birthdateD\\\": \\\"27\\\", \\\"age\\\": \\\"16\\\", \\\"constellations\\\": \\\"射手座\\\", \\\"place\\\": \\\"東京\\\", \\\"mode\\\": \\\"dialog\\\" }\" https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY=%s | jq .utt -M -r | xargs -J i growlnotify -m i --image ~/.vim/bundle/airjulius.vim/icon/twitter.png", result_str, key);

    system(cmd);

  }
}
```

コンパイル:

> gcc -shared -o test.jpi test.c


コマンド:

> cd ~/.vim/bundle/airjulius.vim/t/

> ./run-origin.sh

そして、`-plugindir .`というように、コマンド実行時にプラグインを置いたディレクトリを指定して使います。

###バックグラウンドでの実行

これは簡単で、`run-origin.shやrun.sh`などのJulius実行のスクリプトを以下のようにして起動するだけです。

```
$ ./run-origin.sh > /dev/null 2>&1 &
```

若しくは、これらのスクリプトをバックグラウンド用にします。

```
#!/bin/zsh
dir=${0:a:h}

#gcc -shared -o test.jpi test.c

${dir:h}/tool/bin/julius -C ${dir:h}/tool/main.jconf -C ${dir:h}/tool/am-gmm.jconf -nostrip -quiet -1pass -plugindir $dir > /dev/null 2>&1 &
```

私の場合は、コマンド起動用と会話用のスクリプトを分けて作っていますので、コマンド起動用のみは、バックグラウンドで永続的に動かしてあります。これは、特定の発音を拾うと、特定のコマンドを実行するというようなやつで、内容的には、上記で書いたJulius Pluginと変わりません。

これをバックグラウンドで動かしている限り、音声入力で会話を開始したり、終わらせたりすることができるというわけです。

会話を始めるためのJulius Pluginは、以下のような形で作りました。ただし、上記と同じく、`growlnotify`コマンドの部分は、LinuxとMacで異なりますので注意です。また、出力される結果は、デフォルトでは、`hoge_。`というように、`。`の前に半角の空白があるので、注意してください。


####Mac

```background.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>

    int
get_plugin_info(int opcode, char *buf, int buflen)
{
    switch(opcode) {
        case 0:
            strncpy(buf, "simple output plugin", buflen);
            break;
    }
    return 0;
}
    void
result_best_str(char *result_str, char *stm)
{
    char use[1024];
    char cmd[1024];
    char start[] = "起動 。";
    char stop[] = "おやすみ 。";

    if (result_str != NULL) {
        printf("%s", result_str);

            sprintf(use, "growlnotify --image ~/Pictures/icon/syui.png -m '%s'", result_str);
            system(use);

        if (strcmp(result_str, start) == 0) {
            printf("start %s", result_str);

            sprintf(use, "growlnotify --image ~/Pictures/icon/syui.png -m '%s : ユイを起動します' -t 'ユイ・システム'", result_str);
            system(use);

            sprintf(cmd, "~/.vim/bundle/airjulius.vim/t/run-origin.sh");
            system(cmd);
        } else if (strcmp(result_str, stop) == 0) {
            printf("stop %s", result_str);
            sprintf(use, "growlnotify --image ~/Pictures/icon/syui.png -m '%s : ユイを終了します' -t 'ユイ・システム'", result_str);
            system(use);
            sprintf(cmd, "pkill julius");
            system(cmd);
        }
    } else {
        /*
           sprintf(use, "growlnotify --image ~/Pictures/icon/syui.png -m '%s' -t 'コマンドではありません'", result_str);
           system(use);
           */
        printf("null %s", result_str);
    }
}
```

あとは、コンパイルです。

> gcc -shared -o background.jpi background.c

そして、`-plugindir ~/hoge`というように、コマンド実行時にプラグインを置いたディレクトリを指定して使います。

これが何をするのかというと、音声認識結果である`result_str`を`strcmp`を使って照合して、入力された音声が`起動 。`と同じなら`~/.vim/bundle/airjulius.vim/t/run-origin.sh`という会話を開始するスクリプトを実行するという処理を書いています。これは、先ほど上に書いたプラグインを実行するスクリプトになります。

他に、`おやすみ 。`という音声を読み取ると、スクリプトを終了するための`pkill julius`と通知である`growlnotify`の処理も書いています。

`ps -h | grep julius | grep -v back | cut -d ' ' -f 1 | xargs kill`

###単語ファイル

単語ファイルは、首尾よく行けば、`~/.vim/bundle/airjulius.vim/tool/model/lang_m`内になります。

具体的には、以下のコマンドで個別に設定を分けていき、目的に応じた内容を作り上げていくことで、快適に使えるかどうかが決まってきます。

```bash:
$ cd ~/.vim/bundle/airjulius.vim/tool/model/lang_m

$ cp bccwj.60k.htkdic test.htkdic
```

こういうのは、`-v model/lang_m/test.htkdic`で指定します。ただし、`-C main.conf`として、設定ファイルを使う場合は、そこに書いてあります。

例えば、何らかのコマンドを発動させる単語ファイルは、精度を上げるため、単語を少なくし、場合によっては優先順位を上げ、単語の意味、最終的に出力される内容を改竄する必要がありそうです。

反対に、上にやっているような会話を実現するための単語ファイルは、ある程度会話がスムーズに行きそうに、設定するとよいでしょう。

例えば、`ねえ`という単語を`ねえ、なにか面白いこと言ってよ`に変えたり、`そうかな`を`そうかな、どう思う?`とかすると、簡易な発音で、会話をスムーズに進めることができ...ます...。多分。

##最後に

実は、これ、超短時間で「とりあえず会話できる形にしてみた」というような感じでやりました。なので、それほど簡単にされてるわけでも、便利なわけでもないのですね。

また、Cを初めて書いたこともあって、めちゃくちゃ適当です。本来なら、curl処理、json処理を独自にやったほうが適切だと思われます。更に、OS分岐もしたほうが簡単です。

なので、クライアントという形でインストールしたければ、ある程度手間が掛かりそうですがやってません。

ほぼすべてを使いまわし要素でやってますので、自分なりに良い方に形で実現してってもらえればと思います。

個人的には、バックグラウンド起動用の認識精度(単語ファイルの編集になりそう)を上げて、日常的に使っていけるものかどうなのかを様子見して見る予定です。