【Google Cloud】音ゲー会議 議事録の文字起こしサービスを作った

あらすじ

議事録の書き起こしがめんどくさい。


数時間の会議を何度も聞き返しながら文字に起こすのは相当のハードワークで時間がとられる。
大学の後輩のために書き起こし作業の負担を少しでも軽減させたいと思い、自動書き起こしサービスを作ろうと思い立つが...

※主にGoogle Speech-to-Text APIを使ってサービスを作ろうとして当たった問題について綴ったメモです。内容を端的に知りたい方は次の「できたもの」と最後の「要約」をご覧ください。

できたもの


Web上でサービスのURLにアクセス。Googleアカウントでログインする。
(事前にシステム管理者(俺)側でユーザーの登録作業を済ませておく)

書き起こしたいローカルの音声ファイル(wav, mp3)を選択してアップロードボタン押下


下のテーブルから書き起こしジョブの進行状況を確認できるのでしばらく待つ
(1時間の音声なら15分ぐらい)



完了したらダウンロードリンクが出てくる。押すとtxtファイルのダウンロードが始まる



中身はこんな感じ

 

自分

HKCHO

京音OB。京音とは無関係

きっかけ

音声データから自動音声認識機能を用いた文字起こしを行うサービスの存在を知った。
どうやらAWS, Google Cloud, Azureといった大手クラウドサービスはどこも出しているらしい。AWS以外はリンク先で簡単にお試し利用できる。
Amazon Transcribe
https://aws.amazon.com/jp/transcribe/
Google Speech-to-Text
https://cloud.google.com/speech-to-text/
Azure Speech to Text
https://azure.microsoft.com/ja-jp/products/cognitive-services/speech-to-text/

作りたいものの要件

目的

大学のサークルで行われる話し合いを文字起こしする際にその負担を軽減する。
書き起こした文章はブログ記事になったりするのだが、エンタメ系の記事なので要点をまとめた実務的なものではなく、実際の話しぶりを再現した会話形式のものが多い。
自動書き起こしとの相性がよさそうと判断。

想定業務量

利用頻度 : めちゃ少ない(月1~3回とか)
ユーザー数 : めちゃ少ない(書き起こしする人は1人いればいいので)
リソース使用量(1回): 結構重め(音声は最大3,4時間ぐらい。下手すると数GBぐらいの音声ファイルが飛んでくる)
→当然1回のAPI呼び出しで一瞬で書き起こして返せるような量ではないので、アップロードをトリガーとしたバッチ処理が前提になってきそう。

とりあえず全部 Google Cloudで作ってみることに。

アーキテクチャ

主に以下の4サービスを使用
App Engine(Web層, API)、Cloud Functionsバッチ処理)、Cloud Storage(音声・テキスト)、Datastore(書き起こしジョブの管理)
イメージとしてはこんな感じ

  1. ユーザーはVue.jsのWebアプリからAPIサーバーを介して音声データをCloud Storageにアップロードする
  2. Pub/Subで1.のアップロードをトリガーとしてCloud Functionsを起動、変換ジョブ開始
  3. 変換ジョブ内でCloud Storageから音声データを取得してSpeech-to-Text APIを叩く。レスポンスをテキストデータに編集して再びCloud Storageに格納。署名付きURLを発行する

1~3の進捗はDatastore上で管理し、次のステップに進むたびに逐一更新していく。
ユーザーはこのジョブの状態をWeb上で見ることができる。ダウンロードリンク(署名付きURL)もDatasotreで管理し、ユーザーはそれを見てダウンロードする。

実装してみた

全部書くとちょっと長いのでなぜそうしたか、料金などをメインに

App Engine

フロント&APIサーバーの2つのアプリで使用。
Cloud Runを使う手もあるが、Identity-Aware Proxyを使う際にロードバランサーの設定が不要なのでこちらを使った。Web層はどうせ大した負荷はないはずなので。

フロント側は、Vue.js(+bootstrap)を使用したが、あまり手間取りたくないので1画面のみの小さな構成に。コンポーネントも1個しか作っていない
API側は、小さいアプリケーション向きと言われているらしいPython+flaskを使用。

本当はローカル環境からGoogle上のリソースにアクセスするような設定をすべきだと思うが、めんどくさくて本番環境に上げてはテストしていた

ちなみに無料枠を使用したいためスタンダード環境を選択したが、無料枠は(インスタンス数)×(稼働時間)を毎日28以内に収める必要がある。アプリを2つ立ち上げている以上、インスタンス数は最小でも2になるため24時間稼働するとオーバーになるのだが、ユーザーがいつアクセスするのかもよくわからないし、最大インスタンス数のみ1に設定してあとはオートスケーリングに任せることにした。フルに稼働したとしても書き起こしのジョブに比べれば微々たるものだろう。
アップロードするファイルのサイズ分だけリソースを消費せざるをえないことも問題だが、料金体系を調べると意外とこちらも微々たる数値だった

Identity-Aware Proxy (IAP)

認証機能として使用。ログイン画面を自分で実装しなくとも、この設定を行うだけでGoogleアカウント使ったユーザー認証が行える。アクセスを許可するユーザーの登録に関しては手動で行った。無料

Datastore

NoSQLデータベース。RDBであるCloud SQLと比べて料金が安く、シンプルにジョブの管理をしたいだけなので、ここでJSONデータを管理した。
実際今回のシステムで料金計算ツールを使って見積もってみたところ、Cloud SQLなら数ドル/月の料金がかかるのに対し、Datastoreなら無料枠内に余裕で入ってしまう計算に。

Pub/Sub

Cloud Functionsのトリガーとして使用した。Cloud Storageの指定のフォルダに音声ファイルがアップロードされるとメッセージがPub/Subに送信される。Cloud Functions側の設定でそれに応答して起動するようにする。
どうやらアップロードに限らず、ストレージに何らかのイベント(登録更新削除)があった場合にメッセージ送信が行われるらしい。つまり、音声ファイルを削除した場合もCloud Functionsが起動してしまうということだ。
メッセージ内にファイル名の情報が入っているので、それを元にCloud Functions側で本当にファイルの存在確認を行うことで対処した。ちなみにメッセージの内容からはファイルが登録されたのか削除されたのかよく分からなかった。
料金はメッセージのサイズが合計10GB/月であれば無料らしいので多分無料枠内だろうと判断した

Cloud Storage

今回の料金発生源その1。無料枠は5GB/月らしいので、wavファイルを無造作に置いたままにしておくと余裕で超えるし、合計ファイルファイルサイズに比例して課金が発生してしまう。ということでライフサイクル設定で1週間で削除することにした。音声ファイルは文字起こしが終われば不要だし、テキストファイルも永久に保存しておく必要はないはず

Cloud Functions

Speech-to-Text APIを呼び出すジョブとして使用。基本的にはリクエストに音声データを設定し、レスポンスをテキストファイル化して、ストレージに置くだけの簡単なジョブになるはずだったもの
呼び出し回数が200万回/秒まで無料など相当大量のリクエストでも無料枠に収まるそうなのでこちらも料金はたかが知れているだろう

Speech-to-Text API

料金発生源その2無料枠は音声が60分/月以内の場合のみのため、今回はどうしても課金が発生せざるを得ない。
ちなみに現在料金は1分0.024ドルのため、音声1時間あたり200円ぐらいで見積もるといい感じ。
調べてみたところAWS、Azureも似たような料金体系をしていて、単価もほぼ同じだった。競合他社同士で差がつかないよう意図的に揃えているのだろう

【実装中に直面した問題】

大小さまざまですが、実装中に手間取った部分を列挙しました

1つのプロジェクトで2つのApp Engineアプリを起動する

app.yamlを2つ用意してservice名を設定する
→それぞれをgcloud app deploy
デフォルトだと下記のようなURLが割り当てられる。変更もできるようだがURLにこだわりはないのでそのままにした。

https://[サービス名]-dot-[プロジェクトID].appspot.com/

https://www.serversus.work/topics/vyly8dwer5uql5ra5xdg/

まだCORS(Cross-Origin Resource Sharing)の設定で消耗しているの?

はい、私はCORS(Cross-Origin Resource Sharing)の設定で消耗しています。
↑にも書いたように、アプリごとにオリジンが異なるため、フロント側からバックエンドのAPIを呼び出そうとするとCORSエラーが発生する。
いろいろ調べてFlask-CORSを使えだの様々な情報が出てきたが、今回の構成に合致していい感じになる対処方法は以下の通りだった。
https://stackoverflow.com/questions/51756878/react-js-on-app-engine-as-a-service-querying-an-api-running-as-another-service-o
要するにdispatch.yamlを使ってルーティングの設定を行い、フロント側からAPIを叩く際は全部その送信先に送るようにする、みたいな感じ。バックエンド側で特定オリジンからのリクエストを許可する、みたいな設定をどっかに追加する方法ばかり探していて手間取った

Storageの特定フォルダが更新されたときのみPub/Subにメッセージ送信したい

デフォルトではStorage→Pub/Subの更新の通知はバケット単位で行われるので、特定のフォルダ(音声データのみ置くフォルダのみ)の更新のみ検知したい場合は -p オプションを使う。

gsutil notification create -p photos/ gs://example-bucket

大したことではないが意外とこの情報にたどり着くのに時間がかかった。公式で日本語訳なかったし
公式:
https://cloud.google.com/storage/docs/gsutil/commands/notification

Speech-to-Text APIに渡した方がいい情報

Speech-to-Text APIに渡すパラメータにの中にチャネル数(audio_channel_count)サンプリング周波数(sample_rate_hertz)がある。

オーディオファイルの仕組みには詳しくないのだが、pythonのライブラリであるpydubを使用すると比較的簡単に取得することができる模様。
https://algorithm.joho.info/programming/python/pydub-time-sampling-rate-channel/
ちなみにサンプリング周波数は渡さない方法もあるようだが露骨に処理時間が増えるのでやっぱり渡した方が良い。

さらに変換の精度を上げる方法として音声適応(SpeechContext)がある。
会話のシチュエーションが限られているなど、特定の単語が高頻度で登場する場合はその単語をパラメーターに渡しておくとより出やすくなるそうだ。
公式ドキュメントではwhether, weatherの例が掲載されている
https://cloud.google.com/speech-to-text/docs/speech-adaptation?hl=ja

mp3ファイルを扱う場合は別ライブラリが必要

GoogleのSpeech-to-Text APIが対応しているのは基本的にwavファイルのみで、mp3を扱えるライブラリは一応beta版(speech_v1p1beta1)という扱いになっている
使用感に差はない

様々な処理上の限界

サイズのデカい音声ファイルの扱うので、複数のサービスで上限・限界に直面し、やりくりに悩まされた。今回一番悩んだところである

HTTPリクエスト(ファイルアップロードのAPI)の限界

App Engineスタンダード環境ではHTTPリクエストのタイムアウトが10分、リクエストサイズ上限が32MBに設定されているらしい。そんな枠に収まるわけがないので、分割してアップロードする必要がある。

Cloud Functionsの限界

Cloud Functions(第1世代)は540秒でタイムアウトする。つまり、文字起こし処理に時間がかかってしまう場合は、ここでも音声を分割してSpeech-to-Text APIに流す必要があり、書き起こし結果をガッチャンコしないといけない。
そして残念ながらSpeech-to-Text APIは長時間の音声の場合それなりに処理時間を要するのでそう対処せざるをえなかった。
Cloud Functions 第2世代ならタイムアウト時間も長いからそっちなら?とも思ったが、Cloud StorageやPub/SubをトリガーとするとするEventarcトリガーの場合、第2世代でも540秒でタイムアウトする。他の方法があるかもしれないが今のところ解決方法は見つけていない。

Speech-to-Text APIの限界

まずはどのぐらい処理時間がかかるのかについて調査してみたが、だいたい音声時間×0.3~0.7ぐらいの時間がかかるようだ。10分の音声を流すと返ってくるまで3~7分ぐらいかかる計算だ。
60秒以上の長い音声の文字起こしにはlong_running_recognizeという非同期メソッドを使う。公式ドキュメント上は「非同期音声認識の上限は 480 分です」とあるが、Cloud Functionsのタイムアウトボトルネックになって実質的にはそんな長時間の音声を変換できない。そもそも数十分もかかる処理をAPIで呼び出すこと自体が良くない気がするし。
さらに言うと、同時並行的に何回も呼び出すと処理時間が増えてタイムアウトする。例えば180分の音声を10分ずつ分割してAPIを18回同時に呼び出したりするとなんかそんな感じになった。
ちなみに今回はあまり関係ないが、Cloud Storage以外のローカルファイル等をAPIのリクエストに乗せる場合は10MBの制限もあるらしい。

インスタンスの限界

同時並行でAPIをいっぱい呼び出すとパンクしちゃうなら文字起こしバッチの最大インスタンス数を制限すればよいのでは?と思ったが、Pub/Sub→Cloud Functionsで長い待ちが発生するとなんか怒られる
ということで最終的に、長時間の音声の場合は
1時間ずつ区切る→それを10分で分割して同時並行処理→全部終わったら次の1時間を区切る→...
みたいな処理の流れにした。

音声分割処理の限界

上記様々な制約から音声を小分けしてちょっとずつ変換に流す必要があるのだが、10分ずつ区切るなど音声を一定時間で分割する場合、フロント側でやるのか、バックエンド側でやるのかという問題がある。
まずフロント側の検討だが、javascriptにはそういったいい感じのライブラリはあまり備わっていない。また、そもそもの問題としてフロント側で処理する場合、音声ファイルのデカさ的にユーザーを待たせる時間が増えることが目に見えているので散々試した挙句にあきらめた。なお前述のHTTPリクエストの限界という観点から、分割してアップロードすること自体は必要である。
バックエンド側はpydubというちょうどいい感じに音声を操れるライブラリがあるため分割処理は比較的容易だ。
何も考えず10分で分割した場合、話している途中の音声をぶった切ってしまう可能性がある。こちらも無音部分で音声を区切るsplit_on_silenceを使用すれば音声データに疎い人でも比較的たやすく対処可能だ。
https://algorithm.joho.info/programming/python/pydub-split-on-silence/
しかしこのメソッド、数時間の音声をそのまんま渡すと処理時間がべらぼうにかかって終わらない。ということで本当に区切りたい1分ぐらいのデータだけ渡すように工夫する。それ以外は下記のような処理の速いAudioSegmentで分割する。
https://algorithm.joho.info/programming/python/pydub-split/

処理上の限界を克服する

上記の処理上の限界を克服するため処理の流れは下記のようになった

  1. (フロント)音声ファイルをbyte数で分割してCloud Storageにアップロード
  2. オブジェクトを合体させる(compose)API呼び出し
  3. 2の合体ファイルがCloud Storageに置かれたのをトリガーに、Cloud Functions呼び出し。pydubで時間単位+無音部分で音声分割する
  4. 3の分割ファイルがCloud Storageに置かれたのをトリガーに、Cloud Functions呼び出し。Speech-to-Text APIを呼び出してテキストファイルを生成。
  5. 4のテキストファイルが置かれたのをトリガーにテキストファイルを合体させる(compose)Cloud Functions呼び出し。テキストをダウンロードする署名付きURLを発行してDatastore更新。

1.と4.のアップロードと文字起こしは時間がかかるので、各分割パートの処理が終了するたびにDatastoreを更新して簡単に進捗を見れるようにした。「%」は(処理が終わったファイル数)/(分割したファイル総数)を表している。

1.の参考サイト
https://hapicode.com/javascript/bigfile-upload.html#javascript-slice-%E3%81%A6%E3%82%99%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%88%86%E5%89%B
2.&5.の参考サイト
https://cloud.google.com/storage/docs/composing-objects#permissions-code-samples

また、3の音声分割以降のロジックをフローにするとこんな感じ。
実際は無音部分で分割する処理等があるのでもう少し面倒

リリース後どうなった?

実際の課金はいくらぐらいか

リリースから間もないため現在調査集計中。
料金計算ツールを使用した事前の見積もりでは、Cloud Storage、App Engine、Cloud Functions、Speech-to-Text API合わせて15ドル/月ほどではないかと予想している

状況によって文字起こし精度が非常に落ちる

変換する音声によっては、かなりめちゃくちゃな文章が返ってくる。
考えられる原因は多分以下の通り

1. 変換する音声の質が悪い

比較対象としてニュース原稿をアナウンサーの要領で読み上げてみた。
<原文>

コンビニエンスストアファミリーマートは 深夜利用客向けに食品や日用雑貨を充実させたモデル店舗を、 東京都練馬区武蔵野市で相次いでオープンしました。

ライバル会社ローソンの事業展開に対抗するのが狙いです。

深夜に作りたての弁当が食べられることや従来より広く作られた店内に 女性客も多く来店しているとの事です。

<読み上げた音声を文字起こし>

コンビニエンスストアファミリーマートは、 深夜利用客向けに食品や日用雑貨を充実させたモデル店舗を 東京都練馬区武蔵野市開いてオープンしました

ライバル会社ローソンの事業展開に対抗するのが狙いです

深夜に作りたての弁当が食べられることや、従来より良く作られた店内に 女性客も多く来店しているとのことです。

こちらは精度が良い。滑舌が良い、一人で喋る、雑音がないなどの条件が揃っていると正確になるようだ。

続いて会議の録音(一部分)
<原文>

A:理論値を出すとさ、ダイヤを5個もらえるんだけど、

ダイヤを5個もらえて1回ガチャ10連ガチャ回すのにダイヤ350個やから

B:その手のゲームは、なんていうか、昔のGREEとかの路線を継承してるんで

A:そう、なんか、絶対無料でやらせたらへんっていう強い意思を感じる

B:しかもなんかガチャリセットごとになんか特攻キャラみたいなんを持って

イベント走ったら もらえるポイントみたいなんが4倍とかになって...

 

<読み上げた音声を文字起こし>

ダイヤを5個もらえ(抜け落ち)

(抜け落ち)一回ガチャ10連ガチャ回すのにダイヤ350個やから

(抜け落ち)私のグリーとかあの辺のゲームのあの何て言うか路線を継承してるんで

(抜け落ち)絶対無料でやらせたらへんっていう強い意思を感じる

(抜け落ち)何かもらえるポイントみたいなんが4倍とかになって

日常会話だと個人の喋り方のクセ(イントネーション・発音)が無意識に変わるし、文章にすると文法的に破綻している会話をしていることも多いので、そこを汲み取ってくれない傾向がありそう。
さらに録音の質が悪い話者の発言はまるまる変換されていなかったりする。
会議の参加者それぞれが自分の声を録音しておくなどの対策をすれば精度が向上するだろうか?

2. カスタマイズがあんまり機能しない

音声適応(SpeechContext)の機能がうまく効いてくれない。
このAPIは、まずどう発音したかを認識してから文章に直しているものと思われる。ひらがなカタカナで書き起こして後から漢字に変換するようなイメージだ。
そして音声適応は公式ドキュメントを読む限り、同音異義語がある場合に出力する単語の傾向を指定しているように見える。つまり、そもそも発音の認識が誤っている場合は、いくら音声適応を設定したところで望む単語が出てくることはない。1.の質の悪さも相まって余計そうなる。

また、日本語中に混じる英語表現をアルファベットで登録したい場合も多分うまくいかない。よく使われていそうな単語は除き「スピーチ」を「Speech」と変換するような、カタカナ語の変換は苦手なようでシチュエーションによってはかなり困りそう。
例外的にUSA(ユーエスエー)のような略語はちゃんと出てくるので、アルファベットのカタカナ読みはあまり問題ないように見える。

3.そもそも英語以外は英語に比べて精度が劣る説

クラウド音声認識サービス一般に言えることらしいが、日本語の音声認識は難しいようだ。下記の記事ではその理由について触れられている。
https://www.itmedia.co.jp/news/articles/2104/08/news058.html
ただ個人的には日本が取り立てて難しいというよりは、大手クラウドサービス企業が米国にある以上、非英語言語(非欧州言語)の特性をそこまで親切に汲み取ってくれるわけではない、というだけのことではないのかという気がしている。

4.句読点、段組みなどの文章整形のハードル

このAPIではAutomatic Punctuationのオプションを使えば句読点を打つぐらいまではやってくれる。ただし、1.で挙げた音声の質の問題からあんまりうまく機能してくれなかった。
加えて、改行・段組み・カッコ書き・表記の統一などの文章整形もやってくれない。
当然と言えば当然なのだが、音声認識が文章整形までしてくれる段階まで進化してくれないと、結局のところ筆記メモの代用の使途に留まり、ありがたみもその程度のものにならざるを得ないのではないかという気がしている。

いずれにせよこの界隈を席巻するような高度な音声認識サービスは認知されておらず、しばらくは人力の介在余地多しの状態が続きそう、という印象である。

改良の余地

以下、実装していない部分など改良できそうな部分を箇条書きする

  • ユーザー数が増えた場合の対応を特に考えていない(アクセス数が増えた時何の処理に負荷がかかるか、ユーザー登録自動化はできないかなど)
  • 音声を早送り加工して再生時間を短くしたりすれば処理が高速化できる?
  • APIを同時並行で呼び出すと遅くなる問題は複数アカウントから呼び出せば解決できない?(そもそもアカウントごとの制約なのか?)
  • Cloud Functions第2世代の使用を早々に諦めてしまったが、実はタイムアウト時間を延ばす方法があったりして
  • ソースコードのgit管理とかCI/CDとか、開発環境の整備を一切やっていない
  • 多人数の会話で話者を識別する方法。基本的にチャネル数を増やさないといけないが、音の高低などで話者を識別する外部のサービスやらライブラリとか使えないだろうか。知らんけど
  • ↑それか「参加者は全員自分の声を録音してね」にするか
  • 「Whisper」など他のAPIを使用すれば精度が上がったりしないだろうか

https://prtimes.jp/main/html/rd/p/000000007.000063429.html

要約

  • サービスとしては主にApp Engine, CloudFunctions, Cloud Storage, Datastoreを使用。フロントエンドはVue.js、バックエンドはAPI・バッチともにpythonで実装。
  • 文字起こしサービスを作るうえで面した主な制約は以下の通り。ファイルを分割して処理するなどで対策を行ったがもっとスマートに実装する方法があったかもしれない

・App Engine APIの制約(32MB, 10分)

・Cloud Functionsの制約(540秒)

・Speech-to-Text APIの同時実行数(多いほど処理が遅くなる。アカウントごとの制約?)

  • 音声の分割などをやりたい場合はpydubというライブラリが便利。js(フロント側)はいい感じのライブラリが少ないので基本バックエンドで加工処理した方がいい
  • Speech-to-Text API自体の処理時間は体感 再生時間×0.3~0.7 ぐらい
  • 料金は調査中。事前見積もりでは15ドル/月ぐらいかかると予想
  • できたサービスの質はあんまりよくなかった。複数人かつラフな会話だと、Speech-to-Textの音声認識精度がそこまで良くない
  • 音声認識APIは文章整形など高度なことまでやってくれるわけでもないし、外部向けに見せる文章を作成するにはまだまだ人の手を加えないといけないなという感想

 

過去の私の記事はこちら↓

【フクチョの日記】今年やったゲームのレビュー【🔞見ちゃダメ!】

京大音ゲーサークルOBによる元老院会議議事録 part1/2

【IIDX】専用BGA・ムービー 担当数の多いデザイナー・VJランキング①【現行AC】~わざわざ手作業で数える人~ - 京音メンバーの日記

「いちか、9月中にIIDX DP皆伝は取れ」 - 京音メンバーの日記

バレへんやろ...と思って作ったであろう低難易度手抜き譜面を摘発する会 定例報告 - 京音メンバーの日記