2015年8月26日水曜日

電話番号案内システム 脱俗人オートメーション化


◆ 目的
104に電話をかけると電話番号案内サービスが利用できる。
現在は人が対応しているだろうがこの仕組みを完全自動化させてみる。



◆ 実例
本アプリでこのようなことが実現できた。
(例1)
人:Question
「シンガポールに動物園ってあるかな。あったら有名なところ教えて」

自動:Answer
「シンガポール動物園は+65-****-****です」


(例2)
人:Question
「汐留にいるんだけれどさ、タクシー呼んでくんない」

自動:Answer
「○○自動車交通は03-****-****です。
「××タクシーは03-****-****です」



◆ 処理概要
1. 電話での質問の受けつけ
音声:「汐留の近くにある餃子の王将につなげてくれる?」

2. 受け付けた音声をテキストへ変換
文字列:"汐留の近くにある餃子の王将につなげてくれる"

3. 文章になっているテキストを形態要素解析し、一般・固有名詞の抽出
汐留    名詞,固有名詞,人名,姓,*,*,汐留,シオドメ,シオドメ
餃子    名詞,一般,*,*,*,*,餃子,ギョウザ,ギョーザ
王将    名詞,一般,*,*,*,*,王将,オウショウ,オーショー

4. 抽出したキーワードを検索
電話番号 汐留 餃子 王将
電話番号というキーワードを付与すして検索エンジンにかけているだけである。

5. 検索結果をテキスト処理し、電話番号の抽出
03-xxxx-xxxx
実際は餃子の王将は汐留にはなく新橋にあるのだが目的に合致したものが抽出できる。
地図情報の参照の困難さは検索システムに任せきりにする。

6. 返答する応答文を音声合成する
お探しの電話番号は、03-xxxx-xxxx です。



◆ 構築環境、ライブラリ
(電話処理)
電話処理のためのオープンソースのPVXパッケージであるasteriskを使う。
(参考) twilioを使ってもいいだろう。

(音声認識、音声変換)
この分野で有名なnuance社のアプリケーションを利用する。
無料で使える条件があり、REST経由で操作できる使い勝手もいい。
appid、appkeyを取得しておくこと。

(形態要素解析)
mecabを使う。

(検索)
検索処理が一番のカナメであろうがここの説明は割愛する。
本気で商用化する場合はいろいろ難しい問題に直面するだろう。
ただし、簡易アプリであれば単純に検索エンジンへリクエストを投げ、それらしい電話番号を取得すればいいだろう。



◆ コード概要
● nuanceの使い方
${}は変数である。各環境で読み替えてほしい。

(音声をテキストに変換する場合)
$ curl "https://dictation.nuancemobility.net:443/NMDPAsrCmdServlet/dictation?\
appId=${appid}&\
appKey=${appkey}" \
-H "Content-Type: audio/x-wav;codec=pcm;bit=16;rate=8000" \
-H "Accept-Language: ja_JP" \
-H "Transfer-Encoding: chunked" \
-H "Accept: application/xml" \
-H "Accept-Topic: Dictation" \
-k --data-binary @${soundfile}

(テキストを音声に変換する場合)
$ curl "https://tts.nuancemobility.net:443/NMDPTTSCmdServlet/tts?\
appId=${appid}&\
appKey=${appkey}&\
ttsLang=jp_JP" \
-H "Content-Type: text/plain; charset=UTF-8" \
-H "Accept: audio/x-wav" \
-d "${answer}" > ${sound_tmp}

asteriskで利用する場合は、レートを変換する必要がある。
$ sox ${sound_tmp} -r 8000 -c1 ${sound}


● mecabの使い方
linuxのコマンドであれば対象の文字列をmecabコマンドへ標準入力で渡せばよい。
$ echo "こんにちは。自動で対応できますか?" | mecab
mecabを利用したアプリがあるので参考になるだろう。


● asterisk
スマートフォンのアプリストアから無料のsipクライアントをダウンロードし、以下を設定すれば電話できるようにする。

ドメイン               : 203.0.113.1
内線番号(sipアカウント)  : 201
パスワード             : 00000000


最低限動かすためのasteriskの編集ファイルは多くない。
この辺を見ておけばいいだろう。

(sipの設定)
/etc/asterisk/sip.conf
externip = 203.0.113.1
localnet = 192.168.0.0/255.255.255.0

[201]
type=friend
defaultuser=201
secret=00000000

(RTP(realtime transport protocol)の設定)
RTPのポート範囲を定義できる。
デフォルト値のままでいいだろう。
多くのポート番号が必要ない場合には範囲を絞ればよい。

/etc/asterisk/rtp.conf
[general]
rtpstart=10000
rtpend=20000


(フロー処理)
ここがフローを制御するメイン部分である。
extensions.confでフローを記載するのは手間である。
従来のextensions.confをより簡単に記述するための言語であるAEL(Asterisk Extension Language)を使う。

/etc/asterisk/extensions.ael
context default {
        201 => jump initialize@development;
};

context development {
  initialize => {
    files = "/usr/share/asterisk";
    cmds  = "/var/lib/asterisk";

    question_sound = "${files}/texts/question";
    question_text = "${files}/texts/question.txt";

    answer_text = "${files}/texts/answer.txt";
    answer_sound = "${files}/sounds/answer";

    sorry_sound = "${files}/sounds/sorry";
    thanks_sound = "${files}/sounds/thanks";

    g_fail = 0;

    goto start;

  start:
    Ringing;
    Wait(1);
    Answer(3000);
    Playback(${files}/sounds/intro);
    goto request1;

  request1:
    Playback(${files}/sounds/request1);
    goto find;

  find:
    Record(${question_sound}:wav);
    Playback(${question_sound});
    goto speech2text;

  speech2text:
    // space + ',' is not allowded.
    AGI(${cmds}/speech2text.sh,${question_sound}.wav,${question_text});

    if ( "${g_fail}" = "2" ){
       goto sorry;
    }

    switch ("${AGISTATUS}") {
      case "SUCCESS":
        goto recognize;
        break;
      default:
        goto request_again;
        break;
    }

  recognize:
    //AGI(${cmds}/search.sh,${question_text},${answer_text});
    AGISTATUS = "SUCCESS";
    switch ("${AGISTATUS}") {
      case "SUCCESS":
        goto text2speech;
        break;
      default:
        goto request_again;
        break;
    }

  text2speech:
    AGI(${cmds}/text2speech.sh,${answer_text},${answer_sound}.wav);
    switch ("${AGISTATUS}") {
      case "SUCCESS":
        goto finalize;
        break;
      default:
        goto request_again;
        break;
    }

  request_again:
    Playback(${files}/sounds/request2);
    g_fail = ${g_fail} + 1;
    goto find;

  sorry:
    Playback(${sorry_sound});
    Hangup;

  finalize:
    Playback(${answer_sound});
    Playback(${thanks_sound});
    Hangup;
  };

};



◆ 改善ポイント
簡易的に作っているので多く改善ポイントが見つかるだろう。

内部で呼び出す実行ファイルの引数に音声ファイルとテキストファイルを決め打ちした名前で渡している。
マルチ対応処理させるためには、ユニークな一時ファイル作るようにしなければならない。

IVR(Interactive Voice Response)の処理も非常に単純である。
問い合わせ者の意図に反する回答であれば再度処理を継続させるなりの考慮が必要である。
また自由文を受け付ける仕様にしたため種々の例外処理が発生するだろう。

あとはやはり検索方法である。
何を実現させたいかその仕様に応じて試行錯誤が必要である。