音声を文字起こしするAPIを何種類か触ってみた

この記事は [eeic (東京大学電気電子・電子情報工学科) Advent Calendar](https://qiita.com/advent-calendar/2018/eeic) 19日目の記事です。

今年もあと少しですね。卒論から目を背けながらこの記事を書いています。
eeicのアドベントカレンダー、色々な記事が入り混じっててとても面白いのでぜひ他の記事も読んでみてください。退学・休学ネタから始まり自分のやってる研究の話とか大学生活のtipsとかなんでもあると思います。どうやら１つのカレンダーに入りきらなかったらしくこないだ[eeic （東京大学工学部電気電子・電子情報工学科）その2 ](https://qiita.com/advent-calendar/2018/eeic2)もできていました。こっちにも面白い記事たくさんあるのでぜひご覧ください。

また、今年の僕のAdvent Calendarは2つ目になります。一つ目は今はまっている競プロのことをふわっと語ったのでこちらもよろしければどうぞ。
[競プロはいいぞ - Qiita](https://qiita.com/ysugiyama12/items/bc586215af1023ac0f5b)

さて、今回は文字起こしAPIの話を少ししようと思います。といっても音声認識のアルゴリズムとかに深く突っ込むわけではなく、一般向けに提供されている文字起こしAPIにはこんなものがあって、こんな特徴があるよっていうのをざっと紹介したいと思います。

# きっかけ
バイト先のとある業務で複数の音声認識APIを触る機会がありました。それまでに触ったことあるものといえば、GoogleスプレッドシートにYoutubeで再生している音声を自動で書き起こすやつ(調べれば出てきます、結構便利です笑)くらいでした。それぞれの文字起こしの処理を実装していく上で、APIごとに長所や短所などが明確になってきたので今回はそれをまとめておきたいと思います。

少し前にシステムが一通り完成し、そのプレスリリースがこないだ出ていました。もしよければご覧ください。
[TBSの音声認識API比較システム「もじひかクン」を開発](https://prtimes.jp/main/html/rd/p/000000006.000024407.html)

今回文字起こしをするために用いたAPIは以下の6つです。

- Google Cloud Speech-to-Text
- Watson Speech to Text
- Bing Speech API
- Amazon Transcribe
- NTT SpeechRec
- AmiVoice

前半4つまでは比較的有名だと思います。後半２つは無料で公開しているものはなく、その企業と直接やり取りすることでライセンスキーをもらうことでAPIの操作が可能となります。以下ではこれらのAPIを一つずつ紹介していきます。なお、文字起こしは全て日本語の音声を用いる想定です。

# 各APIの紹介
## Google Cloud Speech-to-Text
https://cloud.google.com/speech-to-text/docs/reference/rest/?hl=ja

Googleが開発を進めている音声認識APIです。最新のディープラーニングの技術をふんだんに取り入れているそうで、体感ではもっとも精度よく音声認識が可能です。特に雑音に対してとても頑健で、他のAPIでは全く意味のある音声が取り出されない音声に対してもこのAPIは会話部分のみを精度よく検出することが可能です。
この精度の支えとなっているのはやはりGoogleが有している莫大な量の学習データだと思っています。かなりマニアックなボキャブラリに対しても適切に変換して拾ってくれるのでその対応力の高さには驚かされました。後ほど各APIで同一の音声の認識結果を比較したものも示します。

GoogleのAPIの音声認識には主に2種類、「非同期通信API」と「同期通信API」　の2種類があります。
同期通信APIではクライアントが音声をサーバーに送信し、サーバが結果を返すまでこちらは接続を保ったまま待機する必要があります。手軽に実行することができる一方で、音声の最大長が60秒までという制限があります。
一方、非同期通信APIでははじめに音声ファイルを送信すると、各々の書き起こし処理にユニークなIDが割り当てられ、今後の結果はそのIDを用いて管理します。定期的にそのIDの状態をチェックしに行くことができ、もし文字起こしが正常に完了していればsuccessのステータスとともに書き起こし結果が表示されます。音声は最大3時間程度まで対応しているそうです。
ただし、非同期通信を用いた方法では音声ファイルはGoogle Cloud Storageに事前にアップロードしておく必要があります。この操作が少し面倒ではありますが、ネットワークの切断や安定性の低下などによる文字起こしの失敗がほとんど起こらなくなるのでメリットも大きいと思います。実際、６つのAPIの中でもっとも失敗が少ないのがこのGoogle APIでした。

Inputとして必要となるのはチャンネル数1のflacファイルです。ffmpegを用いて動画ファイルから変換する場合、以下のコマンドによって実行できます。例では16kHzの音声ファイルに変換しています。

```
$ ffmpeg -i input.mp4 -ab 160k -ac 1 -ar 16000 -vn output.flac
```
音声送信の部分ですが、公式のレファレンスに丁寧な説明があったのでそれにしたがって進めていくだけです。最初にGoogle APIの実装を行ったのでこの時はそれほどありがたみを感じなかったのですが、**このAPIは提供されている機能や情報がどちゃくそ親切です。**さすがGoogle、非エンジニアにでも簡単に触れるように作っているのですね。
簡単に実装部分のコードを貼り付けておきます。コードの解説はメインではないので重要そうな部分だけ抜粋して貼り付けておきます。

```python
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "config/gcs_config.json" # APIキーなどの情報を環境変数に設定

gcs_uri = 'gs://sample/' + filename + '.flac' # Google Cloud Storage上にuploadしたファイルを示すuri
headers = {
    'Content-Type': 'application/json',
}

with open("config/gcs_config.json", "r") as fp:
    gcs_config = json.load(fp)
    gcs_api_key = gcs_config['key']

params = (
    ('key', gcs_api_key),
)
data = {
    "config": {
        "encoding":"FLAC",
        "language_code": "ja-JP"
    },
    "audio": {
        "uri":gcs_uri
    }
}
data = json.dumps(data)

response = requests.post('https://speech.googleapis.com/v1/speech:longrunningrecognize', headers=headers, params=params, data=data)
request_name = response.json()['name']

res = requests.get("https://speech.googleapis.com/v1/operations/" + str(request_name), params=params)

```
音声の送信部分のみ示しました。あとはresの内容を定期的に見に行くことで結果が出ているかを確認することができます。こことは別にGCSへのアップロード処理もあるのですが、そこはPythonのライブラリを用いて簡単に行うことができます。

全体的に認識精度が高く、とても扱いやすいAPIでした。次に行ってみましょう。

## Watson Speech to Text
https://www.ibm.com/watson/jp-ja/developercloud/speech-to-text.html

こちらはIBM社が提供している音声認識APIです。Googleと並び利用者が多いAPIの一つであり、Pythonをはじめいくつかの言語でライブラリが提供されています。
音声認識は同期的通信のみをサポートしていて、音声ファイルの限度は長さではなくファイルサイズ(およそ100MB)となっています。100MBを超えたファイルを送信したところ適当な位置で切られた認識結果が返ってきました。
印象としてはノイズが強くなった途端認識結果が格段に悪くなるという感じでした。クリアな音声に対しては比較的精度の良い認識結果を示していたと思います。
また、認識された単語の信頼度についてword_confidenceというパラメータを結果内に有しています。この値によって、どのくらいの自信を持ってその単語を結果に示してきたのかがわかります。

Watson APIでも入力ファイルとしてflacファイルを用いました。変換コマンドは以下。

```
$ ffmpeg -i input.mp4 -ab 160k -ac 1 -ar 16000 -vn output.flac
```

このAPIもPythonで便利なライブラリが提供されていたのでそれを使わせてもらいます。API用のusernameとpasswordをSpeechToText関数に放り込むことで実行が可能となります。

```python

watson_username = "xxxxxxx"
watson_password = "yyyyyyy"
path = '/path/to/file'
lang = 'ja'

s2t = SpeechToTextV1( username=watson_username, password=watson_password)
s2t_json = s2t.recognize( audio=open(path, 'rb'), model=lang + '_NarrowbandModel', content_type='audio/flac', word_confidence=True)
```

とても単純ですね。同期的な通信なので音声認識が終了するとs2t_json変数に結果が格納される形になります。

Google APIよりは少し精度は落ちるものの、実装が簡単で使いやすいWatson APIでした。では次に行きます。

## Bing Speech API
https://docs.microsoft.com/ja-jp/azure/cognitive-services/Speech/home
Microsoftが提供しているBingのSpeech APIです。**今回の6つの中では問題児です。**その特徴をみていきましょう。

REST APIの形なので基本的な構成としてはAPIキーを元に期限付きのトークンを生成してヘッダーにのせ、POSTで音声データとともに送信するというものです。この仕組み自体はいいのですが、**一回の実行で30秒程度までの音声認識しか実行することができません。**何か他の方法があるのではないかと思い結構頑張って探したのですが見つかりませんでした。代替方法を何か知っている人がいたら教えてください。m(_ _)m

認識結果はそこまで悪くはありませんが、分割の境界線の部分でどうしても音声が切れてしまうのでそこで認識結果の乱れが生じていました。

元の音声を30秒ごとに分割しなければならかなかったので前処理が少し面倒でした。
まずいつも通り音声形式を変換します。wavファイルを使うことができたので今回は以下のコマンドで変換を行います。

```
$ ffmpeg -i input.mp4 -ab 160k -ac 2 -ar 16000 -vn output.wav
```
この後、得られたwavファイルを一つの長さが30秒程度になるように分割します。適当にファイルサイズを決めてその
大きさごとに切っていけば良いと思います。
分割し終えたファイルを一つずつPOSTで送信し、それらの文字起こし結果をつなぎ合わせることで結果を得ました。

```python
data = raw_data
params = {
    "version": "3.0",
    "requestid": "b2c95ede-97eb-4c88-81e4-80f32d6aee54",
    "appid": "D4D52672-91D7-4C74-8AD8-42B1D98141A5",
    "format": "json",
    "locale": lang,
    "device.os": "Mac",
    "scenarios": scenarios,
    "instanceid": "565D69FF-E928-4B7E-87DA-9A750B96D9E3" # from Sample Bot Framework
}

url = "https://speech.platform.bing.com/recognize?" + urllib.parse.urlencode(params)
headers = {"Content-type": "audio/wav; samplerate={0}".format(samplerate),
        "Authorization": "Bearer " + token }

response = requests.post(url, data=data, headers=headers)
```
tokenの生成方法は省略しましたが、bingのAPIキーを用いて別途POST通信を行うことでtokenが発行されます。

とにかく実装が面倒でした。しかも通信がたくさん発生するのでたびたび失敗します。要改善ですね。では次です。

## Amazon Transcribe
https://aws.amazon.com/jp/transcribe/

Amazonが提供している音声認識APIです。今はどうなっているかわかりませんが、このシステムを作った時には日本語の文字起こしはまだ提供されておらず英語のみだったためあまり細かなことは調べていません。
Googleの非同期通信と同様に文字起こしを実行したいファイルをAmazon S3上にアップロードし、S3上のファイルを用いて実行します。
音声ファイルの形式はBingのものと同じでwavファイルを使うことができます。

```python
transcribe = boto3.client('transcribe',region_name='us-east-1')
job_name = "job_name" + str(int(time.time()))

transcribe.start_transcription_job(
    TranscriptionJobName=job_name,
    Media={'MediaFileUri': job_uri},
    MediaFormat='wav',
    LanguageCode='en-US'
)
```

上記4つのAPIは公式のdocumentationが提供されており、ある程度は無料で使うことができるが、以下の2つはAPIを扱っている会社に直接連絡してAPIキーをもらう必要があります。
## NTT SpeechRec
http://www.v-series.jp/speechrec/
NTTが開発をしている音声認識APIです。wrapperやREST APIのような便利なものが提供されていないので、自分でデータをうまく整形して送ってやる必要があります。
音声の最大長やサンプリングレートなどの様々なパラメータをjsonファイルで指定し、音声は240msごとに分割して送ります。
また、送信する間隔を音声と同じくらいの長さ(240ms程度)空けないといけない仕様らしいです。  
音声ファイルはpcmファイルまたはrawファイルという制限があり、リトルエンディアンを指定してあげる必要があります。実装時にしばらくビッグエンディアンでやって動かないというやらかしをしてました。  

```
$ ffmpeg -i input.mp4 -f s16le -acodec pcm_s16le -ac 1 -ar 16000 output.pcm
```
サンプルコードとしてJavaのファイルを提供してもらったので、Pythonに直すことはせずそのままJavaファイルのまま使いました。結構細かい処理をごちゃごちゃやっていたので特定の箇所をピックアップして貼るということはしないでおきます。

## AmiVoice
https://www.advanced-media.co.jp/amivoice
株式会社アドバンスト・メディアが提供している音声認識システムです。他のAPIでは英語をはじめとした複数言語の文字起こしをサポートしていますが、このサービスは日本語のAPIのみをサポートしていてその分国内シェアはかなり高いらしいです。  
使う音声ファイルはNTTの時と同じくrawファイルなのですが、こちらはビックエンディアンを指定することに注意してください。

```
$ ffmpeg -i input.mp4 -f s16be -acodec pcm_s16be -ac 1 -ar 16000 output.raw
```
こちらもサンプルデータとしてJavaのソースコードをいただいたので特に大きな実装は行なっていません。ただ、内部ではビット演算のようなものを色々とやっていたので決して使いやすいシステムとは言えなさそうでした。  

## 文字起こし結果を比較してみよう
これら6つの文字起こし結果を実際に比べてみましょう。今回文字起こしを行う音声はYouTubeから適当に拾ってきた下の動画に含まれるものを使用しました。
[QRコード決済競争激化　100億円分ポイント還元も(18/11/23)](https://www.youtube.com/watch?v=HzekzeEUUD4)

この動画をinputとして与えると、日本語に対応していないAmazon以外の5つのAPIによって文字起こしが実行されます。
5つ並べてみてみましょう。 
 
### Google

`消費増税の還元策などからキャッシュレス決済に参入する会社が相次ぐ中スマートフォンを使った QR コード決済の会社が総額100億円文のポイントを還元する利用者拡大策を発表しました 32便利な決済手段を提供することだけではございません我々が目指したいのは新しいオカリナ誰を再発明することです QR コードなどを使うキャッシュレス決済の兄弟はソフトバンクとヤフーが50%ずつ出資して立ち上げられたサービスです来月4日からの利用金額の20%文をポイントで還元するキャンペーンの総額は100億円に登り台北は利用者や加盟店舗を一気に増やしたい考えです QR コードによるキャッシュレス決済は楽天や LINE などが相次いで参入するなどユーザー獲得競争が激しさを増しています `

### Watson
`消費増税の還元策などからキャッシュレス決済に参入する会社が相次ぐなんかスマートフォンを使ったＱＲコード決済の会社が総額百億円分のポイント還元する利用者拡大策を発表しました さんに便利なやっぱり何を提供することだけではございません われわれが目指したいのは新しいお金流れを再発明することです ＱＲコードなどを使うキャッシュレス決済の停滞はソフトバンクとＹＡＨＯＯな五十パーセントずつ出資して立ち上げられたサービスです 来月四日からの利用金額の二十％分をポイントで還元するキャンペーンの総額は百億円にのぼり弟弟は利用者や加盟店と一気に増やしたい関内です ＱＲコードによるキャッシュレス決済は楽天やないんなどが相次いで参入するなどユーザー獲得競争が激しさを増しています `

### Bing
`消費税のかんげんさくなどからキャッシュレス決済に参入する会社が相次ぐ中スマートphoneを使ったqrコード決済の会社が総額100億円分のポイント還元する理由破格大作を発表しました晩年便利な優しさを提供することだけではございません我々が目指したのは新しいお金流れを再発することです泣レコードなどを使うキャッシュレス決済の平米はソフトバンクとヤフーな50%出資して立ち上げられたサービスです来月4日からの利用金額の20%分をポイントでかネスレキャンペーンの総額は100億円に登りペンは利用者や亀井店舗日記に増やしたい考えですqrコードによるキャッシュレス決済は楽天やラインのニュースユーザー獲得競争な激しさを探しています`

### NTT
`消費増税の還元策などからキャッシュレス決済に参入する会社が相次ぐなかスマートフォンを使ったｑｒコード決済の会社が総額100億円分のポイントを還元する利用者拡大策を発表しましたパンに便利な決済手段を提供することだけではございません我々が目指したいのは新しいお借り流れを再発明することですｑｒコードなどを使うキャッシュレス決済のｐｔはソフトバンクとヤフーが50％ずつ出資して立ち上げられたサービスです来月4日からの利用金額の20％分をポイントで還元するキャンペーンの総額は100億円にのぼりベイビーは利用者や加盟店との日記に増やしたい考えですｑｒコードによるキャッシュレス決済は楽天やラインなどが相次いで参入するなどユーザー獲得競争が激しさを増しています`

### Amivoice
`消費増税の還元策などから、キャッシュレス決済に参入する会社が相次ぐ中、スマートフォンを使ったQRコード決済の会社が総額100億円分のポイントを還元する、利用者拡大策を発表しました。単に便利な優しさを提供することだけではございません。我々が目指したいのは、新しいお金流れを再発明する事です。QRコードなどを使うキャッシュレス決済のペーペーは、ソフトバンクとヤフーが50％ずつ出資して立ち上げられたサービスです。来月4日からの利用金額の20％分をポイントで還元するキャンペーンの総額は100億円に上り、ペーペーは利用者や加盟店分を一気に増やしたい考えです。QRコードによるキャッシュレス決済は、楽天や、LINEなどが相次いで参入するなど、ユーザー獲得競争が激しさを増しています。`

この音声は比較的クリアであればどの言語もそれほど悪い結果にはなっていないようです。しかし、ノイズなどが混じってくるとまた違う結果が出てくるので、この辺はAPIによって得意不得意があるようです。

## まとめ
~~いかがでしたか？~~ ←これ書くと薄っぺらいサイト認定されるらしいので気をつけましょう
6種類のAPIを調べて実際に文字起こしを行う部分まで実装しましたが、やはり使いやすさの差がすごくあるというイメージでした。やはりGoogleなどはいかにユーザに簡単に使ってもらえるかみたいな部分を重要視しているのかもしれないですね。
上の動画の文字起こしをみた限りではGoogleが一番優秀そうですね。やはり持っているデータの数が違うのでしょうか。それでも他のAPIでもそこそこマニアックな単語をうまく認識できているのでさすがだと思いました。また機会があれば他のAPIや、もっと根本的な音声認識の技術についても深掘りしてみたいですね。それではこの辺で失礼します。