Twilio Speech RecognitionのpartialResultCallbackオプション

Twilioの新機能Speech Recogniton(音声認識)は、従来のTwiMLの<Gather>の拡張として実装されています。

<Gahter>はDTMFを認識する機能で、終了のキーの押下またはタイムアウトのタイミングで押されたキーの情報を取得することができます。

Speech Recognitionの場合も同様にタイムアウトのタイミングで、音声認識されて生成された文字列を取得できます。このような仕様なので、音声をリアルタイムに認識しつつ何かをやるようなアプリケーションを作るのは難しそうです。

しかし、ドキュメントによるとSpeech Recognitionをする場合の<Gather>のオプション・パラメータにpartialResultCallbackというのがあり、ここで指定したURLにTwilioはリアルタイムに部分的な音声認識の結果を送信するとのこと。

この機能を使うことにより、リアルタイムに処理を行うアプリケーションを作ることができないかと思い、partialResultCallback指定先にどのように情報が送られるのか試してみました。

送られる情報

  • SequenceNumber … シーケンス番号(五月雨式に送信されて順番が入れ替わって届くことがあるため)
  • Stability … 試した範囲では0.01か0.9のいずれかでした。
  • UnstableSpeechResult
  • StableSpeechResult … 試した範囲では常に空だったので、出力しませんでした。

動画

partialResultCallback指定先では上記情報をファイルに追記するようにし、tail -f の出力を画面に表示しています。

タイムアウト後、actionで指定したURLに送信される認識結果SpeechResultを最後にファイルに追記しています。 

出力結果

ファイルに出力された内容です。

Sequence Number = 3
Stability = 0.01
UnstableSpeechResult = '昔々'

Sequence Number = 2
Stability = 0.01
UnstableSpeechResult = '昔の'

Sequence Number = 1
Stability = 0.01
UnstableSpeechResult = '昔'

Sequence Number = 6
Stability = 0.01
UnstableSpeechResult = '昔々あると'

Sequence Number = 5
Stability = 0.01
UnstableSpeechResult = '昔々ある'

Sequence Number = 4
Stability = 0.01
UnstableSpeechResult = 'むかしむかし'

Sequence Number = 0
Stability = 0.01
UnstableSpeechResult = '床'

Sequence Number = 7
Stability = 0.01
UnstableSpeechResult = '昔々あるとこ'

Sequence Number = 9
Stability = 0.01
UnstableSpeechResult = '昔々あるところに'

Sequence Number = 11
Stability = 0.01
UnstableSpeechResult = '昔々あるところにおじい'

Sequence Number = 10
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお'

Sequence Number = 12
Stability = 0.01
UnstableSpeechResult = '昔々あるところにおじいさ'

Sequence Number = 14
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんと'

Sequence Number = 8
Stability = 0.01
UnstableSpeechResult = '昔々あるところ'

Sequence Number = 16
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さん'

Sequence Number = 19
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んで'

Sequence Number = 13
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さん'

Sequence Number = 17
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが'

Sequence Number = 18
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住ん'

Sequence Number = 15
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆'

Sequence Number = 22
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいました'

Sequence Number = 21
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいまし'

Sequence Number = 20
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んで今'

Sequence Number = 23
Stability = 0.9
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいました'

Sequence Number = 24
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたおじい'

Sequence Number = 29
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山'

Sequence Number = 27
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたおじいさんは'

Sequence Number = 26
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたおじいさん'

Sequence Number = 25
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたおじいさ'

Sequence Number = 30
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ'

Sequence Number = 28
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたおじいさんぱ家'

Sequence Number = 31
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へし'

Sequence Number = 33
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈り'

Sequence Number = 32
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へしば'

Sequence Number = 34
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りに'

Sequence Number = 35
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにお'

Sequence Number = 37
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあ'

Sequence Number = 38
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにお婆さん'

Sequence Number = 39
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは'

Sequence Number = 41
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川'

Sequence Number = 36
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りに'

Sequence Number = 43
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川伊勢'

Sequence Number = 42
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ'

Sequence Number = 44
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯'

Sequence Number = 40
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは彼'

Sequence Number = 49
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きました'

Sequence Number = 48
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きまし'

Sequence Number = 47
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きま'

Sequence Number = 46
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行き'

Sequence Number = 45
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に'

Sequence Number = 50
Stability = 0.9
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きました'

Sequence Number = 52
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばさん'

Sequence Number = 53
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばさんが'

Sequence Number = 51
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きました小正'

Sequence Number = 58
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で'

Sequence Number = 55
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばさんの顔'

Sequence Number = 59
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川です'

Sequence Number = 54
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばさんなか'

Sequence Number = 61
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯'

Sequence Number = 60
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で水'

Sequence Number = 56
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川'

Sequence Number = 63
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯して'

Sequence Number = 57
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばさんが変わっ'

Sequence Number = 62
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯し'

Sequence Number = 64
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯してい'

Sequence Number = 66
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると'

Sequence Number = 65
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯している'

Sequence Number = 67
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯しているとか'

Sequence Number = 71
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上'

Sequence Number = 72
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上か'

Sequence Number = 73
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から'

Sequence Number = 69
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると変わっ'

Sequence Number = 70
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると彼は彼'

Sequence Number = 68
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると彼は'

Sequence Number = 74
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃'

Sequence Number = 75
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃が'

Sequence Number = 77
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこ'

Sequence Number = 76
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃が飛ん'

Sequence Number = 78
Stability = 0.9
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこ'

Sequence Number = 79
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこ'

Sequence Number = 82
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れ'

Sequence Number = 83
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れて'

Sequence Number = 81
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらことな'

Sequence Number = 80
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと'

Sequence Number = 84
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れてき'

Sequence Number = 87
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れてきました'

Sequence Number = 85
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れてきま'

Sequence Number = 86
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れてきまし'

Sequence Number = 88
Stability = 0.9
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れてきました'

Speech Result:
'昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れてきました'

 

 最後に

この結果だけ見るとpartialResultCallbackが使い物になるかどうかは定かではありませんが、TwilioのSpeech Recognitionは裏でGoogle APIを呼び出しているということなので、Google APIの使われ方を検索するとうまい応用があるかもしれませんね。

参考

Twilio TwiML <Gather>の仕様 … TwiML™ Voice: <Gather> - Twilio

Twilio Blog … Introducing Speech Recognition - Public Beta Now Open

 

ESP8266とTwilio音声認識でヘボコン・ロボット用IoTモジュール作成

先に作ったヘボコン・ロボット用IoTモジュールを改良しました。

今回はTwilio SIGNAL 2017で発表された新機能、Speech Recognition(音声認識)とFunctionsを使ってみました。

仕様

ロボットが攻撃を受けたら自動的に操縦者に電話をかけ、音声合成で報告し、操縦者の音声による指示を認識して、ロボットのサーボモータを動かします。

デモ動画

 システム

f:id:tsun226:20170625215130p:plain

ESP-WROOM-02WiFi機能を持ったAdruino互換CPU(スイッチサイエンス製ESPr Developerを使用)

R … ルータ(iPhoneテザリングを利用)

Twilio … (REST APIによりFunctionsを起動)

Web Server … Dockerコンテナ上にnginx,php-cgi,twilio-php-master環境を用意(Twilioから音声認識された文字情報を受け取り保存し、またESP-WOOM-02にこの文字情報から生成したサーボモータ制御情報を渡す)

ロボットモジュール配線図

f:id:tsun226:20170625215148p:plain

動作

  1. ロボットのスイッチに敵ロボットが当たる。
  2. ESP-WROOM-02WiFi→ルータ→インターネット経由でTwilio REST APIに接続し、架電のリクエストをする。
  3. TwilioはFunctionsに登録されている指示に従い、操縦者に架電し音声合成によりメッセージを伝え、操縦者の音声を認識してWeb Serverに文字情報として伝える。
  4. Web ServerはTwilioから受け取った文字情報を保存する。
  5. ESP-WROOM-02は定期的にWeb Serverをポーリングし、4.で保存された文字情報から生成されたサーボモータ制御用情報(角度を表す数値)を取得し、サーボモータの角度をコントロールする。

ESP-WROOM-02スケッチ

定期的にWeb Serverをポーリングし、取得した値をサーボモータに出力します。

スイッチがONになるとTwilio REST APIに架電リクエストをPOSTします。

#ifdef ESP8266
extern "C" {
#include "user_interface.h"
}
#endif

#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <Servo.h>

ADC_MODE(ADC_VCC);

WiFiClientSecure client;
Servo servo;

// Twilio Call parameters
const char* asid = “{Twilio ASID}”;
const char* authorization = "Basic {TwilioのBASIC認証データ(BASE64)}”;
const char* twimlUrl = “{TwilioのFunctionsのURL}”;
const char* to = “{宛先電話番号}“;
const char* from = “{発信者電話番号(Twilio側電話番号)}}”;

// HeboCall parameters
const char *hebocallAuthorization = "Basic {Web ServerのBASIC認証データ(BASE64)}”;
const char *hebocallUrl = “{Web Serverサーボモータ制御用情報取得URL}“;

// Wi-Fi parameters
const char* ssid = “{ルータのSSID}”;
const char* pass = “{ルータのパスワード}“;

// Twilio REST API endpoint
const char* twilioHost = "api.twilio.com";
const char* twilioPath0 = "/2010-04-01/Accounts/";
const char* twilioPath1 = "/Calls.json";

// HeboCall endpoint
const char* hebocallHost = “{Web ServerホストのFQDN}“;
const int hebocallPort = {Web Serverのポート番号};
const char* hebocallPath = “{Web Serverのサーボモータ制御用情報取得ファイルパス}”;

#define IO_SWITCH 14
#define IO_LED0 13
#define IO_LED1 12
#define IO_SERVO 16
String content;
String hebocallContent;

void setup() {
  Serial.begin(115200);
  Serial.println("Setup.");
  pinMode(IO_SWITCH, INPUT);
  pinMode(IO_LED0, OUTPUT);
  pinMode(IO_LED1, OUTPUT);
  servo.attach(IO_SERVO);
  digitalWrite(IO_LED0, LOW);
  digitalWrite(IO_LED1, LOW);
  digitalWrite(IO_SERVO, 0);
  wifiConnection();
  makeContent();
  makeHebocallContent();
}

void loop() {
  if(digitalRead(IO_SWITCH) == HIGH) {
    if(WiFi.status() == WL_CONNECTED || wifiConnection()) {
      httpsPost();
      for(int i = 0; i < 500; i++) {
        led(10);
        delay(10);
      }
    }
  }
  led(50);
  checkAction(500);
  delay(10);
}

boolean wifiConnection() {
  WiFi.begin(ssid, pass);
  int count = 0;
  Serial.print("Connecting Wi-Fi");
  while (count < 50) {
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println();
      Serial.println("Connected. IP ADDRESS: " + WiFi.localIP().toString());
      return(true);
    }
    delay(500);
    Serial.print(".");
    count++;
  }
  Serial.println("Timed out.");
  return(false);
}

// POST request to Twilio
void httpsPost() {
  Serial.println("Connecting to Twilio API endpoint...");
  if (client.connect(twilioHost, 443)) {
    client.println(content);
    delay(10);
    Serial.print("Waiting Twilio API response...");
    String response = client.readString();
    int bodypos = response.indexOf("\r\n\r\n") + 4;
    Serial.println();
    Serial.println("RESPONSE: " + response.substring(bodypos));
    return;
  } else {
    Serial.println("ERROR");
    return;
  }
}

void makeContent() {
  String data = "Url=" + (String)twimlUrl + "&To=" + (String)to + "&From=" +   (String)from;
  String header = "POST " + (String)twilioPath0 + asid + (String)twilioPath1 + " HTTP/1.1\r\n" +
  "Host: " + (String)twilioHost + "\r\n" +
  "Authorization: " + (String)authorization + "\r\n" +
  "User-Agent: ESP8266/1.0\r\n" +
  "Connection: close\r\n" +
  "Content-Type: application/x-www-form-urlencoded;\r\n" +
  "Content-Length: " + data.length();
  content = header + "\r\n\r\n" + data;
  Serial.println(content);
}

void checkAction(int wait) {
  static int count = 0;
  String result;
  int angle;

  count++;
  if(count < wait) {
    return;
  }
  count = 0;
  result = httpsGet();
  angle = result.toInt();
  servo.write(angle);
  Serial.println("Servo angle = " + String(angle));
}


// GET Action from Web Server
String httpsGet() {
  String line;
  int i;
  int p, pos[2];

  Serial.println("Connecting to Web Server ...");
  if (client.connect(hebocallHost, hebocallPort)) {
    client.println(hebocallContent);
    delay(10);
    Serial.print("Waiting HeboCall response...");
    line = client.readString();
    p = line.indexOf("\r\n\r\n") + 4;
    for(i = 0; i < 2 && p < line.length(); i++) {
      p = line.indexOf("\r\n", p) + 2;
      pos[i] = p;
    }
    Serial.println();
    Serial.println("HEBOCALLRESPONSE: " + line.substring(pos[0], pos[1]));
    return(line.substring(pos[0], pos[1]));
  } else {
    Serial.println("ERROR");
    return("89");
  }
}

void makeHebocallContent() {
  hebocallContent = "GET " + (String)hebocallPath + " HTTP/1.1\r\n" +
  "Host: " + (String)hebocallHost + "\r\n" +
  "Authorization: " + (String)hebocallAuthorization + "\r\n" +
  "User-Agent: ESP8266/1.0\r\n" +
  "Connection: Keep-Alive\r\n\r\n";
  Serial.println(hebocallContent);
}

void led(int wait) {
  static int flag = 0;
  static int count = 0;

  count++;
  if(count < wait) {
    return;
  }
  count = 0;
  if(flag == 0) {
    digitalWrite(IO_LED0, HIGH);
    digitalWrite(IO_LED1, LOW);
    flag = 1;
  } else {
    digitalWrite(IO_LED0, LOW);
    digitalWrite(IO_LED1, HIGH);
    flag = 0;
  }
}

Twilio Functionsコード

メッセージを音声合成し送信し、受信した音声を認識してその結果(文字列)を{Web Server文字情報保存のURL}にPOSTします。

(この機能はWeb Serverに置いても、TwiML Binsを使っても可能ですが、新機能Functionsを使ってみたかったのです:-)

exports.handler = function(context, event, callback) {
    const message = "攻撃されています。攻撃されています。指示をしてください。"
    const messageNoRes = "指示を受け取れませんでした。"
    let twiml = new Twilio.twiml.VoiceResponse();
    let gatherParams = {};
    gatherParams.input = "speech";
    gatherParams.language = "ja-JP";
    gatherParams.timeout = "3";
    gatherParams.action = ‘{Web Server文字情報保存のURL}’;
    gatherParams.method = "POST";
    let sayParams = {};
    sayParams.language = "ja-JP";
    sayParams.voice = "alice";
    twiml.gather(gatherParams).say(sayParams, message);
    twiml.say(sayParams, messageNoRes)
    callback(null, twiml);
};

Web Server文字情報受け取りPHPコード

パラメータSpeechResultの値として渡されたTwilioにより認識された文字列を、ファイル /mnt/data/speech_text に保存します。

<?php
require_once dirname(__FILE__) . "/twilio-php-master/Services/Twilio.php";

$message_post = "。了解しました";
$speech_result = $_POST["SpeechResult"];
syslog(LOG_ERR, "speechresult = '" . $speech_result . "'");

# save file
$file_path = "/mnt/data/speech_text";
$result = file_put_contents($file_path , $speech_result . "\n");
if($result == FALSE) {
    syslog(LOG_ERR, "ERROR: Couldn't save speech result (" .  $speech_result . ") to file(" . $file_path . ")");
}

# make TwiML
$twiml = new Services_Twilio_Twiml();
$twiml->say($speech_result . $message_post,
    array(
        'language' => "ja-JP",
        'voice' => "alice"
    ));
print $twiml;

?>

Web Serverサーボモータ制御用情報取得PHPコード

ファイル /mnt/data/speech_text 保存されている文字列に含まれる文字により、0, 89 または 179 の角度情報を返します。

<?php
$file_path = "/mnt/data/speech_text";
$speech_result = file_get_contents($file_path);
if(preg_match('/(怒|怖|恐|攻|撃|襲|発|射|始|ang|aggr|att)/i', $speech_result) == 1) {
    print "179\n";
    exit;
}
if(preg_match('/(笑|楽|元|停|止|終|smile|laugh|easy)/i', $speech_result) == 1) {
    print "0\n";
    exit;
}

print "89\n";
?> 

果たして、このモジュールをどこかのヘボコンで披露する機会は来るのでしょうか。

ESP-WROOM-02(Arduino互換)とTwilioで電話をかける

まえがき

(読み飛ばしてかまいません)

2016年夏、息子がヘボコン・ワールドチャンピオンシップにに参加し、Arduino AGのファウンダーの一人であるDavid Cuartielles氏によりArduino賞に選ばれました。そして、副賞としてArduino/Genuinoスタータキットを2セット頂きました。

このイベントはとても盛り上がり、家族のいい思い出となりました。

自分は小学生の頃(40年以上前^^;)から電子工作を始めており、仕事はソフトウェア開発であるものの、ここ十数年PIC、ArduinoRaspberry Pi等いろんなことができるデバイスが簡単に入手できる環境になってきて、手を出したいと思っていました。そこに息子がArduinoをもらうというきっかけがあったので、これ幸いと息子と一緒にArduinoをいじり始めました。

電話を始めとするコミュニケーション・クラウド・サービスであるTwilioにも馴染みがあるので、ヘボコンArduinoTwilioというキーワードの組み合わせで何かできないかと思い、考えたシナリオはこうです。ヘボコンが攻撃を受けたら搭載されたArduinoがTwilio経由で電話を操縦者にかけて、攻撃されていることを音声合成で報告するというものです。

システム 

全体のシステムは下図の通りです。

 

f:id:tsun226:20170206213636p:plain

構成:

ESP-WROOM-02WiFi機能を持ったAdruino互換CPU(今回は開発用のスイッチサイエンス製ESPr Developerを使用)

R … ルータ(iPhoneテザリングを利用)

Twilio … (REST APIによりTwilioデベロッパーセンターのTwiML Binを利用)

動作:

  1. ヘボコン上のスイッチに敵のヘボコンが当たり、プログラムが起動する。
  2. Arduino(互換)がWiFi→ルータ→インターネット経由でTwilio REST APIに接続し、架電のリクエストをする。
  3. Twilioは事前に登録されている指示に従い架電し、音声合成によりメッセージを伝える。

 この記事では、Twilioの設定(TwiML)とESP-WROOM-02(Arduino互換)で動作させるプログラム(スケッチ)について主に説明します。

 

Twilioの設定

Twilioの基本的な使い方については、説明を省略します(Twilioのサイトを参照してください)。

TwiMLの作成

  1. Twilioサイトにログインし、デベロッパーセンター → TwiML Bins を選択します。
  2. 「+」マークをクリックし、TwiML Binを新規作成します。
  3. ConfigurationのFRIENDLY NAMEに任意の名前を、TWIMLには下記内容を記載します。
    「メッセージ」の部分が音声合成されます。任意のメッセージに置き換えてください。

    <?xml version="1.0" encoding="UTF-8"?>
    <Response>
    <Say voice="alice" language="ja-JP" >
    メッセージ
    </Say>
    </Response> 

  4. Save をクリックして登録します。

  5. たった今作成したTwiML Binをクリックしてもう一度内容を表示させると、URLが生成されています。
    このURLはスケッチで使われますので、コピーしておいてください。
  6. Cancelをクリックして閉じます。

ESP_WROOM-02のスケッチ

Mac上のArduino IDE 1.8.1を使いました。

  1. Arduino IDEESP-WROOM-02を開発するための準備をします。
    次のページを参考にしました。
    ESP-WROOM-02開発ボードをArduino IDEで開発する方法
  2. 次の内容のスケッチを作成します。

    #ifdef ESP8266
    extern "C" {
      #include "user_interface.h"
    }
    #endif

    #include <ESP8266WiFi.h>
    #include <WiFiClientSecure.h>

    ADC_MODE(ADC_VCC);

    WiFiClientSecure client;

    // Twilio Call parameters
    const char* asid = "アカウントSID";
    const char* authorization = "Basic ベーシック認証情報";
    const char* twimlUrl = "TwiML BinsのURL";
    const char* to = "通知先電話番号";
    const char* from = "発信元電話番号";

    // Router
    const char* ssid = "ルータのSSID";
    const char* pass = "ルータのパスワード";

    // Twilio REST API endpoint
    const char* twilioHost = "api.twilio.com";
    const char* twilioPath0 = "/2010-04-01/Accounts/";
    const char* twilioPath1 = "/Calls.json";


    void setup() {
      Serial.begin(115200);
      if (wifiConnection()) {
        httpsPost();
      }
    }

    void loop() {

    }

    boolean wifiConnection() {
      WiFi.begin(ssid, pass);
      int count = 0;
      Serial.print("Connecting Wi-Fi");
      while (count < 50) {
        if (WiFi.status() == WL_CONNECTED) {
          Serial.println();
          Serial.println("Connected. IP ADDRESS: " + WiFi.localIP().toString());
          return(true);
        }
        delay(500);
       Serial.print(".");
        count++;
      }
      Serial.println("Timed out.");
      return(false);
    }

    void httpsPost() {
      Serial.println("Connecting to Twilio API endpoint...");
      if (client.connect(twilioHost, 443)) {
        Serial.println("Connected.");
        String data = "Url=" + (String)twimlUrl + "&To=" + (String)to + "&From=" + (String)from;
        String header = "POST " + (String)twilioPath0 + asid +       (String)twilioPath1 + " HTTP/1.1\r\n" +
        "Host: " + (String)twilioHost + "\r\n" +
        "Authorization: " + (String)authorization + "\r\n" +
        "User-Agent: ESP8266/1.0\r\n" +
        "Connection: close\r\n" +
        "Content-Type: application/x-www-form-urlencoded;\r\n" +
        "Content-Length: " + data.length();
        client.println(header);
        client.println();
        client.println(data);
        delay(10);
        Serial.print("Waiting Twilio API response...");
        String response = client.readString();
        int bodypos = response.indexOf("\r\n\r\n") + 4;
        Serial.println();
        Serial.println("RESPONSE: " + response.substring(bodypos));
        return;
      } else {
        Serial.println("ERROR");
        return;
      }
    }

     

動作の様子(デモ動画)

 上記のシステムを少しアレンジしたものの、動作の様子です。

 

とても簡単に、小さなモジュールが電話をかけ音声合成でメッセージを伝えるシステムを作ることができました。

WiFi機能を持つArduino(互換)を使えばインターネットに接続できるので、インターネット・サービスを利用するIoTモジュールを簡単に試作することができますね。

Twilioを使った気象情報架電通知システムの試作

1. 概要

気象情報の変化を市区町村別、かつ注意報/警報/特別警報別のアカウントに投稿する、既存のツイッターbotを拡張し、登録してある電話番号に架電して音声合成で通知するシステムを試作しました。

ユーザは下記情報を登録し、該当する気象情報が発生した時に、架電通知されます。

  • 電話番号
  • エリア(市区町村)
  • 通知して欲しい情報
    ー 注意報/警報/特別警報別
    ー 発表/解除/継続

 

Pubsubhubbubプロトコルにより気象庁の実証実験で提供されている気象情報が当システムに通知され、その情報を解析してメッセージを構成し、Twilio APIに架電と音声合成による通知を要求し、Twilioがユーザに対し架電通知を行います。

f:id:tsun226:20160416140220p:plain

 

2. 入力

気象庁の実証実験では、Googleが運用するAlert Hubを介してpubsubhubbubプロトコルによるPush方式で、都道府県単位の気象情報の変化の概要がAtom形式で通知されます。

② この概要情報には詳細情報(市区町村単位の注意報/警報/特別警報)へのリンクが含まれており、概要情報に含まれる都道府県名が、当システムが架電通知またはツイートするエリアを含むときに、HTTP GETによりXML形式の詳細情報を取得します。

f:id:tsun226:20160416140253p:plain

 

3. システム

 4つのDockerコンテナにより構成されています。

いずれのコンテナにおいてもnginxが稼働しており、HTTPアクセスによりperlまたはphpスクリプトが起動され、処理されます。

 今回、既存システムにPhoneコンテナDockwilioコンテナを追加して、システムの拡張を行いました。

f:id:tsun226:20160416194919p:plain

 

3.1. AlertSubコンテナ(既存)

2.で述べたAlert Hubや気象庁からの情報を受け取り、解析して、エリア毎*1に通知する要素(発表/解除/継続、注意報/警報/特別警報、注意報警報の内容)を構成し、PhoneコンテナTweetコンテナに通知します(図の③、⑥)。

*1  架電通知またはツイート対象として登録されているエリアのみ対象にします。

 

3.2. Phoneコンテナ

登録ユーザ情報から、通知して欲しい条件がAlertSubコンテナから通知されたエリア、発表/解除/継続、注意報/警報/特別警報に一致するユーザを検索し、通知要素を元に構成したメッセージと電話番号をDockwilioコンテナに通知します(図の④)。

 

3.3. Dockwilioコンテナ

Phoneコンテナから通知された情報に従い、架電するようにTwilio APIに要求し(図の⑤)、その後TwilioからのHTTPS GETアクセスに対しメッセージを音声合成して伝える旨通知します(図の⑥)。

  • Twilioは通話をはじめとする各種コミュニケーション手段を提供するクラウドサービスです。
  • DockwilioコンテナはTwilioを使った架電と音声合成メッセージ通知を簡単にできるDockerコンテナです。

 

3.4. Tweetコンテナ(既存)

AlertSubコンテナから通知された情報のエリア、注意報/警報/特別警報に対応するツイッターのアカウントに、構成したメッセージを投稿するようにTwitter APIに要求します(図の⑦)。

 

AlertSub, Phone, Tweetのコンテナは、emerry/jmaをカスタマイズしたものです。

Dockwilioコンテナのリポジトリemerry/dockwilioです。

当システムは試作システムで、一般にサービスを提供していません。

Dockwilio - Twilioを制御するDockerコンテナ(実験バージョン ) = コンテナ仕様編 =

更新:ver.2015.12.12リリースに伴い一部変更しました。

 

Dockwilioは、Twilioを使って簡単に架電&音声合成メッセージ再生をすることができるDockerのコンテナです。

仕様を説明します。

 

仕組みについては概要編を、実行例については簡単手順編を参照してください。

 

Dockwilioは次の2つの機能を持っています。

  • TwilioREST APIにに対し架電を要求する。
  • 上記要求によるTwilioからのHTTPアクセスに対し、音声メッセージのTwiMLを返す。

実験バージョンでは<Say>動詞1回実行のみサポートします。

コンテナはcentos6がベースで、内部で2つのnginxプロセスが稼働します。

 

1. 設定ファイル

Twilioの認証情報や発信元電話番号等を設定します。

ファイル名:dwilio.conf

 dwilio.conf はYAMLで記述します。

設定する内容については 4.パラメータ で説明します。

2. Dockwilioコンテナの起動

  • -d を指定し、detachedします。
  • -p 443:443 を指定し、TwilioからのTwiML要求をコンテナにフォワードします。
  • -v {設定ファイルのディレクトリ}:/etc/dockwilio:ro を指定し、設定ファイルのあるディレクトリをマウントします。
  • イメージは emerry/dockwilio です。

        起動例:

docker run -d -p 443:443 -v /dockwilio_config_dir:/etc/dockwilio:ro -v /etc/localtime:/etc/localtime:ro --name dwilio emerry/dockwilio

3. 架電&音声メッセージ要求

架電先電話番号、再生する音声メッセージ等を指定します。

ホスト内からDockwilioTCP 80番ポートに対し、YAMLまたはJSON形式でHTTP POSTします。

指定する内容は 4.パラメータ で説明します。

4. パラメータ

1.設定ファイル、3.架電&音声メッセージ要求 で設定/指定できるパラメータは下記の通りです。

パラメータ機能設定ファイル架電要求時備考
account_sid Account SID  
auth_token Auth Token  
from 発信元電話番号 E.164形式
twiml_url_base ホストのURL 必須 N/A https:で始まりファイルパスは含まない
basic_user BASIC認証のユーザ名 N/A 指定しない場合はBASIC認証を行わない
basic_password BASIC認証のパスワード N/A 指定しない場合はBASIC認証を行わない
call_timeout 呼び出し待ち時間(秒) デフォルトは60秒
language <Say>動詞の言語指定 デフォルトは ja-JP
voice <Say>動詞の音声指定 デフォルトは alice
syslog_facility syslogのfacility 必須  
twiml_ssl_crt TwiMLアクセス用SSL CERTIFICATEファイル名 必須 N/A 設定ファイルと同じディレクトリに配置
twiml_ssl_key TwiMLアクセス用SSL KEYファイル名 必須 N/A 設定ファイルと同じディレクトリに配置
debug デバッグ出力フラグ(true/false) N/A デフォルトはfalse
to 架電先電話番号 E.164形式
say 音声合成するメッセージ  

5. 戻り値

架電処理終了後の戻り値は、架電要求と同じ書式です。

codestatusmessage備考
0 completed  

正常終了

1 busy   話中
no-answer   無応答
-1 canceled   キャンセルされた(システムの問題)
failed   失敗した(システムの問題)
(なし)   Service_Twilio_RestException ライブラリのエラー(システムまたはプログラムの問題)
その他     エラーと警告の辞書参照

Dockwilio - Twilioを制御するDockerコンテナ(実験バージョン ) = 簡単手順編 =

更新:ver.2015.12.12リリースに伴い一部変更。

 

Dockwilioは、Twilioを使って簡単に架電&音声合成メッセージ再生をすることができるDockerのコンテナです。

概要編に続き、実行するための簡単手順を説明します。

 

前提:

  • Dockerエンジンがホストにインストールされている。
  • Twilioにアカウントがあり、発信用電話番号を購入してある(Account SID, Auth Token、電話番号の情報が必要)。
  • TwiMLアクセスのためのSSLサーバ証明書を用意してある。
  • Dockwilioへの架電要求はホスト内から行う。

なお、Dockwilioイメージはcentos6がベースとなっており、内部で2つのnginxプロセスが稼働します。

 

1. 準備

TwilioからTwiMLを取得するためのHTTPSアクセスを許可します。

# iptables -A INPUT -p tcp -m tcp -dport 443 -j ACCEPT 

 

Dockwilioの設定ファイルを作成します。

ファイル名は「dwilio.conf」でYAML形式で記述します。

account_sid: ACxxxxxxxxxxxx
auth_token: xxxxxxxxxxxxx
from: +8150xxxxxxxx
twiml_url_base: https://hostname.example/
syslog_facility: local0
twiml_ssl_crt: fullchain.pem
twiml_ssl_key: privkey.pem

twiml_url_base の hostname.example にはDockwilioが稼働するホストのFQDNを指定します。

 2. Dockwilioコンテナの起動

docker run -d -p 443:443 -v /dockwilio_config_dir:/etc/dockwilio:ro -v /etc/localtime:/etc/localtime:ro --name dwilio emerry/dockwilio

 ここで、/dockwilio_config_dir は設定ファイル(dwilio.conf)およびSSL証明書のあるディレクトリです。

コンテナを起動すると、コンテナのIPアドレスがsyslog(local0)に出力されます。

Nov 22 11:32:20 4acb42090932 logger: IP address of Dockwilio: inet 172.17.0.51/16 scope global eth0

 3. 架電と音声メッセージ再生の実行

 ホスト内からDockwilioコンテナに対しHTTP POSTすることで(YAMLまたはJSON形式のいずれか)、架電の要求をします。

URLは「http://DockwilioコンテナのIPアドレス/say.{yaml,json}」で、架電の終了後に結果とともに戻ります。

 

下記はcurlコマンドを使用した例ですが、Content-length を指定する必要があるためlsコマンドで予めサイズを調べています。

YAMLの例:

$ ls -l post_data.yaml
-rw-r--r-- 1 user users 49 Nov 21 12:59 2015 post_data.yaml

$ cat post_data.yaml
to: +8180xxxxxxxx
say: Hello. How are you doing?

$ curl -XPOST http://172.17.0.51/say.yaml -H 'Content-length: 49' --data-binary @post_data.yaml
code: 0
status: completed
message: 

JSONの例:

$ ls -l post_data.json
-rw-r--r-- 1 user users 62 Nov 21 13:06 post_data.json

$ cat post_data.json
{
"to": "+818046684833",
"say": "Hello. How are you doing?"
}

$ curl -XPOST http://172.17.0.51/say.json -H 'Content-length: 62' --data-binary @post_data.json
{“code”:”0”,”status”:”completed”,”message”:””}

 

 仕様についてはコンテナ仕様編を参照してください。

 

 

 

Dockwilio - Twilioを制御するDockerコンテナ(実験バージョン ) = 概要編 =

Twilioは、電話サービスを使ったアプリケーションを作って提供するためのCloudサービスです。

しかし、Twilioを使うためにはWebサーバを用意し、そこにアプリケーション(TwiMLと呼ばれるXML)を配置しなければならないというのが、初学者にはわかりづらくハードルを高くしていると思われます。

例えば、電話をかけて音声メッセージを伝えるだけの場合、もしTwilioに相手の電話番号とメッセージ内容をAPIに伝えるだけでできれば簡単なのですが、実際にはメッセージ内容をWebサーバに配置し、それからREST APIに架電要求しなければなりません。

f:id:tsun226:20160416001253p:plain

 

このような単純なアプリケーションを実現するためにだけにWebサーバを構築し、TwiMLの管理(生成、削除)をしなければならないのは、ちょっと面倒ですね。

 

そこで作ったのがTwilioを制御するためのDockerコンテナであるDockwilioです。

Dockwilioへの1回の要求で、架電と音声メッセージの再生を行えるものです。

 

具体的には、設定ファイルにTwilioの認証情報等を記述してDockwilioを起動しておき、HTTP POST により相手の電話番号やメッセージ内容等を伝えると、Twilioを制御して架電し、音声合成によるメッセージを再生します。

Dockwilioは常駐するコンテナで、HTTP POSTにより何度でも架電することも可能です(同時架電も可能)。

また、TwiMLの生成と削除はDockwilio内部で行われ、ユーザが意識する必要もありません。

f:id:tsun226:20160416001429p:plain

Dockwilioは、Dockerのコンテナにより構築されるサービスにおいて、架電&音声メッセージによる通知が必要なときにパーツとして組み合わせるのに適しています。

 

使い方については簡単手順編 を、仕様についてはコンテナ仕様編を参照してください。