この記事はSFC-RG Advent Calendar 2017 の24日目です.


序論

グローバル化の流れに乗り慶應義塾大学 環境情報学部/総合政策学部 にも英語化の波が来ている.徳田・村井・楠本・中村・高汐・バンミーター・三次・植原・中澤・武田合同研究室でも発表会が一部英語化されている.外国語を操るのは難しいことであり,英語での会話には話者の癖が出やすい事は想像に難くない.著者には「I mean」を多用する癖がある.一方,某教授には「You know」を多用する癖があり,授業や質疑の度に,性格のネジ曲がった一部の学生がTwitter で教授の「You know」を数えている.この現状に対し,声を大にして主張したい事がある.

数え上げは人間の仕事ではない!!!!!!!!!!!!!!!

数え上げはコンピュータが最も得意とするところであり,コンピュータにやらせるべき仕事を手動で行うとは,ITエンジニアとして嘆かわしい.よって本稿では機械学習プロダクトを利用して「You know」の数え上げを自動化する.機械学習プロダクトにはIBM Cloud のSpeech to Text を用いる.


開発環境等


準備

まずIBM Cloud のウェブインタフェースでアカウントを作成する.次に,そのアカウント内でSpeech to Text サービスインスタンス(インスタンス?)を作成し,Credentials を取得する.

IBM Cloud でのアカウント作成

アカウント作成で困る事はないと思うので省略.

IBM Cloud でのサービスインスタンスの作成

  1. IBM Cloud にログイン
  2. 左上のメニュー内「ダッシュボード」
  3. 右上の「リソースの作成」
  4. ほとんど一番下の「Speech To Text」
  5. 「価格プラン」の「Lite」を選択
  6. 右下の「作成」
  7. ダッシュボードのサービス内にSpeech To Textが出来てるはずなのでクリック
  8. 左の「サービス資格情報」
  9. 右側「新資格情報」
  10. 右下「追加」

以上の操作でCredential が作られて,「資格情報の表示」で確認出来るはず.


使い方

実装物をココからclone もしくはdownload した後,requirements.txt 内の依存パッケージをインストールする.

$ pip3 install -r requirements.txt

また,Credentials の設定を行う.speech.cfg.example をspeech.cfg にコピーし,上で確認したCredentialsを設定する.keyword も任意の文字列に設定可能である.

$ cp speech.cfg.example speech.cfg
$ emacs speech.cfg

[auth]
username = verydifficultusername
password = verydifficultpassword
[keyword]
keyword = you know

以上で準備は整った.以下のコマンドで実行する.

$ python3 ./stt-keyword-counter.py -t 10

-tオプションはtimeout を指定する.この例では10秒で終了する.


実装

実装物はココに置いてある.本稿ではstt-keyword-counter.py を実装した.その他の実装はibm-dev/watson-streaming-stt のfork そのままであり,stt-keyword-counter.py でもimport して利用している.transcript.py は,マイクから取得した音声をリアルタイムでAPI に送り,結果を表示するサンプルプログラムである.中身を読むと,マルチスレッドプログラムであり,マイクから音声を取得してAPI に送り続けるスレッドと,API から返ってきたメッセージを処理するスレッドがある事が分かる.本稿では返ってきたメッセージに用があるので,本稿用のon_message() とon_close() を実装する事にした.

ところで,Speech to Text API にリクエストを送ると結果がjsonで返ってくる.

{
	'results': [
		{
			'alternatives': [
				{
					'transcript': 'you know ', 
					'confidence': 0.939, 
					'word_confidence': [
						['you', 1.0], 
						['know', 0.906]
					], 
					'timestamps': [
						['you', 0.67, 0.87], 
						['know', 0.87, 1.25]
					]
				}, 
				{
					'transcript': "you don't "
				}, 
				{
					'transcript': 'you know I '
				}
			], 
			'final': True
		}
	], 
	'result_index': 0
}

入力された文章及び語の候補が並んでおり,その確信度(confidence)も提示されている.このメッセージでは’you know’が入力された可能性が最も高く,”you don’t” や’you know I’ も可能性も低いながら存在すると判定されている.’final’ は最終結論を示すbool 値である.IBM Cloud Speech to Text のリアルタイム変換では,音声データの送信中に,それまでに入力された音声を文章に変換した結果を載せたメッセージがAPI から随時返されてくる.この変換結果には二種類あり,速報値と最終値である.この速報値は,文章が完結していない等の情報不足により確信度が足りないものの,速さを優先して返してくる変換結果であり,更に先の文章が入力される事で,より確信度の高い変換結果に差し替えられる可能性がある.他方,最終値はこれ以上変更されない変換結果であり,その時点までの変換の最終結論である.音声入力が終了しておらずとも,機械学習での変換にとってキリが良いタイミングで最終値も随時送られてくる.速報値のメッセージでは’final’ がFalse となっており,最終値ではTrue となっている.

メッセージの中身が分かったところで,on_message() の実装を以下に示す.

FINALS = []
count = 0
keyword = None

def on_message(self, msg):
    """Print and 'say' the keyword if the keyword is found in the
    established result.
    """
    """
    While we are processing any non trivial stream of speech Watson
    will start chunking results into bits of transcripts that it
    considers "final", and start on a new stretch. It's not always
    clear why it does this. However, it means that as we are
    processing text, any time we see a final chunk, we need to save it
    off for later.
    """
    data = json.loads(msg)
    if "results" in data:
        if data["results"][0]["final"]:
            FINALS.append(data)
            count_ = data['results'][0]['alternatives'][0]['transcript'].count(
                keyword)
            for _ in range(count_):
                print("%s!" % keyword)
                os.system('say %s' % keyword)
            global count
            count += count_

on_message() はAPI からのメッセージを受信した時に呼び出される関数であり,main() 内でwebsocket.WebSocketApp のインスタンスを作成する際に指定される.

    ws = websocket.WebSocketApp(url,
                                header=headers,
                                on_message=on_message,
                                on_error=on_error,
                                on_close=on_close)

on_message() の実装に話を戻す.変更される可能性がある速報値に惑わされたくないので,’final’ がTrue である場合にのみ,メッセージのコンテンツからkeyword を探し,見つけた個数をグローバル変数count に足す.また,見つけた個数分だけ標準出力にkeyword を表示し,say コマンドでkeyword を読み上げる実装になっている.say コマンドではなくpyttsx3 を使いたかったが著者の環境では正常動作しないらしく,断念した.セキュリティ上は避けたいsystem 関数を使う事になったが,system 関数の引数となるkeyword はconfig ファイルから指定するので,今回は良しとしよう.

on_close() の実装を以下に示す.on_close() は,最終値として提示された文章を繋げたものと,keyword を見つけた個数を表示する.

def on_close(ws):
    """Upon close, print the complete and final transcript."""
    transcript = "".join([x['results'][0]['alternatives'][0]['transcript']
                          for x in FINALS])
    print("Recognized text is '%s'" % transcript)
    print("%d '%s' have been found!!" % (count, keyword))

なお,日本語を扱いたい場合には,API のURL を変更する事で米国英語向けから日本語向けに切り替えられる.main() 内に記述されている.

    url = ("wss://stream.watsonplatform.net//speech-to-text/api/v1/recognize"
           "?model=en-US_BroadbandModel")
    url = ("wss://stream.watsonplatform.net//speech-to-text/api/v1/recognize"
           "?model=ja-JP_BroadbandModel")

実行結果

本プログラムの実行結果を以下に例示する.なお,本実行では,ややこしさを避けるためにsay コマンドをdisable してある.enable してあるとsay コマンドの発声もマイクに拾われて大変ややこしい.

まず,マイクに向かって「You know I love you.」と発音してみた.

$ python3 ./stt-keyword-counter.py -t 5
* recording
you know!
* done recording
Recognized text is 'you know I love you '
1 'you know' have been found!!

この結果,一個の「you know」が検出された.最終値の集合も’you know I love you ‘となっており,正確に変換されている.次に,「You know I love you, you know you love me.」と発音してみた.

$ python3 ./stt-keyword-counter.py -t 5
* recording
* done recording
you know!
you know!
Recognized text is 'you know I love you you know you Love Me '
2 'you know' have been found!!

この結果,二個の「you know」が検出された.変換自体も正確に実行されている.


評価

某発表会の動画を入力して自動カウントし,手動カウントと照らし合わせて正確性を評価するのが筋だが,IBM Cloud Speech to Text の評価にしかならないので今回は省略する.


終わりに

本稿では,外国語での会話中に癖で多用してしまう語を数え上げる仕事を,機械学習プロダクトを利用して自動化した.本稿のシステムを用いる事で,性格のネジ曲がった学生がマニュアル You know カウンティングから解放され,授業内容に集中可能となる.

今後の発展としては,Twitterと連携して「You know」を検出した時に回数を自動ツイートする事が考えられる.また,IBM Cloud 以外の音声認識システムを用いる事で,IBM Cloud の無料分を使い切ってしまった場合や,オフラインでの動作も可能となる.

学生諸君には,本稿を通じて,「楽をするために手間をかける」ITエンジニアの心意気に触れてもらえれば幸いである.


謝辞

O先生ごめんなさい.


付録

著者はキリト君になりたいので,keyword をlink start に指定し,「リンクスタート!!」と連呼してみた. その結果,一度目の試行では

$ python3 ./stt-keyword-counter.py -t 10
 * recording
 * done recording
 link stopped link stopped link stopped
 0 "link start" have been found!!

との出力を得て,二度目では

$ python3 ./stt-keyword-counter.py -t 10
 * recording
 * done recording
 nine stop thinkstock need to stop thinkstock thinkstock
 0 "link start" have been found!!

との出力を得た.著者はキリト君になれなかった.